Merge "Fix text on tab was not located in the middle" into udc-dev
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java b/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java
index f56e1ee..36174c6 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java
@@ -20,6 +20,7 @@
 import android.app.JobSchedulerImpl;
 import android.app.SystemServiceRegistry;
 import android.app.tare.EconomyManager;
+import android.app.tare.IEconomyManager;
 import android.content.Context;
 import android.os.DeviceIdleManager;
 import android.os.IDeviceIdleController;
@@ -58,6 +59,7 @@
                 Context.POWER_EXEMPTION_SERVICE, PowerExemptionManager.class,
                 PowerExemptionManager::new);
         SystemServiceRegistry.registerStaticService(
-                Context.RESOURCE_ECONOMY_SERVICE, EconomyManager.class, EconomyManager::new);
+                Context.RESOURCE_ECONOMY_SERVICE, EconomyManager.class,
+                (b) -> new EconomyManager(IEconomyManager.Stub.asInterface(b)));
     }
 }
diff --git a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
index 581ea7a..0bea028 100644
--- a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
+++ b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
@@ -19,7 +19,9 @@
 import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.annotation.SystemService;
+import android.annotation.TestApi;
 import android.content.Context;
+import android.os.RemoteException;
 import android.util.Log;
 
 import java.lang.annotation.Retention;
@@ -30,6 +32,7 @@
  *
  * @hide
  */
+@TestApi
 @SystemService(Context.RESOURCE_ECONOMY_SERVICE)
 public class EconomyManager {
     private static final String TAG = "TARE-" + EconomyManager.class.getSimpleName();
@@ -95,13 +98,17 @@
         }
     }
 
-
+    /** @hide */
+    @TestApi
     public static final int ENABLED_MODE_OFF = 0;
+    /** @hide */
     public static final int ENABLED_MODE_ON = 1;
     /**
      * Go through the motions, tracking events, updating balances and other TARE state values,
      * but don't use TARE to affect actual device behavior.
+     * @hide
      */
+    @TestApi
     public static final int ENABLED_MODE_SHADOW = 2;
 
     /** @hide */
@@ -114,6 +121,7 @@
     public @interface EnabledMode {
     }
 
+    /** @hide */
     public static String enabledModeToString(@EnabledMode int mode) {
         switch (mode) {
             case ENABLED_MODE_OFF: return "ENABLED_MODE_OFF";
@@ -123,11 +131,18 @@
         }
     }
 
+    /** @hide */
+    @TestApi
     public static final String KEY_ENABLE_TARE_MODE = "enable_tare_mode";
+    /** @hide */
     public static final String KEY_ENABLE_POLICY_ALARM = "enable_policy_alarm";
+    /** @hide */
     public static final String KEY_ENABLE_POLICY_JOB_SCHEDULER = "enable_policy_job";
+    /** @hide */
     public static final int DEFAULT_ENABLE_TARE_MODE = ENABLED_MODE_OFF;
+    /** @hide */
     public static final boolean DEFAULT_ENABLE_POLICY_ALARM = true;
+    /** @hide */
     public static final boolean DEFAULT_ENABLE_POLICY_JOB_SCHEDULER = true;
 
     // Keys for AlarmManager TARE factors
@@ -612,4 +627,27 @@
     public static final long DEFAULT_JS_ACTION_JOB_MIN_RUNNING_BASE_PRICE_CAKES = arcToCake(1);
     /** @hide */
     public static final long DEFAULT_JS_ACTION_JOB_TIMEOUT_PENALTY_BASE_PRICE_CAKES = arcToCake(60);
+
+    //////// APIs below ////////
+
+    private final IEconomyManager mService;
+
+    /** @hide */
+    public EconomyManager(IEconomyManager service) {
+        mService = service;
+    }
+
+    /**
+     * Returns the current enabled status of TARE.
+     * @hide
+     */
+    @EnabledMode
+    @TestApi
+    public int getEnabledMode() {
+        try {
+            return mService.getEnabledMode();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/apex/jobscheduler/framework/java/android/app/tare/IEconomyManager.aidl b/apex/jobscheduler/framework/java/android/app/tare/IEconomyManager.aidl
index bb15011..2be0db7 100644
--- a/apex/jobscheduler/framework/java/android/app/tare/IEconomyManager.aidl
+++ b/apex/jobscheduler/framework/java/android/app/tare/IEconomyManager.aidl
@@ -21,4 +21,5 @@
   * {@hide}
   */
 interface IEconomyManager {
+    int getEnabledMode();
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
index 7f6a75e..c707069 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -1351,6 +1351,11 @@
         }
 
         @Override
+        public int getEnabledMode() {
+            return InternalResourceService.this.getEnabledMode();
+        }
+
+        @Override
         public int handleShellCommand(@NonNull ParcelFileDescriptor in,
                 @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
                 @NonNull String[] args) {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 5999e3c4..2d9a99c 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -777,6 +777,17 @@
 
 }
 
+package android.app.tare {
+
+  public class EconomyManager {
+    method public int getEnabledMode();
+    field public static final int ENABLED_MODE_OFF = 0; // 0x0
+    field public static final int ENABLED_MODE_SHADOW = 2; // 0x2
+    field public static final String KEY_ENABLE_TARE_MODE = "enable_tare_mode";
+  }
+
+}
+
 package android.app.usage {
 
   public class StorageStatsManager {
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index c131ce5..e31486f 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -2354,8 +2354,7 @@
                 return mUiAutomation;
             }
             if (mustCreateNewAutomation) {
-                mUiAutomation = new UiAutomation(getTargetContext().getMainLooper(),
-                        mUiAutomationConnection);
+                mUiAutomation = new UiAutomation(getTargetContext(), mUiAutomationConnection);
             } else {
                 mUiAutomation.disconnect();
             }
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index 658e084..247d5bc 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -16,6 +16,8 @@
 
 package android.app;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+
 import android.accessibilityservice.AccessibilityGestureEvent;
 import android.accessibilityservice.AccessibilityService;
 import android.accessibilityservice.AccessibilityService.Callbacks;
@@ -30,6 +32,7 @@
 import android.annotation.SuppressLint;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -45,6 +48,7 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.ArraySet;
 import android.util.DebugUtils;
 import android.util.Log;
@@ -69,8 +73,10 @@
 import android.view.inputmethod.EditorInfo;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback;
 import com.android.internal.inputmethod.RemoteAccessibilityInputConnection;
+import com.android.internal.util.Preconditions;
 import com.android.internal.util.function.pooled.PooledLambda;
 
 import libcore.io.IoUtils;
@@ -202,6 +208,8 @@
 
     private final IUiAutomationConnection mUiAutomationConnection;
 
+    private final int mDisplayId;
+
     private HandlerThread mRemoteCallbackThread;
 
     private IAccessibilityServiceClient mClient;
@@ -261,24 +269,49 @@
 
     /**
      * Creates a new instance that will handle callbacks from the accessibility
+     * layer on the thread of the provided context main looper and perform requests for privileged
+     * operations on the provided connection, and filtering display-related features to the display
+     * associated with the context (or the user running the test, on devices that
+     * {@link UserManager#isVisibleBackgroundUsersSupported() support visible background users}).
+     *
+     * @param context the context associated with the automation
+     * @param connection The connection for performing privileged operations.
+     *
+     * @hide
+     */
+    public UiAutomation(Context context, IUiAutomationConnection connection) {
+        this(getDisplayId(context), context.getMainLooper(), connection);
+    }
+
+    /**
+     * Creates a new instance that will handle callbacks from the accessibility
      * layer on the thread of the provided looper and perform requests for privileged
      * operations on the provided connection.
      *
      * @param looper The looper on which to execute accessibility callbacks.
      * @param connection The connection for performing privileged operations.
      *
+     * @deprecated use {@link #UiAutomation(Context, IUiAutomationConnection)} instead
+     *
      * @hide
      */
+    @Deprecated
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     public UiAutomation(Looper looper, IUiAutomationConnection connection) {
-        if (looper == null) {
-            throw new IllegalArgumentException("Looper cannot be null!");
-        }
-        if (connection == null) {
-            throw new IllegalArgumentException("Connection cannot be null!");
-        }
+        this(DEFAULT_DISPLAY, looper, connection);
+        Log.w(LOG_TAG, "Created with deprecatead constructor, assumes DEFAULT_DISPLAY");
+    }
+
+    private UiAutomation(int displayId, Looper looper, IUiAutomationConnection connection) {
+        Preconditions.checkArgument(looper != null, "Looper cannot be null!");
+        Preconditions.checkArgument(connection != null, "Connection cannot be null!");
+
         mLocalCallbackHandler = new Handler(looper);
         mUiAutomationConnection = connection;
+        mDisplayId = displayId;
+
+        Log.i(LOG_TAG, "Initialized for user " + Process.myUserHandle().getIdentifier()
+                + " on display " + mDisplayId);
     }
 
     /**
@@ -719,8 +752,14 @@
     }
 
     /**
-     * Gets the windows on the screen of the default display. This method returns only the windows
-     * that a sighted user can interact with, as opposed to all windows.
+     * Gets the windows on the screen associated with the {@link UiAutomation} context (usually the
+     * {@link android.view.Display#DEFAULT_DISPLAY default display).
+     *
+     * <p>
+     * This method returns only the windows that a sighted user can interact with, as opposed to
+     * all windows.
+
+     * <p>
      * For example, if there is a modal dialog shown and the user cannot touch
      * anything behind it, then only the modal window will be reported
      * (assuming it is the top one). For convenience the returned windows
@@ -730,21 +769,23 @@
      * <strong>Note:</strong> In order to access the windows you have to opt-in
      * to retrieve the interactive windows by setting the
      * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag.
-     * </p>
      *
      * @return The windows if there are windows such, otherwise an empty list.
      * @throws IllegalStateException If the connection to the accessibility subsystem is not
      *            established.
      */
     public List<AccessibilityWindowInfo> getWindows() {
+        if (DEBUG) {
+            Log.d(LOG_TAG, "getWindows(): returning windows for display " + mDisplayId);
+        }
         final int connectionId;
         synchronized (mLock) {
             throwIfNotConnectedLocked();
             connectionId = mConnectionId;
         }
         // Calling out without a lock held.
-        return AccessibilityInteractionClient.getInstance()
-                .getWindows(connectionId);
+        return AccessibilityInteractionClient.getInstance().getWindowsOnDisplay(connectionId,
+                mDisplayId);
     }
 
     /**
@@ -1112,8 +1153,10 @@
      * @return The screenshot bitmap on success, null otherwise.
      */
     public Bitmap takeScreenshot() {
-        Display display = DisplayManagerGlobal.getInstance()
-                .getRealDisplay(Display.DEFAULT_DISPLAY);
+        if (DEBUG) {
+            Log.d(LOG_TAG, "Taking screenshot of display " + mDisplayId);
+        }
+        Display display = DisplayManagerGlobal.getInstance().getRealDisplay(mDisplayId);
         Point displaySize = new Point();
         display.getRealSize(displaySize);
 
@@ -1126,10 +1169,12 @@
             screenShot = mUiAutomationConnection.takeScreenshot(
                     new Rect(0, 0, displaySize.x, displaySize.y));
             if (screenShot == null) {
+                Log.e(LOG_TAG, "mUiAutomationConnection.takeScreenshot() returned null for display "
+                        + mDisplayId);
                 return null;
             }
         } catch (RemoteException re) {
-            Log.e(LOG_TAG, "Error while taking screenshot!", re);
+            Log.e(LOG_TAG, "Error while taking screenshot of display " + mDisplayId, re);
             return null;
         }
 
@@ -1509,6 +1554,14 @@
         return executeShellCommandInternal(command, true /* includeStderr */);
     }
 
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public int getDisplayId() {
+        return mDisplayId;
+    }
+
     private ParcelFileDescriptor[] executeShellCommandInternal(
             String command, boolean includeStderr) {
         warnIfBetterCommand(command);
@@ -1564,6 +1617,7 @@
         final StringBuilder stringBuilder = new StringBuilder();
         stringBuilder.append("UiAutomation@").append(Integer.toHexString(hashCode()));
         stringBuilder.append("[id=").append(mConnectionId);
+        stringBuilder.append(", displayId=").append(mDisplayId);
         stringBuilder.append(", flags=").append(mFlags);
         stringBuilder.append("]");
         return stringBuilder.toString();
@@ -1601,6 +1655,55 @@
         return (mFlags & UiAutomation.FLAG_DONT_USE_ACCESSIBILITY) == 0;
     }
 
+    /**
+     * Gets the display id associated with the UiAutomation context.
+     *
+     * <p><b>NOTE: </b> must be a static method because it's called from a constructor to call
+     * another one.
+     */
+    private static int getDisplayId(Context context) {
+        Preconditions.checkArgument(context != null, "Context cannot be null!");
+
+        UserManager userManager = context.getSystemService(UserManager.class);
+        // TODO(b/255426725): given that this is a temporary solution until a11y supports multiple
+        // users, the display is only set on devices that support that
+        if (!userManager.isVisibleBackgroundUsersSupported()) {
+            return DEFAULT_DISPLAY;
+        }
+
+        int displayId = context.getDisplayId();
+        if (displayId == Display.INVALID_DISPLAY) {
+            // Shouldn't happen, but we better handle it
+            Log.e(LOG_TAG, "UiAutomation created UI context with invalid display id, assuming it's"
+                    + " running in the display assigned to the user");
+            return getMainDisplayIdAssignedToUser(context, userManager);
+        }
+
+        if (displayId != DEFAULT_DISPLAY) {
+            if (DEBUG) {
+                Log.d(LOG_TAG, "getDisplayId(): returning context's display (" + displayId + ")");
+            }
+            // Context is explicitly setting the display, so we respect that...
+            return displayId;
+        }
+        // ...otherwise, we need to get the display the test's user is running on
+        int userDisplayId = getMainDisplayIdAssignedToUser(context, userManager);
+        if (DEBUG) {
+            Log.d(LOG_TAG, "getDisplayId(): returning user's display (" + userDisplayId + ")");
+        }
+        return userDisplayId;
+    }
+
+    private static int getMainDisplayIdAssignedToUser(Context context, UserManager userManager) {
+        if (!userManager.isUserVisible()) {
+            // Should also not happen, but ...
+            Log.e(LOG_TAG, "User (" + context.getUserId() + ") is not visible, using "
+                    + "DEFAULT_DISPLAY");
+            return DEFAULT_DISPLAY;
+        }
+        return userManager.getMainDisplayIdAssignedToUser();
+    }
+
     private class IAccessibilityServiceClientImpl extends IAccessibilityServiceClientWrapper {
 
         public IAccessibilityServiceClientImpl(Looper looper, int generationId) {
@@ -1621,6 +1724,7 @@
                     if (DEBUG) {
                         Log.d(LOG_TAG, "init(): connectionId=" + connectionId + ", windowToken="
                                 + windowToken + ", user=" + Process.myUserHandle()
+                                + ", UiAutomation.mDisplay=" + UiAutomation.this.mDisplayId
                                 + ", mGenerationId=" + mGenerationId
                                 + ", UiAutomation.mGenerationId="
                                 + UiAutomation.this.mGenerationId);
diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java
index 13e800e..d96a9d1 100644
--- a/core/java/android/app/UiAutomationConnection.java
+++ b/core/java/android/app/UiAutomationConnection.java
@@ -22,6 +22,7 @@
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.graphics.Bitmap;
@@ -117,7 +118,8 @@
                 throw new IllegalStateException("Already connected.");
             }
             mOwningUid = Binder.getCallingUid();
-            registerUiTestAutomationServiceLocked(client, flags);
+            registerUiTestAutomationServiceLocked(client,
+                    Binder.getCallingUserHandle().getIdentifier(), flags);
             storeRotationStateLocked();
         }
     }
@@ -553,7 +555,7 @@
     }
 
     private void registerUiTestAutomationServiceLocked(IAccessibilityServiceClient client,
-            int flags) {
+            @UserIdInt int userId, int flags) {
         IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
                 ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
         final AccessibilityServiceInfo info = new AccessibilityServiceInfo();
@@ -571,10 +573,11 @@
         try {
             // Calling out with a lock held is fine since if the system
             // process is gone the client calling in will be killed.
-            manager.registerUiTestAutomationService(mToken, client, info, flags);
+            manager.registerUiTestAutomationService(mToken, client, info, userId, flags);
             mClient = client;
         } catch (RemoteException re) {
-            throw new IllegalStateException("Error while registering UiTestAutomationService.", re);
+            throw new IllegalStateException("Error while registering UiTestAutomationService for "
+                    + "user " + userId + ".", re);
         }
     }
 
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 9f9c222..7383e63 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -493,7 +493,7 @@
          * @hide
          */
         @TestApi
-        public static final int RESOURCES_SDK_INT = SDK_INT + ACTIVE_CODENAMES.length;
+        public static final int RESOURCES_SDK_INT = SDK_INT;
 
         /**
          * The current lowest supported value of app target SDK. Applications targeting
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 1fac142..390503b 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -62,7 +62,7 @@
             in IAccessibilityInteractionConnection connection);
 
     void registerUiTestAutomationService(IBinder owner, IAccessibilityServiceClient client,
-        in AccessibilityServiceInfo info, int flags);
+        in AccessibilityServiceInfo info, int userId, int flags);
 
     void unregisterUiTestAutomationService(IAccessibilityServiceClient client);
 
diff --git a/core/tests/coretests/src/android/app/UiAutomationTest.java b/core/tests/coretests/src/android/app/UiAutomationTest.java
new file mode 100644
index 0000000..3ac5057
--- /dev/null
+++ b/core/tests/coretests/src/android/app/UiAutomationTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Looper;
+import android.os.UserManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class UiAutomationTest {
+
+    private static final int SECONDARY_DISPLAY_ID = 42;
+    private static final int DISPLAY_ID_ASSIGNED_TO_USER = 108;
+
+    private final Looper mLooper = Looper.getMainLooper();
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private UserManager mUserManager;
+    @Mock
+    private IUiAutomationConnection mConnection;
+
+    @Before
+    public void setFixtures() {
+        when(mContext.getMainLooper()).thenReturn(mLooper);
+        mockSystemService(UserManager.class, mUserManager);
+
+        // Set default expectations
+        mockVisibleBackgroundUsersSupported(/* supported= */ false);
+        mockUserVisibility(/* visible= */ true);
+        // make sure it's not used, unless explicitly mocked
+        mockDisplayAssignedToUser(INVALID_DISPLAY);
+        mockContextDisplay(DEFAULT_DISPLAY);
+    }
+
+    @Test
+    public void testContextConstructor_nullContext() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new UiAutomation((Context) null, mConnection));
+    }
+
+    @Test
+    public void testContextConstructor_nullConnection() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new UiAutomation(mContext, (IUiAutomationConnection) null));
+    }
+
+    @Test
+    public void testGetDisplay_contextWithSecondaryDisplayId() {
+        mockContextDisplay(SECONDARY_DISPLAY_ID);
+
+        UiAutomation uiAutomation = new UiAutomation(mContext, mConnection);
+
+        // It's always DEFAULT_DISPLAY regardless, unless the device supports visible bg users
+        assertWithMessage("getDisplayId()").that(uiAutomation.getDisplayId())
+                .isEqualTo(DEFAULT_DISPLAY);
+    }
+
+    @Test
+    public void testGetDisplay_contextWithInvalidDisplayId() {
+        mockContextDisplay(INVALID_DISPLAY);
+
+        UiAutomation uiAutomation = new UiAutomation(mContext, mConnection);
+
+        assertWithMessage("getDisplayId()").that(uiAutomation.getDisplayId())
+                .isEqualTo(DEFAULT_DISPLAY);
+    }
+
+    @Test
+    public void testGetDisplay_visibleBgUsers() {
+        mockVisibleBackgroundUsersSupported(/* supported= */ true);
+        mockContextDisplay(SECONDARY_DISPLAY_ID);
+        // Should be using display from context, not from user
+        mockDisplayAssignedToUser(DISPLAY_ID_ASSIGNED_TO_USER);
+
+        UiAutomation uiAutomation = new UiAutomation(mContext, mConnection);
+
+        assertWithMessage("getDisplayId()").that(uiAutomation.getDisplayId())
+                .isEqualTo(SECONDARY_DISPLAY_ID);
+    }
+
+    @Test
+    public void testGetDisplay_visibleBgUsers_contextWithInvalidDisplayId() {
+        mockVisibleBackgroundUsersSupported(/* supported= */ true);
+        mockContextDisplay(INVALID_DISPLAY);
+        mockDisplayAssignedToUser(DISPLAY_ID_ASSIGNED_TO_USER);
+
+        UiAutomation uiAutomation = new UiAutomation(mContext, mConnection);
+
+        assertWithMessage("getDisplayId()").that(uiAutomation.getDisplayId())
+                .isEqualTo(DISPLAY_ID_ASSIGNED_TO_USER);
+    }
+
+    private <T> void mockSystemService(Class<T> svcClass, T svc) {
+        String svcName = svcClass.getName();
+        when(mContext.getSystemServiceName(svcClass)).thenReturn(svcName);
+        when(mContext.getSystemService(svcName)).thenReturn(svc);
+    }
+
+    private void mockVisibleBackgroundUsersSupported(boolean supported) {
+        when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(supported);
+    }
+
+    private void mockContextDisplay(int displayId) {
+        when(mContext.getDisplayId()).thenReturn(displayId);
+    }
+
+    private void mockDisplayAssignedToUser(int displayId) {
+        when(mUserManager.getMainDisplayIdAssignedToUser()).thenReturn(displayId);
+    }
+
+    private void mockUserVisibility(boolean visible) {
+        when(mUserManager.isUserVisible()).thenReturn(visible);
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
index e22f3f0..5fbb4c3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
@@ -37,8 +37,6 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -287,16 +285,7 @@
             return defaultValue;
         }
 
-        try {
-            Method method = mService.getClass().getDeclaredMethod("getDeviceSideInternal",
-                    BluetoothDevice.class);
-            method.setAccessible(true);
-            return (int) method.invoke(mService, device);
-        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
-            Log.e(TAG, "fail to get getDeviceSideInternal\n" + e.toString() + "\n"
-                    + Log.getStackTraceString(new Throwable()));
-            return defaultValue;
-        }
+        return mService.getDeviceSide(device);
     }
 
     /**
@@ -313,17 +302,7 @@
             return defaultValue;
         }
 
-        try {
-            Method method = mService.getClass().getDeclaredMethod("getDeviceModeInternal",
-                    BluetoothDevice.class);
-            method.setAccessible(true);
-            return (int) method.invoke(mService, device);
-        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
-            Log.e(TAG, "fail to get getDeviceModeInternal\n" + e.toString() + "\n"
-                    + Log.getStackTraceString(new Throwable()));
-
-            return defaultValue;
-        }
+        return mService.getDeviceMode(device);
     }
 
     public String toString() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 87a7758..db38d34 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -75,6 +75,7 @@
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
@@ -115,6 +116,7 @@
     private final SessionTracker mSessionTracker;
     private final Optional<SideFpsController> mSideFpsController;
     private final FalsingA11yDelegate mFalsingA11yDelegate;
+    private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor;
     private int mTranslationY;
     // Whether the volume keys should be handled by keyguard. If true, then
     // they will be handled here for specific media types such as music, otherwise
@@ -300,6 +302,7 @@
         @Override
         public void onSwipeUp() {
             if (!mUpdateMonitor.isFaceDetectionRunning()) {
+                mKeyguardFaceAuthInteractor.onSwipeUpOnBouncer();
                 boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(
                         FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
                 mKeyguardSecurityCallback.userActivity();
@@ -389,7 +392,8 @@
             FalsingA11yDelegate falsingA11yDelegate,
             TelephonyManager telephonyManager,
             ViewMediatorCallback viewMediatorCallback,
-            AudioManager audioManager
+            AudioManager audioManager,
+            KeyguardFaceAuthInteractor keyguardFaceAuthInteractor
     ) {
         super(view);
         mLockPatternUtils = lockPatternUtils;
@@ -414,6 +418,7 @@
         mTelephonyManager = telephonyManager;
         mViewMediatorCallback = viewMediatorCallback;
         mAudioManager = audioManager;
+        mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 350c4ed..33a8224 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -154,6 +154,15 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.dump.DumpsysTableLogger;
+import com.android.systemui.keyguard.domain.interactor.FaceAuthenticationListener;
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
+import com.android.systemui.keyguard.shared.model.AcquiredAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.AuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.DetectionStatus;
+import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.FailedAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus;
 import com.android.systemui.keyguard.shared.model.SysUiFaceAuthenticateOptions;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.WeatherData;
@@ -372,6 +381,8 @@
     private final FingerprintManager mFpm;
     @Nullable
     private final FaceManager mFaceManager;
+    @Nullable
+    private KeyguardFaceAuthInteractor mFaceAuthInteractor;
     private final LockPatternUtils mLockPatternUtils;
     @VisibleForTesting
     @DevicePostureInt
@@ -1165,8 +1176,21 @@
         Trace.endSection();
     }
 
+    /**
+     * @deprecated This is being migrated to use modern architecture, this method is visible purely
+     * for bridging the gap while the migration is active.
+     */
     private void handleFaceAuthFailed() {
         Assert.isMainThread();
+        String reason =
+                mKeyguardBypassController.canBypass() ? "bypass"
+                        : mAlternateBouncerShowing ? "alternateBouncer"
+                                : mPrimaryBouncerFullyShown ? "bouncer"
+                                        : "udfpsFpDown";
+        requestActiveUnlock(
+                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
+                "faceFailure-" + reason);
+
         mLogger.d("onFaceAuthFailed");
         mFaceCancelSignal = null;
         setFaceRunningState(BIOMETRIC_STATE_STOPPED);
@@ -1180,6 +1204,10 @@
                 mContext.getString(R.string.kg_face_not_recognized));
     }
 
+    /**
+     * @deprecated This is being migrated to use modern architecture, this method is visible purely
+     * for bridging the gap while the migration is active.
+     */
     private void handleFaceAcquired(int acquireInfo) {
         Assert.isMainThread();
         mLogger.logFaceAcquired(acquireInfo);
@@ -1189,8 +1217,19 @@
                 cb.onBiometricAcquired(FACE, acquireInfo);
             }
         }
+
+        if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceAcquireInfo(
+                acquireInfo)) {
+            requestActiveUnlock(
+                    ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
+                    "faceAcquireInfo-" + acquireInfo);
+        }
     }
 
+    /**
+     * @deprecated This is being migrated to use modern architecture, this method is visible purely
+     * for bridging the gap while the migration is active.
+     */
     private void handleFaceAuthenticated(int authUserId, boolean isStrongBiometric) {
         Trace.beginSection("KeyGuardUpdateMonitor#handlerFaceAuthenticated");
         try {
@@ -1203,7 +1242,7 @@
                 mLogger.logFaceAuthForWrongUser(authUserId);
                 return;
             }
-            if (isFaceDisabled(userId)) {
+            if (!isFaceAuthInteractorEnabled() && isFaceDisabled(userId)) {
                 mLogger.logFaceAuthDisabledForUser(userId);
                 return;
             }
@@ -1215,6 +1254,10 @@
         Trace.endSection();
     }
 
+    /**
+     * @deprecated This is being migrated to use modern architecture, this method is visible purely
+     * for bridging the gap while the migration is active.
+     */
     private void handleFaceHelp(int msgId, String helpString) {
         Assert.isMainThread();
         mLogger.logFaceAuthHelpMsg(msgId, helpString);
@@ -1226,22 +1269,10 @@
         }
     }
 
-    private final Runnable mRetryFaceAuthentication = new Runnable() {
-        @Override
-        public void run() {
-            mLogger.logRetryingAfterFaceHwUnavailable(mHardwareFaceUnavailableRetryCount);
-            updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
-                    FACE_AUTH_TRIGGERED_RETRY_AFTER_HW_UNAVAILABLE);
-        }
-    };
-
-    private void onFaceCancelNotReceived() {
-        mLogger.e("Face cancellation not received, transitioning to STOPPED");
-        mFaceRunningState = BIOMETRIC_STATE_STOPPED;
-        KeyguardUpdateMonitor.this.updateFaceListeningState(BIOMETRIC_ACTION_STOP,
-                FACE_AUTH_STOPPED_FACE_CANCEL_NOT_RECEIVED);
-    }
-
+    /**
+     * @deprecated This is being migrated to use modern architecture, this method is visible purely
+     * for bridging the gap while the migration is active.
+     */
     private void handleFaceError(int msgId, final String originalErrMsg) {
         Assert.isMainThread();
         String errString = originalErrMsg;
@@ -1299,6 +1330,28 @@
         if (lockedOutStateChanged) {
             notifyLockedOutStateChanged(FACE);
         }
+
+        if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceError(msgId)) {
+            requestActiveUnlock(
+                    ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
+                    "faceError-" + msgId);
+        }
+    }
+
+    private final Runnable mRetryFaceAuthentication = new Runnable() {
+        @Override
+        public void run() {
+            mLogger.logRetryingAfterFaceHwUnavailable(mHardwareFaceUnavailableRetryCount);
+            updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
+                    FACE_AUTH_TRIGGERED_RETRY_AFTER_HW_UNAVAILABLE);
+        }
+    };
+
+    private void onFaceCancelNotReceived() {
+        mLogger.e("Face cancellation not received, transitioning to STOPPED");
+        mFaceRunningState = BIOMETRIC_STATE_STOPPED;
+        KeyguardUpdateMonitor.this.updateFaceListeningState(BIOMETRIC_ACTION_STOP,
+                FACE_AUTH_STOPPED_FACE_CANCEL_NOT_RECEIVED);
     }
 
     private void handleFaceLockoutReset(@LockoutMode int mode) {
@@ -1343,10 +1396,61 @@
         return mFingerprintRunningState == BIOMETRIC_STATE_RUNNING;
     }
 
+    /**
+     * @deprecated This is being migrated to use modern architecture.
+     */
+    @Deprecated
     public boolean isFaceDetectionRunning() {
+        if (isFaceAuthInteractorEnabled()) {
+            return getFaceAuthInteractor().isRunning();
+        }
         return mFaceRunningState == BIOMETRIC_STATE_RUNNING;
     }
 
+    private boolean isFaceAuthInteractorEnabled() {
+        return mFaceAuthInteractor != null && mFaceAuthInteractor.isEnabled();
+    }
+
+    private @Nullable KeyguardFaceAuthInteractor getFaceAuthInteractor() {
+        return mFaceAuthInteractor;
+    }
+
+    /**
+     * Set the face auth interactor that should be used for initiating face authentication.
+     */
+    public void setFaceAuthInteractor(@Nullable KeyguardFaceAuthInteractor faceAuthInteractor) {
+        mFaceAuthInteractor = faceAuthInteractor;
+        mFaceAuthInteractor.registerListener(mFaceAuthenticationListener);
+    }
+
+    private FaceAuthenticationListener mFaceAuthenticationListener =
+            new FaceAuthenticationListener() {
+                @Override
+                public void onAuthenticationStatusChanged(@NonNull AuthenticationStatus status) {
+                    if (status instanceof AcquiredAuthenticationStatus) {
+                        handleFaceAcquired(
+                                ((AcquiredAuthenticationStatus) status).getAcquiredInfo());
+                    } else if (status instanceof ErrorAuthenticationStatus) {
+                        ErrorAuthenticationStatus error = (ErrorAuthenticationStatus) status;
+                        handleFaceError(error.getMsgId(), error.getMsg());
+                    } else if (status instanceof FailedAuthenticationStatus) {
+                        handleFaceAuthFailed();
+                    } else if (status instanceof HelpAuthenticationStatus) {
+                        HelpAuthenticationStatus helpMsg = (HelpAuthenticationStatus) status;
+                        handleFaceHelp(helpMsg.getMsgId(), helpMsg.getMsg());
+                    } else if (status instanceof SuccessAuthenticationStatus) {
+                        FaceManager.AuthenticationResult result =
+                                ((SuccessAuthenticationStatus) status).getSuccessResult();
+                        handleFaceAuthenticated(result.getUserId(), result.isStrongBiometric());
+                    }
+                }
+
+                @Override
+                public void onDetectionStatusChanged(@NonNull DetectionStatus status) {
+                    handleFaceAuthenticated(status.getUserId(), status.isStrongBiometric());
+                }
+            };
+
     private boolean isTrustDisabled() {
         // Don't allow trust agent if device is secured with a SIM PIN. This is here
         // mainly because there's no other way to prompt the user to enter their SIM PIN
@@ -1360,6 +1464,10 @@
                 || isSimPinSecure();
     }
 
+    /**
+     * @deprecated This method is not needed anymore with the new face auth system.
+     */
+    @Deprecated
     private boolean isFaceDisabled(int userId) {
         // TODO(b/140035044)
         return whitelistIpcs(() ->
@@ -1371,7 +1479,10 @@
     /**
      * @return whether the current user has been authenticated with face. This may be true
      * on the lockscreen if the user doesn't have bypass enabled.
+     *
+     * @deprecated This is being migrated to use modern architecture.
      */
+    @Deprecated
     public boolean getIsFaceAuthenticated() {
         boolean faceAuthenticated = false;
         BiometricAuthenticated bioFaceAuthenticated = mUserFaceAuthenticated.get(getCurrentUser());
@@ -1619,6 +1730,9 @@
     void setAssistantVisible(boolean assistantVisible) {
         mAssistantVisible = assistantVisible;
         mLogger.logAssistantVisible(mAssistantVisible);
+        if (isFaceAuthInteractorEnabled()) {
+            mFaceAuthInteractor.onAssistantTriggeredOnLockScreen();
+        }
         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
                 FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED);
         if (mAssistantVisible) {
@@ -1832,54 +1946,27 @@
 
                 @Override
                 public void onAuthenticationFailed() {
-                        String reason =
-                                mKeyguardBypassController.canBypass() ? "bypass"
-                                        : mAlternateBouncerShowing ? "alternateBouncer"
-                                                : mPrimaryBouncerFullyShown ? "bouncer"
-                                                        : "udfpsFpDown";
-                        requestActiveUnlock(
-                                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
-                                "faceFailure-" + reason);
-
                     handleFaceAuthFailed();
                 }
 
                 @Override
                 public void onAuthenticationSucceeded(FaceManager.AuthenticationResult result) {
-                    Trace.beginSection("KeyguardUpdateMonitor#onAuthenticationSucceeded");
                     handleFaceAuthenticated(result.getUserId(), result.isStrongBiometric());
-                    Trace.endSection();
                 }
 
                 @Override
                 public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
-                    if (mFaceAcquiredInfoIgnoreList.contains(helpMsgId)) {
-                        return;
-                    }
                     handleFaceHelp(helpMsgId, helpString.toString());
                 }
 
                 @Override
                 public void onAuthenticationError(int errMsgId, CharSequence errString) {
                     handleFaceError(errMsgId, errString.toString());
-
-                    if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceError(errMsgId)) {
-                        requestActiveUnlock(
-                                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
-                                "faceError-" + errMsgId);
-                    }
                 }
 
                 @Override
                 public void onAuthenticationAcquired(int acquireInfo) {
                     handleFaceAcquired(acquireInfo);
-
-                    if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceAcquireInfo(
-                            acquireInfo)) {
-                        requestActiveUnlock(
-                                ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
-                                "faceAcquireInfo-" + acquireInfo);
-                    }
                 }
     };
 
@@ -2628,7 +2715,9 @@
      * @param reason One of the reasons {@link FaceAuthApiRequestReason} on why this API is being
      * invoked.
      * @return current face auth detection state, true if it is running.
+     * @deprecated This is being migrated to use modern architecture.
      */
+    @Deprecated
     public boolean requestFaceAuth(@FaceAuthApiRequestReason String reason) {
         mLogger.logFaceAuthRequested(reason);
         updateFaceListeningState(BIOMETRIC_ACTION_START, apiRequestReasonToUiEvent(reason));
@@ -2643,6 +2732,7 @@
     }
 
     private void updateFaceListeningState(int action, @NonNull FaceAuthUiEvent faceAuthUiEvent) {
+        if (isFaceAuthInteractorEnabled()) return;
         // If this message exists, we should not authenticate again until this message is
         // consumed by the handler
         if (mHandler.hasMessages(MSG_BIOMETRIC_AUTHENTICATION_CONTINUE)) {
@@ -3154,6 +3244,9 @@
     }
 
     public boolean isFaceLockedOut() {
+        if (isFaceAuthInteractorEnabled()) {
+            return getFaceAuthInteractor().isLockedOut();
+        }
         return mFaceLockedOutPermanent;
     }
 
@@ -3202,13 +3295,23 @@
         return mIsUnlockWithFingerprintPossible.getOrDefault(userId, false);
     }
 
+    /**
+     * @deprecated This is being migrated to use modern architecture.
+     */
+    @Deprecated
     private boolean isUnlockWithFacePossible(int userId) {
+        if (isFaceAuthInteractorEnabled()) {
+            return getFaceAuthInteractor().canFaceAuthRun();
+        }
         return isFaceAuthEnabledForUser(userId) && !isFaceDisabled(userId);
     }
 
     /**
      * If face hardware is available, user has enrolled and enabled auth via setting.
+     *
+     * @deprecated This is being migrated to use modern architecture.
      */
+    @Deprecated
     public boolean isFaceAuthEnabledForUser(int userId) {
         // TODO (b/242022358), make this rely on onEnrollmentChanged event and update it only once.
         updateFaceEnrolled(userId);
@@ -3232,6 +3335,7 @@
     }
 
     private void stopListeningForFace(@NonNull FaceAuthUiEvent faceAuthUiEvent) {
+        if (isFaceAuthInteractorEnabled()) return;
         mLogger.v("stopListeningForFace()");
         mLogger.logStoppedListeningForFace(mFaceRunningState, faceAuthUiEvent.getReason());
         mUiEventLogger.log(faceAuthUiEvent, getKeyguardSessionId());
@@ -4098,6 +4202,9 @@
         mStatusBarStateController.removeCallback(mStatusBarStateControllerListener);
         mTelephonyListenerManager.removeActiveDataSubscriptionIdListener(mPhoneStateListener);
         mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionListener);
+        if (isFaceAuthInteractorEnabled()) {
+            mFaceAuthInteractor.unregisterListener(mFaceAuthenticationListener);
+        }
 
         if (mDeviceProvisionedObserver != null) {
             mContext.getContentResolver().unregisterContentObserver(mDeviceProvisionedObserver);
@@ -4127,6 +4234,7 @@
         pw.println("  getUserHasTrust()=" + getUserHasTrust(getCurrentUser()));
         pw.println("  getUserUnlockedWithBiometric()="
                 + getUserUnlockedWithBiometric(getCurrentUser()));
+        pw.println("  isFaceAuthInteractorEnabled: " + isFaceAuthInteractorEnabled());
         pw.println("  SIM States:");
         for (SimData data : mSimDatas.values()) {
             pw.println("    " + data.toString());
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt
index d15a2af..6c62a39 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt
@@ -39,7 +39,7 @@
     private fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) =
         mainExecutor.execute {
             action?.let {
-                if (event.tracking || event.expanded) {
+                if (event.tracking || (event.expanded && event.fraction > 0)) {
                     Log.v(TAG, "Detected panel interaction, event: $event")
                     it.onPanelInteraction.run()
                     disable()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index aabdafb..7a23759 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -83,6 +83,7 @@
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.FalsingManager;
@@ -151,6 +152,7 @@
     @NonNull private final DumpManager mDumpManager;
     @NonNull private final SystemUIDialogManager mDialogManager;
     @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @NonNull private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor;
     @NonNull private final VibratorHelper mVibrator;
     @NonNull private final FeatureFlags mFeatureFlags;
     @NonNull private final FalsingManager mFalsingManager;
@@ -818,7 +820,8 @@
             @NonNull AlternateBouncerInteractor alternateBouncerInteractor,
             @NonNull SecureSettings secureSettings,
             @NonNull InputManager inputManager,
-            @NonNull UdfpsUtils udfpsUtils) {
+            @NonNull UdfpsUtils udfpsUtils,
+            @NonNull KeyguardFaceAuthInteractor keyguardFaceAuthInteractor) {
         mContext = context;
         mExecution = execution;
         mVibrator = vibrator;
@@ -882,6 +885,7 @@
                     }
                     return Unit.INSTANCE;
                 });
+        mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor;
 
         final UdfpsOverlayController mUdfpsOverlayController = new UdfpsOverlayController();
         mFingerprintManager.setUdfpsOverlayController(mUdfpsOverlayController);
@@ -1141,6 +1145,7 @@
         if (!mOnFingerDown) {
             playStartHaptic();
 
+            mKeyguardFaceAuthInteractor.onUdfpsSensorTouched();
             if (!mKeyguardUpdateMonitor.isFaceDetectionRunning()) {
                 mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardFaceAuthNotSupportedModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardFaceAuthNotSupportedModule.kt
new file mode 100644
index 0000000..a44df7e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardFaceAuthNotSupportedModule.kt
@@ -0,0 +1,35 @@
+/*
+ *   Copyright (C) 2023 The Android Open Source Project
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+package com.android.systemui.keyguard.dagger
+
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
+import com.android.systemui.keyguard.domain.interactor.NoopKeyguardFaceAuthInteractor
+import dagger.Binds
+import dagger.Module
+
+/**
+ * Module that provides bindings for face auth classes that are injected into SysUI components that
+ * are used across different SysUI variants, where face auth is not supported.
+ *
+ * Some variants that do not support face authentication can install this module to provide a no-op
+ * implementation of the interactor.
+ */
+@Module
+interface KeyguardFaceAuthNotSupportedModule {
+    @Binds
+    fun keyguardFaceAuthInteractor(impl: NoopKeyguardFaceAuthInteractor): KeyguardFaceAuthInteractor
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index 0abce82..c4fc883 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -44,6 +44,8 @@
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.log.FaceAuthenticationLogger
 import com.android.systemui.log.SessionTracker
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.user.data.repository.UserRepository
 import java.io.PrintWriter
@@ -78,7 +80,7 @@
     val isAuthenticated: Flow<Boolean>
 
     /** Whether face auth can run at this point. */
-    val canRunFaceAuth: Flow<Boolean>
+    val canRunFaceAuth: StateFlow<Boolean>
 
     /** Provide the current status of face authentication. */
     val authenticationStatus: Flow<AuthenticationStatus>
@@ -87,10 +89,10 @@
     val detectionStatus: Flow<DetectionStatus>
 
     /** Current state of whether face authentication is locked out or not. */
-    val isLockedOut: Flow<Boolean>
+    val isLockedOut: StateFlow<Boolean>
 
     /** Current state of whether face authentication is running. */
-    val isAuthRunning: Flow<Boolean>
+    val isAuthRunning: StateFlow<Boolean>
 
     /** Whether bypass is currently enabled */
     val isBypassEnabled: Flow<Boolean>
@@ -129,6 +131,8 @@
     private val keyguardRepository: KeyguardRepository,
     private val keyguardInteractor: KeyguardInteractor,
     private val alternateBouncerInteractor: AlternateBouncerInteractor,
+    @FaceDetectTableLog private val faceDetectLog: TableLogBuffer,
+    @FaceAuthTableLog private val faceAuthLog: TableLogBuffer,
     dumpManager: DumpManager,
 ) : DeviceEntryFaceAuthRepository, Dumpable {
     private var authCancellationSignal: CancellationSignal? = null
@@ -224,17 +228,19 @@
         // Face detection can run only when lockscreen bypass is enabled
         // & detection is supported & biometric unlock is not allowed.
         listOf(
-                canFaceAuthOrDetectRun(),
-                logAndObserve(isBypassEnabled, "isBypassEnabled"),
+                canFaceAuthOrDetectRun(faceDetectLog),
+                logAndObserve(isBypassEnabled, "isBypassEnabled", faceDetectLog),
                 logAndObserve(
                     biometricSettingsRepository.isNonStrongBiometricAllowed.isFalse(),
-                    "nonStrongBiometricIsNotAllowed"
+                    "nonStrongBiometricIsNotAllowed",
+                    faceDetectLog
                 ),
                 // We don't want to run face detect if it's not possible to authenticate with FP
                 // from the bouncer. UDFPS is the only fp sensor type that won't support this.
                 logAndObserve(
                     and(isUdfps(), deviceEntryFingerprintAuthRepository.isRunning).isFalse(),
-                    "udfpsAuthIsNotPossibleAnymore"
+                    "udfpsAuthIsNotPossibleAnymore",
+                    faceDetectLog
                 )
             )
             .reduce(::and)
@@ -246,6 +252,7 @@
                     cancelDetection()
                 }
             }
+            .logDiffsForTable(faceDetectLog, "", "canFaceDetectRun", false)
             .launchIn(applicationScope)
     }
 
@@ -254,26 +261,34 @@
             it == BiometricType.UNDER_DISPLAY_FINGERPRINT
         }
 
-    private fun canFaceAuthOrDetectRun(): Flow<Boolean> {
+    private fun canFaceAuthOrDetectRun(tableLogBuffer: TableLogBuffer): Flow<Boolean> {
         return listOf(
-                logAndObserve(biometricSettingsRepository.isFaceEnrolled, "isFaceEnrolled"),
+                logAndObserve(
+                    biometricSettingsRepository.isFaceEnrolled,
+                    "isFaceEnrolled",
+                    tableLogBuffer
+                ),
                 logAndObserve(
                     biometricSettingsRepository.isFaceAuthenticationEnabled,
-                    "isFaceAuthenticationEnabled"
+                    "isFaceAuthenticationEnabled",
+                    tableLogBuffer
                 ),
                 logAndObserve(
                     userRepository.userSwitchingInProgress.isFalse(),
-                    "userSwitchingNotInProgress"
+                    "userSwitchingNotInProgress",
+                    tableLogBuffer
                 ),
                 logAndObserve(
                     keyguardRepository.isKeyguardGoingAway.isFalse(),
-                    "keyguardNotGoingAway"
+                    "keyguardNotGoingAway",
+                    tableLogBuffer
                 ),
                 logAndObserve(
                     keyguardRepository.wakefulness
                         .map { WakefulnessModel.isSleepingOrStartingToSleep(it) }
                         .isFalse(),
-                    "deviceNotSleepingOrNotStartingToSleep"
+                    "deviceNotSleepingOrNotStartingToSleep",
+                    tableLogBuffer
                 ),
                 logAndObserve(
                     combine(
@@ -282,15 +297,18 @@
                     ) { a, b ->
                         !a || b
                     },
-                    "secureCameraNotActiveOrAltBouncerIsShowing"
+                    "secureCameraNotActiveOrAltBouncerIsShowing",
+                    tableLogBuffer
                 ),
                 logAndObserve(
                     biometricSettingsRepository.isFaceAuthSupportedInCurrentPosture,
-                    "isFaceAuthSupportedInCurrentPosture"
+                    "isFaceAuthSupportedInCurrentPosture",
+                    tableLogBuffer
                 ),
                 logAndObserve(
                     biometricSettingsRepository.isCurrentUserInLockdown.isFalse(),
-                    "userHasNotLockedDownDevice"
+                    "userHasNotLockedDownDevice",
+                    tableLogBuffer
                 )
             )
             .reduce(::and)
@@ -299,20 +317,27 @@
     private fun observeFaceAuthGatingChecks() {
         // Face auth can run only if all of the gating conditions are true.
         listOf(
-                canFaceAuthOrDetectRun(),
-                logAndObserve(isLockedOut.isFalse(), "isNotLocked"),
+                canFaceAuthOrDetectRun(faceAuthLog),
+                logAndObserve(isLockedOut.isFalse(), "isNotInLockOutState", faceAuthLog),
                 logAndObserve(
                     deviceEntryFingerprintAuthRepository.isLockedOut.isFalse(),
-                    "fpLockedOut"
+                    "fpLockedOut",
+                    faceAuthLog
                 ),
-                logAndObserve(trustRepository.isCurrentUserTrusted.isFalse(), "currentUserTrusted"),
+                logAndObserve(
+                    trustRepository.isCurrentUserTrusted.isFalse(),
+                    "currentUserTrusted",
+                    faceAuthLog
+                ),
                 logAndObserve(
                     biometricSettingsRepository.isNonStrongBiometricAllowed,
-                    "nonStrongBiometricIsAllowed"
+                    "nonStrongBiometricIsAllowed",
+                    faceAuthLog
                 ),
                 logAndObserve(
                     userRepository.selectedUserInfo.map { it.isPrimary },
-                    "userIsPrimaryUser"
+                    "userIsPrimaryUser",
+                    faceAuthLog
                 ),
             )
             .reduce(::and)
@@ -326,6 +351,7 @@
                     cancel()
                 }
             }
+            .logDiffsForTable(faceAuthLog, "", "canFaceAuthRun", false)
             .launchIn(applicationScope)
     }
 
@@ -340,7 +366,6 @@
 
             override fun onAuthenticationAcquired(acquireInfo: Int) {
                 _authenticationStatus.value = AcquiredAuthenticationStatus(acquireInfo)
-                faceAuthLogger.authenticationAcquired(acquireInfo)
             }
 
             override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
@@ -401,7 +426,7 @@
 
     override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) {
         if (_isAuthRunning.value) {
-            faceAuthLogger.ignoredFaceAuthTrigger(uiEvent)
+            faceAuthLogger.ignoredFaceAuthTrigger(uiEvent, "face auth is currently running")
             return
         }
 
@@ -438,7 +463,16 @@
                 )
             }
         } else if (fallbackToDetection && canRunDetection.value) {
+            faceAuthLogger.ignoredFaceAuthTrigger(
+                uiEvent,
+                "face auth gating check is false, falling back to detection."
+            )
             detect()
+        } else {
+            faceAuthLogger.ignoredFaceAuthTrigger(
+                uiEvent,
+                "face auth & detect gating check is false"
+            )
         }
     }
 
@@ -467,7 +501,7 @@
     private val currentUserId: Int
         get() = userRepository.getSelectedUserInfo().id
 
-    fun cancelDetection() {
+    private fun cancelDetection() {
         detectCancellationSignal?.cancel()
         detectCancellationSignal = null
     }
@@ -491,10 +525,20 @@
         _isAuthRunning.value = false
     }
 
-    private fun logAndObserve(cond: Flow<Boolean>, loggingContext: String): Flow<Boolean> {
-        return cond.distinctUntilChanged().onEach {
-            faceAuthLogger.observedConditionChanged(it, loggingContext)
-        }
+    private fun logAndObserve(
+        cond: Flow<Boolean>,
+        conditionName: String,
+        logBuffer: TableLogBuffer
+    ): Flow<Boolean> {
+        return cond
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                logBuffer,
+                columnName = conditionName,
+                columnPrefix = "",
+                initialValue = false
+            )
+            .onEach { faceAuthLogger.observedConditionChanged(it, conditionName) }
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceAuthTableLog.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceAuthTableLog.kt
new file mode 100644
index 0000000..6c23032
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceAuthTableLog.kt
@@ -0,0 +1,25 @@
+/*
+ *   Copyright (C) 2023 The Android Open Source Project
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import javax.inject.Qualifier
+
+/** Face auth logs in table format. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class FaceAuthTableLog
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceDetectTableLog.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceDetectTableLog.kt
new file mode 100644
index 0000000..342064f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceDetectTableLog.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import javax.inject.Qualifier
+
+/** Face detect logs in table format. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class FaceDetectTableLog
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt
index 3c66f24..ef8b401 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt
@@ -17,8 +17,17 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
+import com.android.systemui.keyguard.domain.interactor.SystemUIKeyguardFaceAuthInteractor
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 
 @Module
 interface KeyguardFaceAuthModule {
@@ -27,5 +36,31 @@
         impl: DeviceEntryFaceAuthRepositoryImpl
     ): DeviceEntryFaceAuthRepository
 
+    @Binds
+    @IntoMap
+    @ClassKey(SystemUIKeyguardFaceAuthInteractor::class)
+    fun bind(impl: SystemUIKeyguardFaceAuthInteractor): CoreStartable
+
+    @Binds
+    fun keyguardFaceAuthInteractor(
+        impl: SystemUIKeyguardFaceAuthInteractor
+    ): KeyguardFaceAuthInteractor
+
     @Binds fun trustRepository(impl: TrustRepositoryImpl): TrustRepository
+
+    companion object {
+        @Provides
+        @SysUISingleton
+        @FaceAuthTableLog
+        fun provideFaceAuthTableLog(factory: TableLogBufferFactory): TableLogBuffer {
+            return factory.create("FaceAuthTableLog", 100)
+        }
+
+        @Provides
+        @SysUISingleton
+        @FaceDetectTableLog
+        fun provideFaceDetectTableLog(factory: TableLogBufferFactory): TableLogBuffer {
+            return factory.create("FaceDetectTableLog", 100)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
new file mode 100644
index 0000000..06ae11fe8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
@@ -0,0 +1,78 @@
+/*
+ *   Copyright (C) 2023 The Android Open Source Project
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.keyguard.shared.model.AuthenticationStatus
+import com.android.systemui.keyguard.shared.model.DetectionStatus
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Interactor that exposes API to get the face authentication status and handle any events that can
+ * cause face authentication to run.
+ */
+interface KeyguardFaceAuthInteractor {
+
+    /** Current authentication status */
+    val authenticationStatus: Flow<AuthenticationStatus>
+
+    /** Current detection status */
+    val detectionStatus: Flow<DetectionStatus>
+
+    /** Can face auth be run right now */
+    fun canFaceAuthRun(): Boolean
+
+    /** Whether face auth is currently running or not. */
+    fun isRunning(): Boolean
+
+    /** Whether face auth is in lock out state. */
+    fun isLockedOut(): Boolean
+
+    /**
+     * Register listener for use from code that cannot use [authenticationStatus] or
+     * [detectionStatus]
+     */
+    fun registerListener(listener: FaceAuthenticationListener)
+
+    /** Unregister previously registered listener */
+    fun unregisterListener(listener: FaceAuthenticationListener)
+
+    /** Whether the face auth interactor is enabled or not. */
+    fun isEnabled(): Boolean
+
+    fun onUdfpsSensorTouched()
+    fun onAssistantTriggeredOnLockScreen()
+    fun onDeviceLifted()
+    fun onQsExpansionStared()
+    fun onNotificationPanelClicked()
+    fun onSwipeUpOnBouncer()
+}
+
+/**
+ * Listener that can be registered with the [KeyguardFaceAuthInteractor] to receive updates about
+ * face authentication & detection updates.
+ *
+ * This is present to make it easier for use the new face auth API for code that cannot use
+ * [KeyguardFaceAuthInteractor.authenticationStatus] or [KeyguardFaceAuthInteractor.detectionStatus]
+ * flows.
+ */
+interface FaceAuthenticationListener {
+    /** Receive face authentication status updates */
+    fun onAuthenticationStatusChanged(status: AuthenticationStatus)
+
+    /** Receive status updates whenever face detection runs */
+    fun onDetectionStatusChanged(status: DetectionStatus)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index aabd212..da0ada1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -78,6 +78,14 @@
     val primaryBouncerToGoneTransition: Flow<TransitionStep> =
         repository.transition(PRIMARY_BOUNCER, GONE)
 
+    /** OFF->LOCKSCREEN transition information. */
+    val offToLockscreenTransition: Flow<TransitionStep> =
+        repository.transition(KeyguardState.OFF, LOCKSCREEN)
+
+    /** DOZING->LOCKSCREEN transition information. */
+    val dozingToLockscreenTransition: Flow<TransitionStep> =
+        repository.transition(KeyguardState.DOZING, LOCKSCREEN)
+
     /**
      * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <->
      * Lockscreen (0f).
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
new file mode 100644
index 0000000..cad40aa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
@@ -0,0 +1,62 @@
+/*
+ *   Copyright (C) 2023 The Android Open Source Project
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.AuthenticationStatus
+import com.android.systemui.keyguard.shared.model.DetectionStatus
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+
+/**
+ * Implementation of the interactor that noops all face auth operations.
+ *
+ * This is required for SystemUI variants that do not support face authentication but still inject
+ * other SysUI components that depend on [KeyguardFaceAuthInteractor]
+ */
+@SysUISingleton
+class NoopKeyguardFaceAuthInteractor @Inject constructor() : KeyguardFaceAuthInteractor {
+    override val authenticationStatus: Flow<AuthenticationStatus>
+        get() = emptyFlow()
+    override val detectionStatus: Flow<DetectionStatus>
+        get() = emptyFlow()
+
+    override fun canFaceAuthRun(): Boolean = false
+
+    override fun isRunning(): Boolean = false
+
+    override fun isLockedOut(): Boolean = false
+
+    override fun isEnabled() = false
+
+    override fun registerListener(listener: FaceAuthenticationListener) {}
+
+    override fun unregisterListener(listener: FaceAuthenticationListener) {}
+
+    override fun onUdfpsSensorTouched() {}
+
+    override fun onAssistantTriggeredOnLockScreen() {}
+
+    override fun onDeviceLifted() {}
+
+    override fun onQsExpansionStared() {}
+
+    override fun onNotificationPanelClicked() {}
+
+    override fun onSwipeUpOnBouncer() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
new file mode 100644
index 0000000..20ebb71
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
@@ -0,0 +1,200 @@
+/*
+ *   Copyright (C) 2023 The Android Open Source Project
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.keyguard.FaceAuthUiEvent
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.log.FaceAuthenticationLogger
+import com.android.systemui.util.kotlin.pairwise
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+/**
+ * Encapsulates business logic related face authentication being triggered for device entry from
+ * SystemUI Keyguard.
+ */
+@SysUISingleton
+class SystemUIKeyguardFaceAuthInteractor
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    private val repository: DeviceEntryFaceAuthRepository,
+    private val primaryBouncerInteractor: PrimaryBouncerInteractor,
+    private val alternateBouncerInteractor: AlternateBouncerInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    private val featureFlags: FeatureFlags,
+    private val faceAuthenticationLogger: FaceAuthenticationLogger,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+) : CoreStartable, KeyguardFaceAuthInteractor {
+
+    private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf()
+
+    override fun start() {
+        if (!isEnabled()) {
+            return
+        }
+        // This is required because fingerprint state required for the face auth repository is
+        // backed by KeyguardUpdateMonitor. KeyguardUpdateMonitor constructor accesses the biometric
+        // state which makes lazy injection not an option.
+        keyguardUpdateMonitor.setFaceAuthInteractor(this)
+        observeFaceAuthStateUpdates()
+        faceAuthenticationLogger.interactorStarted()
+        primaryBouncerInteractor.isShowing
+            .whenItFlipsToTrue()
+            .onEach {
+                faceAuthenticationLogger.bouncerVisibilityChanged()
+                runFaceAuth(
+                    FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN,
+                    fallbackToDetect = true
+                )
+            }
+            .launchIn(applicationScope)
+
+        alternateBouncerInteractor.isVisible
+            .whenItFlipsToTrue()
+            .onEach {
+                faceAuthenticationLogger.alternateBouncerVisibilityChanged()
+                runFaceAuth(
+                    FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN,
+                    fallbackToDetect = false
+                )
+            }
+            .launchIn(applicationScope)
+
+        merge(
+                keyguardTransitionInteractor.aodToLockscreenTransition,
+                keyguardTransitionInteractor.offToLockscreenTransition,
+                keyguardTransitionInteractor.dozingToLockscreenTransition
+            )
+            .filter { it.transitionState == TransitionState.STARTED }
+            .onEach {
+                faceAuthenticationLogger.lockscreenBecameVisible(it)
+                runFaceAuth(
+                    FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED,
+                    fallbackToDetect = true
+                )
+            }
+            .launchIn(applicationScope)
+    }
+
+    override fun onSwipeUpOnBouncer() {
+        runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false)
+    }
+
+    override fun onNotificationPanelClicked() {
+        runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED, true)
+    }
+
+    override fun onQsExpansionStared() {
+        runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, true)
+    }
+
+    override fun onDeviceLifted() {
+        runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED, true)
+    }
+
+    override fun onAssistantTriggeredOnLockScreen() {
+        runFaceAuth(FaceAuthUiEvent.FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED, true)
+    }
+
+    override fun onUdfpsSensorTouched() {
+        runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN, false)
+    }
+
+    override fun registerListener(listener: FaceAuthenticationListener) {
+        listeners.add(listener)
+    }
+
+    override fun unregisterListener(listener: FaceAuthenticationListener) {
+        listeners.remove(listener)
+    }
+
+    override fun isLockedOut(): Boolean = repository.isLockedOut.value
+
+    override fun isRunning(): Boolean = repository.isAuthRunning.value
+
+    override fun canFaceAuthRun(): Boolean = repository.canRunFaceAuth.value
+
+    override fun isEnabled(): Boolean {
+        return featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)
+    }
+
+    /** Provide the status of face authentication */
+    override val authenticationStatus = repository.authenticationStatus
+
+    /** Provide the status of face detection */
+    override val detectionStatus = repository.detectionStatus
+
+    private fun runFaceAuth(uiEvent: FaceAuthUiEvent, fallbackToDetect: Boolean) {
+        if (featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)) {
+            applicationScope.launch {
+                faceAuthenticationLogger.authRequested(uiEvent)
+                repository.authenticate(uiEvent, fallbackToDetection = fallbackToDetect)
+            }
+        } else {
+            faceAuthenticationLogger.ignoredFaceAuthTrigger(
+                uiEvent,
+                ignoredReason = "Skipping face auth request because feature flag is false"
+            )
+        }
+    }
+
+    private fun observeFaceAuthStateUpdates() {
+        authenticationStatus
+            .onEach { authStatusUpdate ->
+                listeners.forEach { it.onAuthenticationStatusChanged(authStatusUpdate) }
+            }
+            .flowOn(mainDispatcher)
+            .launchIn(applicationScope)
+        detectionStatus
+            .onEach { detectionStatusUpdate ->
+                listeners.forEach { it.onDetectionStatusChanged(detectionStatusUpdate) }
+            }
+            .flowOn(mainDispatcher)
+            .launchIn(applicationScope)
+    }
+
+    companion object {
+        const val TAG = "KeyguardFaceAuthInteractor"
+    }
+}
+
+// Extension method that filters a generic Boolean flow to one that emits
+// whenever there is flip from false -> true
+private fun Flow<Boolean>.whenItFlipsToTrue(): Flow<Boolean> {
+    return this.pairwise()
+        .filter { pair -> !pair.previousValue && pair.newValue }
+        .map { it.newValue }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
index f7355d5..7f6e4a9 100644
--- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
@@ -4,6 +4,7 @@
 import android.hardware.face.FaceSensorPropertiesInternal
 import com.android.keyguard.FaceAuthUiEvent
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.log.dagger.FaceAuthLog
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogLevel.DEBUG
@@ -27,15 +28,15 @@
 constructor(
     @FaceAuthLog private val logBuffer: LogBuffer,
 ) {
-    fun ignoredFaceAuthTrigger(uiEvent: FaceAuthUiEvent) {
+    fun ignoredFaceAuthTrigger(uiEvent: FaceAuthUiEvent, ignoredReason: String) {
         logBuffer.log(
             TAG,
             DEBUG,
-            { str1 = uiEvent.reason },
             {
-                "Ignoring trigger because face auth is currently running. " +
-                    "Trigger reason: $str1"
-            }
+                str1 = uiEvent.reason
+                str2 = ignoredReason
+            },
+            { "Ignoring trigger because $str2, Trigger reason: $str1" }
         )
     }
 
@@ -135,15 +136,6 @@
         logBuffer.log(TAG, DEBUG, "Face authentication failed")
     }
 
-    fun authenticationAcquired(acquireInfo: Int) {
-        logBuffer.log(
-            TAG,
-            DEBUG,
-            { int1 = acquireInfo },
-            { "Face acquired during face authentication: acquireInfo: $int1 " }
-        )
-    }
-
     fun authenticationError(
         errorCode: Int,
         errString: CharSequence?,
@@ -217,4 +209,34 @@
     fun cancellingFaceAuth() {
         logBuffer.log(TAG, DEBUG, "cancelling face auth because a gating condition became false")
     }
+
+    fun interactorStarted() {
+        logBuffer.log(TAG, DEBUG, "KeyguardFaceAuthInteractor started")
+    }
+
+    fun bouncerVisibilityChanged() {
+        logBuffer.log(TAG, DEBUG, "Triggering face auth because primary bouncer is visible")
+    }
+
+    fun alternateBouncerVisibilityChanged() {
+        logBuffer.log(TAG, DEBUG, "Triggering face auth because alternate bouncer is visible")
+    }
+
+    fun lockscreenBecameVisible(transitionStep: TransitionStep?) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            { str1 = "$transitionStep" },
+            { "Triggering face auth because lockscreen became visible due to transition: $str1" }
+        )
+    }
+
+    fun authRequested(uiEvent: FaceAuthUiEvent) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            { str1 = "$uiEvent" },
+            { "Requesting face auth for trigger: $str1" }
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index 7cb1cbe..ef14d1c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -64,6 +64,7 @@
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.plugins.FalsingManager;
@@ -132,6 +133,7 @@
     private final FalsingCollector mFalsingCollector;
     private final LockscreenGestureLogger mLockscreenGestureLogger;
     private final ShadeLogger mShadeLog;
+    private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor;
     private final FeatureFlags mFeatureFlags;
     private final InteractionJankMonitor mInteractionJankMonitor;
     private final FalsingManager mFalsingManager;
@@ -318,7 +320,8 @@
             MetricsLogger metricsLogger,
             FeatureFlags featureFlags,
             InteractionJankMonitor interactionJankMonitor,
-            ShadeLogger shadeLog
+            ShadeLogger shadeLog,
+            KeyguardFaceAuthInteractor keyguardFaceAuthInteractor
     ) {
         mPanelViewControllerLazy = panelViewControllerLazy;
         mPanelView = panelView;
@@ -357,6 +360,7 @@
         mLockscreenGestureLogger = lockscreenGestureLogger;
         mMetricsLogger = metricsLogger;
         mShadeLog = shadeLog;
+        mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor;
         mFeatureFlags = featureFlags;
         mInteractionJankMonitor = interactionJankMonitor;
 
@@ -937,6 +941,7 @@
         // When expanding QS, let's authenticate the user if possible,
         // this will speed up notification actions.
         if (height == 0 && !mKeyguardStateController.canDismissLockScreen()) {
+            mKeyguardFaceAuthInteractor.onQsExpansionStared();
             mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.QS_EXPANDED);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
index 8ee2c6f..74ab47f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.util.Assert
 import com.android.systemui.util.sensors.AsyncSensorManager
@@ -46,6 +47,7 @@
     private val statusBarStateController: StatusBarStateController,
     private val asyncSensorManager: AsyncSensorManager,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val keyguardFaceAuthInteractor: KeyguardFaceAuthInteractor,
     private val dumpManager: DumpManager
 ) : Dumpable, CoreStartable {
 
@@ -72,6 +74,7 @@
             // Not listening anymore since trigger events unregister themselves
             isListening = false
             updateListeningState()
+            keyguardFaceAuthInteractor.onDeviceLifted()
             keyguardUpdateMonitor.requestFaceAuth(
                 FaceAuthApiRequestReason.PICK_UP_GESTURE_TRIGGERED
             )
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index d760189..3e4fd89 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -67,6 +67,7 @@
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
@@ -208,7 +209,8 @@
                 mConfigurationController, mFalsingCollector, mFalsingManager,
                 mUserSwitcherController, mFeatureFlags, mGlobalSettings,
                 mSessionTracker, Optional.of(mSideFpsController), mFalsingA11yDelegate,
-                mTelephonyManager, mViewMediatorCallback, mAudioManager);
+                mTelephonyManager, mViewMediatorCallback, mAudioManager,
+                mock(KeyguardFaceAuthInteractor.class));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
index b41053c..ef750be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
@@ -51,32 +51,37 @@
     @Test
     fun testEnableDetector_expandWithTrack_shouldPostRunnable() {
         detector.enable(action)
-        // simulate notification expand
-        shadeExpansionStateManager.onPanelExpansionChanged(5566f, true, true, 5566f)
+        shadeExpansionStateManager.onPanelExpansionChanged(1.0f, true, true, 0f)
         verify(action).run()
     }
 
     @Test
     fun testEnableDetector_trackOnly_shouldPostRunnable() {
         detector.enable(action)
-        // simulate notification expand
-        shadeExpansionStateManager.onPanelExpansionChanged(5566f, false, true, 5566f)
+        shadeExpansionStateManager.onPanelExpansionChanged(1.0f, false, true, 0f)
         verify(action).run()
     }
 
     @Test
     fun testEnableDetector_expandOnly_shouldPostRunnable() {
         detector.enable(action)
-        // simulate notification expand
-        shadeExpansionStateManager.onPanelExpansionChanged(5566f, true, false, 5566f)
+        shadeExpansionStateManager.onPanelExpansionChanged(1.0f, true, false, 0f)
         verify(action).run()
     }
 
     @Test
+    fun testEnableDetector_expandWithoutFraction_shouldPostRunnable() {
+        detector.enable(action)
+        // simulate headsup notification
+        shadeExpansionStateManager.onPanelExpansionChanged(0.0f, true, false, 0f)
+        verifyZeroInteractions(action)
+    }
+
+    @Test
     fun testEnableDetector_shouldNotPostRunnable() {
         detector.enable(action)
         detector.disable()
-        shadeExpansionStateManager.onPanelExpansionChanged(5566f, true, true, 5566f)
+        shadeExpansionStateManager.onPanelExpansionChanged(1.0f, true, true, 0f)
         verifyZeroInteractions(action)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 64c028e..eae95a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -87,6 +87,7 @@
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.FalsingManager;
@@ -311,7 +312,8 @@
                 mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker,
                 mActivityLaunchAnimator, alternateTouchProvider, mBiometricExecutor,
                 mPrimaryBouncerInteractor, mSinglePointerTouchProcessor, mSessionTracker,
-                mAlternateBouncerInteractor, mSecureSettings, mInputManager, mUdfpsUtils);
+                mAlternateBouncerInteractor, mSecureSettings, mInputManager, mUdfpsUtils,
+                mock(KeyguardFaceAuthInteractor.class));
         verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
         mOverlayController = mOverlayCaptor.getValue();
         verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 2489e04..f21ea3d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -54,6 +54,7 @@
 import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.log.FaceAuthenticationLogger
 import com.android.systemui.log.SessionTracker
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.FakeKeyguardStateController
 import com.android.systemui.statusbar.phone.KeyguardBypassController
@@ -61,8 +62,11 @@
 import com.android.systemui.util.mockito.KotlinArgumentCaptor
 import com.android.systemui.util.mockito.captureMany
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.time.SystemClock
 import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.StandardTestDispatcher
@@ -88,8 +92,6 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.MockitoAnnotations
-import java.io.PrintWriter
-import java.io.StringWriter
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -183,8 +185,11 @@
     private fun createDeviceEntryFaceAuthRepositoryImpl(
         fmOverride: FaceManager? = faceManager,
         bypassControllerOverride: KeyguardBypassController? = bypassController
-    ) =
-        DeviceEntryFaceAuthRepositoryImpl(
+    ): DeviceEntryFaceAuthRepositoryImpl {
+        val systemClock = FakeSystemClock()
+        val faceAuthBuffer = TableLogBuffer(10, "face auth", systemClock)
+        val faceDetectBuffer = TableLogBuffer(10, "face detect", systemClock)
+        return DeviceEntryFaceAuthRepositoryImpl(
             mContext,
             fmOverride,
             fakeUserRepository,
@@ -200,8 +205,11 @@
             keyguardRepository,
             keyguardInteractor,
             alternateBouncerInteractor,
+            faceDetectBuffer,
+            faceAuthBuffer,
             dumpManager,
         )
+    }
 
     @Test
     fun faceAuthRunsAndProvidesAuthStatusUpdates() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
new file mode 100644
index 0000000..3d1d2f4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
@@ -0,0 +1,292 @@
+/*
+ *  Copyright (C) 2023 The Android Open Source Project
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.os.Handler
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.FaceAuthUiEvent
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dump.logcatLogBuffer
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.log.FaceAuthenticationLogger
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
+
+    private lateinit var underTest: SystemUIKeyguardFaceAuthInteractor
+    private lateinit var testScope: TestScope
+    private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
+    private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
+    private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
+    private lateinit var faceAuthRepository: FakeDeviceEntryFaceAuthRepository
+
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        val scheduler = TestCoroutineScheduler()
+        val dispatcher = StandardTestDispatcher(scheduler)
+        testScope = TestScope(dispatcher)
+        val featureFlags = FakeFeatureFlags()
+        featureFlags.set(Flags.FACE_AUTH_REFACTOR, true)
+        bouncerRepository = FakeKeyguardBouncerRepository()
+        faceAuthRepository = FakeDeviceEntryFaceAuthRepository()
+        keyguardTransitionRepository = FakeKeyguardTransitionRepository()
+        keyguardTransitionInteractor = KeyguardTransitionInteractor(keyguardTransitionRepository)
+
+        underTest =
+            SystemUIKeyguardFaceAuthInteractor(
+                testScope.backgroundScope,
+                dispatcher,
+                faceAuthRepository,
+                PrimaryBouncerInteractor(
+                    bouncerRepository,
+                    mock(BouncerView::class.java),
+                    mock(Handler::class.java),
+                    mock(KeyguardStateController::class.java),
+                    mock(KeyguardSecurityModel::class.java),
+                    mock(PrimaryBouncerCallbackInteractor::class.java),
+                    mock(FalsingCollector::class.java),
+                    mock(DismissCallbackRegistry::class.java),
+                    context,
+                    keyguardUpdateMonitor,
+                    mock(KeyguardBypassController::class.java),
+                ),
+                AlternateBouncerInteractor(
+                    mock(StatusBarStateController::class.java),
+                    mock(KeyguardStateController::class.java),
+                    bouncerRepository,
+                    mock(BiometricSettingsRepository::class.java),
+                    FakeDeviceEntryFingerprintAuthRepository(),
+                    FakeSystemClock(),
+                ),
+                keyguardTransitionInteractor,
+                featureFlags,
+                FaceAuthenticationLogger(logcatLogBuffer("faceAuthBuffer")),
+                keyguardUpdateMonitor,
+            )
+    }
+
+    @Test
+    fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromOffState() =
+        testScope.runTest {
+            underTest.start()
+
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    KeyguardState.OFF,
+                    KeyguardState.LOCKSCREEN,
+                    transitionState = TransitionState.STARTED
+                )
+            )
+
+            runCurrent()
+            assertThat(faceAuthRepository.runningAuthRequest.value)
+                .isEqualTo(
+                    Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED, true)
+                )
+        }
+
+    @Test
+    fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromAodState() =
+        testScope.runTest {
+            underTest.start()
+
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    KeyguardState.AOD,
+                    KeyguardState.LOCKSCREEN,
+                    transitionState = TransitionState.STARTED
+                )
+            )
+
+            runCurrent()
+            assertThat(faceAuthRepository.runningAuthRequest.value)
+                .isEqualTo(
+                    Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED, true)
+                )
+        }
+
+    @Test
+    fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromDozingState() =
+        testScope.runTest {
+            underTest.start()
+
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    KeyguardState.DOZING,
+                    KeyguardState.LOCKSCREEN,
+                    transitionState = TransitionState.STARTED
+                )
+            )
+
+            runCurrent()
+            assertThat(faceAuthRepository.runningAuthRequest.value)
+                .isEqualTo(
+                    Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED, true)
+                )
+        }
+
+    @Test
+    fun faceAuthIsRequestedWhenPrimaryBouncerIsVisible() =
+        testScope.runTest {
+            underTest.start()
+
+            bouncerRepository.setPrimaryShow(false)
+            runCurrent()
+
+            bouncerRepository.setPrimaryShow(true)
+
+            runCurrent()
+            assertThat(faceAuthRepository.runningAuthRequest.value)
+                .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN, true))
+        }
+
+    @Test
+    fun faceAuthIsRequestedWhenAlternateBouncerIsVisible() =
+        testScope.runTest {
+            underTest.start()
+
+            bouncerRepository.setAlternateVisible(false)
+            runCurrent()
+
+            bouncerRepository.setAlternateVisible(true)
+
+            runCurrent()
+            assertThat(faceAuthRepository.runningAuthRequest.value)
+                .isEqualTo(
+                    Pair(
+                        FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN,
+                        false
+                    )
+                )
+        }
+
+    @Test
+    fun faceAuthIsRequestedWhenUdfpsSensorTouched() =
+        testScope.runTest {
+            underTest.start()
+
+            underTest.onUdfpsSensorTouched()
+
+            runCurrent()
+            assertThat(faceAuthRepository.runningAuthRequest.value)
+                .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN, false))
+        }
+
+    @Test
+    fun faceAuthIsRequestedWhenOnAssistantTriggeredOnLockScreen() =
+        testScope.runTest {
+            underTest.start()
+
+            underTest.onAssistantTriggeredOnLockScreen()
+
+            runCurrent()
+            assertThat(faceAuthRepository.runningAuthRequest.value)
+                .isEqualTo(
+                    Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED, true)
+                )
+        }
+
+    @Test
+    fun faceAuthIsRequestedWhenDeviceLifted() =
+        testScope.runTest {
+            underTest.start()
+
+            underTest.onDeviceLifted()
+
+            runCurrent()
+            assertThat(faceAuthRepository.runningAuthRequest.value)
+                .isEqualTo(
+                    Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED, true)
+                )
+        }
+
+    @Test
+    fun faceAuthIsRequestedWhenQsExpansionStared() =
+        testScope.runTest {
+            underTest.start()
+
+            underTest.onQsExpansionStared()
+
+            runCurrent()
+            assertThat(faceAuthRepository.runningAuthRequest.value)
+                .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, true))
+        }
+
+    @Test
+    fun faceAuthIsRequestedWhenNotificationPanelClicked() =
+        testScope.runTest {
+            underTest.start()
+
+            underTest.onNotificationPanelClicked()
+
+            runCurrent()
+            assertThat(faceAuthRepository.runningAuthRequest.value)
+                .isEqualTo(
+                    Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED, true)
+                )
+        }
+
+    @Test
+    fun faceAuthIsRequestedWhenSwipeUpOnBouncer() =
+        testScope.runTest {
+            underTest.start()
+
+            underTest.onSwipeUpOnBouncer()
+
+            runCurrent()
+            assertThat(faceAuthRepository.runningAuthRequest.value)
+                .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false))
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 5ca3771..4bfd6a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -93,6 +93,7 @@
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
 import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
@@ -665,7 +666,8 @@
                 mMetricsLogger,
                 mFeatureFlags,
                 mInteractionJankMonitor,
-                mShadeLog
+                mShadeLog,
+                mock(KeyguardFaceAuthInteractor.class)
         );
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
index d8ffe39..908f7cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
@@ -62,6 +62,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.plugins.FalsingManager;
@@ -239,7 +240,8 @@
                 mMetricsLogger,
                 mFeatureFlags,
                 mInteractionJankMonitor,
-                mShadeLogger
+                mShadeLogger,
+                mock(KeyguardFaceAuthInteractor.class)
         );
 
         mFragmentListener = mQsController.getQsFragmentListener();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
index c08ecd0..738f09d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
@@ -24,7 +24,6 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.map
 
 class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository {
 
@@ -46,14 +45,19 @@
     private val _runningAuthRequest = MutableStateFlow<Pair<FaceAuthUiEvent, Boolean>?>(null)
     val runningAuthRequest: StateFlow<Pair<FaceAuthUiEvent, Boolean>?> =
         _runningAuthRequest.asStateFlow()
-    override val isAuthRunning = _runningAuthRequest.map { it != null }
+
+    private val _isAuthRunning = MutableStateFlow(false)
+    override val isAuthRunning: StateFlow<Boolean> = _isAuthRunning
+
     override val isBypassEnabled = MutableStateFlow(false)
 
     override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) {
         _runningAuthRequest.value = uiEvent to fallbackToDetection
+        _isAuthRunning.value = true
     }
 
     override fun cancel() {
+        _isAuthRunning.value = false
         _runningAuthRequest.value = null
     }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 51325e7..0bdb0c8 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -112,6 +112,7 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.view.Display;
 import android.view.IWindow;
 import android.view.InputDevice;
@@ -160,6 +161,7 @@
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.policy.WindowManagerPolicy;
+import com.android.server.utils.Slogf;
 import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
 import com.android.settingslib.RestrictedLockUtils;
@@ -301,7 +303,23 @@
     private final List<SendWindowStateChangedEventRunnable> mSendWindowStateChangedEventRunnables =
             new ArrayList<>();
 
-    private int mCurrentUserId = UserHandle.USER_SYSTEM;
+    @GuardedBy("mLock")
+    private @UserIdInt int mCurrentUserId = UserHandle.USER_SYSTEM;
+
+    // TODO(b/255426725): temporary workaround to support visible background users for UiAutomation:
+    // when the UiAutomation is set in a visible background user, mCurrentUserId points to that user
+    // and mRealCurrentUserId points to the "real" current user; otherwise, mRealCurrentUserId
+    // is set as UserHandle.USER_CURRENT.
+    @GuardedBy("mLock")
+    private @UserIdInt int mRealCurrentUserId = UserHandle.USER_CURRENT;
+
+    // TODO(b/255426725): temporary workaround to support visible background users for UiAutomation
+    // purposes - in the long term, the whole service should be refactored so it handles "visible"
+    // users, not current user. Notice that because this is temporary, it's not trying to optimize
+    // performance / utilization (for example, it's not using an IntArray)
+    @GuardedBy("mLock")
+    @Nullable // only set when device supports visible background users
+    private final SparseBooleanArray mVisibleBgUserIds;
 
     //TODO: Remove this hack
     private boolean mInitialized;
@@ -316,6 +334,7 @@
     private SparseArray<SurfaceControl> mA11yOverlayLayers = new SparseArray<>();
 
     private final FlashNotificationsController mFlashNotificationsController;
+    private final UserManagerInternal mUmi;
 
     private AccessibilityUserState getCurrentUserStateLocked() {
         return getUserStateLocked(mCurrentUserId);
@@ -445,6 +464,10 @@
             mHasInputFilter = true;
         }
         mFlashNotificationsController = new FlashNotificationsController(mContext);
+        mUmi = LocalServices.getService(UserManagerInternal.class);
+        // TODO(b/255426725): not used on tests
+        mVisibleBgUserIds = null;
+
         init();
     }
 
@@ -477,6 +500,15 @@
         mProxyManager = new ProxyManager(mLock, mA11yWindowManager, mContext, mMainHandler,
                 mUiAutomationManager, this);
         mFlashNotificationsController = new FlashNotificationsController(mContext);
+        mUmi = LocalServices.getService(UserManagerInternal.class);
+
+        if (UserManager.isVisibleBackgroundUsersEnabled()) {
+            mVisibleBgUserIds = new SparseBooleanArray();
+            mUmi.addUserVisibilityListener((u, v) -> onUserVisibilityChanged(u, v));
+        } else {
+            mVisibleBgUserIds = null;
+        }
+
         init();
     }
 
@@ -493,6 +525,12 @@
         return mCurrentUserId;
     }
 
+    @GuardedBy("mLock")
+    @Override
+    public SparseBooleanArray getVisibleUserIdsLocked() {
+        return mVisibleBgUserIds;
+    }
+
     @Override
     public boolean isAccessibilityButtonShown() {
         return mIsAccessibilityButtonShown;
@@ -1362,6 +1400,7 @@
     public void registerUiTestAutomationService(IBinder owner,
             IAccessibilityServiceClient serviceClient,
             AccessibilityServiceInfo accessibilityServiceInfo,
+            int userId,
             int flags) {
         if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
             mTraceManager.logTrace(LOG_TAG + ".registerUiTestAutomationService",
@@ -1374,6 +1413,7 @@
                 FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE);
 
         synchronized (mLock) {
+            changeCurrentUserForTestAutomationIfNeededLocked(userId);
             mUiAutomationManager.registerUiTestAutomationServiceLocked(owner, serviceClient,
                     mContext, accessibilityServiceInfo, sIdCounter++, mMainHandler,
                     mSecurityPolicy, this, getTraceManager(), mWindowManagerService,
@@ -1390,9 +1430,49 @@
         }
         synchronized (mLock) {
             mUiAutomationManager.unregisterUiTestAutomationServiceLocked(serviceClient);
+            restoreCurrentUserAfterTestAutomationIfNeededLocked();
         }
     }
 
+    // TODO(b/255426725): temporary workaround to support visible background users for UiAutomation
+    @GuardedBy("mLock")
+    private void changeCurrentUserForTestAutomationIfNeededLocked(@UserIdInt int userId) {
+        if (mVisibleBgUserIds == null) {
+            Slogf.d(LOG_TAG, "changeCurrentUserForTestAutomationIfNeededLocked(%d): ignoring "
+                    + "because device doesn't support visible background users", userId);
+            return;
+        }
+        if (!mVisibleBgUserIds.get(userId)) {
+            Slogf.wtf(LOG_TAG, "Cannot change current user to %d as it's not visible "
+                    + "(mVisibleUsers=%s)", userId, mVisibleBgUserIds);
+            return;
+        }
+        if (mCurrentUserId == userId) {
+            Slogf.w(LOG_TAG, "NOT changing current user for test automation purposes as it is "
+                    + "already %d", mCurrentUserId);
+            return;
+        }
+        Slogf.i(LOG_TAG, "Changing current user from %d to %d for test automation purposes",
+                mCurrentUserId, userId);
+        mRealCurrentUserId = mCurrentUserId;
+        switchUser(userId);
+    }
+
+    // TODO(b/255426725): temporary workaround to support visible background users for UiAutomation
+    @GuardedBy("mLock")
+    private void restoreCurrentUserAfterTestAutomationIfNeededLocked() {
+        if (mVisibleBgUserIds == null) {
+            Slogf.d(LOG_TAG, "restoreCurrentUserForTestAutomationIfNeededLocked(): ignoring "
+                    + "because device doesn't support visible background users");
+            return;
+        }
+        Slogf.i(LOG_TAG, "Restoring current user to %d after using %d for test automation purposes",
+                mRealCurrentUserId, mCurrentUserId);
+        int currentUserId = mRealCurrentUserId;
+        mRealCurrentUserId = UserHandle.USER_CURRENT;
+        switchUser(currentUserId);
+    }
+
     @Override
     public IBinder getWindowToken(int windowId, int userId) {
         if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
@@ -2291,8 +2371,7 @@
     private void updateServicesLocked(AccessibilityUserState userState) {
         Map<ComponentName, AccessibilityServiceConnection> componentNameToServiceMap =
                 userState.mComponentNameToServiceMap;
-        boolean isUnlockingOrUnlocked = LocalServices.getService(UserManagerInternal.class)
-                    .isUserUnlockingOrUnlocked(userState.mUserId);
+        boolean isUnlockingOrUnlocked = mUmi.isUserUnlockingOrUnlocked(userState.mUserId);
 
         for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) {
             AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i);
@@ -2593,6 +2672,19 @@
         }
     }
 
+    private void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) {
+        if (DEBUG) {
+            Slogf.d(LOG_TAG, "onUserVisibilityChanged(): %d => %b", userId, visible);
+        }
+        synchronized (mLock) {
+            if (visible) {
+                mVisibleBgUserIds.put(userId, visible);
+            } else {
+                mVisibleBgUserIds.delete(userId);
+            }
+        }
+    }
+
     /**
      * Called when any property of the user state has changed.
      *
@@ -4025,7 +4117,16 @@
             pw.println("ACCESSIBILITY MANAGER (dumpsys accessibility)");
             pw.println();
             pw.append("currentUserId=").append(String.valueOf(mCurrentUserId));
+            if (mRealCurrentUserId != UserHandle.USER_CURRENT
+                    && mCurrentUserId != mRealCurrentUserId) {
+                pw.append(" (set for UiAutomation purposes; \"real\" current user is ")
+                        .append(String.valueOf(mRealCurrentUserId)).append(")");
+            }
             pw.println();
+            if (mVisibleBgUserIds != null) {
+                pw.append("visibleBgUserIds=").append(mVisibleBgUserIds.toString());
+                pw.println();
+            }
             pw.append("hasWindowMagnificationConnection=").append(
                     String.valueOf(getWindowMagnificationMgr().isConnected()));
             pw.println();
@@ -4052,6 +4153,7 @@
             }
             pw.println();
             mProxyManager.dump(fd, pw, args);
+            mA11yDisplayListener.dump(fd, pw, args);
         }
     }
 
@@ -4437,6 +4539,20 @@
             /* do nothing */
         }
 
+        void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            pw.println("Accessibility Display Listener:");
+            pw.println("    SystemUI uid: " + mSystemUiUid);
+            int size = mDisplaysList.size();
+            pw.printf("    %d valid display%s: ", size, (size == 1 ? "" : "s"));
+            for (int i = 0; i < size; i++) {
+                pw.print(mDisplaysList.get(i).getDisplayId());
+                if (i < size - 1) {
+                    pw.print(", ");
+                }
+            }
+            pw.println();
+        }
+
         private boolean isValidDisplay(@Nullable Display display) {
             if (display == null || display.getType() == Display.TYPE_OVERLAY) {
                 return false;
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java
index c37ea50..8865623 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java
@@ -39,6 +39,7 @@
 import android.os.UserManager;
 import android.util.ArraySet;
 import android.util.Slog;
+import android.util.SparseBooleanArray;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.inputmethod.InputMethodInfo;
 
@@ -88,6 +89,12 @@
          */
         int getCurrentUserIdLocked();
         // TODO: Should include resolveProfileParentLocked, but that was already in SecurityPolicy
+
+        // TODO(b/255426725): temporary hack; see comment on A11YMS.mVisibleBgUserIds
+        /**
+         * Returns the {@link android.os.UserManager#getVisibleUsers() visible users}.
+         */
+        @Nullable SparseBooleanArray getVisibleUserIdsLocked();
     }
 
     private final Context mContext;
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
index baed181..a8a5365 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
@@ -51,6 +51,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.accessibility.AccessibilitySecurityPolicy.AccessibilityUserManager;
+import com.android.server.utils.Slogf;
 import com.android.server.wm.WindowManagerInternal;
 
 import java.io.FileDescriptor;
@@ -59,6 +60,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * This class provides APIs for accessibility manager to manage {@link AccessibilityWindowInfo}s and
@@ -67,6 +69,7 @@
 public class AccessibilityWindowManager {
     private static final String LOG_TAG = "AccessibilityWindowManager";
     private static final boolean DEBUG = false;
+    private static final boolean VERBOSE = false;
 
     private static int sNextWindowId;
 
@@ -209,6 +212,9 @@
          * Constructor for DisplayWindowsObserver.
          */
         DisplayWindowsObserver(int displayId) {
+            if (DEBUG) {
+                Slogf.d(LOG_TAG, "Creating DisplayWindowsObserver for displayId %d", displayId);
+            }
             mDisplayId = displayId;
         }
 
@@ -430,12 +436,27 @@
             synchronized (mLock) {
                 updateWindowsByWindowAttributesLocked(windows);
                 if (DEBUG) {
-                    Slog.i(LOG_TAG, "Display Id = " + mDisplayId);
-                    Slog.i(LOG_TAG, "Windows changed: " + windows);
+                    Slogf.i(LOG_TAG, "mDisplayId=%d, topFocusedDisplayId=%d, currentUserId=%d, "
+                            + "visibleBgUsers=%s", mDisplayId, topFocusedDisplayId,
+                            mAccessibilityUserManager.getCurrentUserIdLocked(),
+                            mAccessibilityUserManager.getVisibleUserIdsLocked());
+                    if (VERBOSE) {
+                        Slogf.i(LOG_TAG, "%d windows changed: %s ", windows.size(), windows);
+                    } else {
+                        List<String> windowsInfo = windows.stream()
+                                .map(w -> "{displayId=" + w.displayId + ", title=" + w.title + "}")
+                                .collect(Collectors.toList());
+                        Slogf.i(LOG_TAG, "%d windows changed: %s", windows.size(), windowsInfo);
+                    }
                 }
                 if (shouldUpdateWindowsLocked(forceSend, windows)) {
                     mTopFocusedDisplayId = topFocusedDisplayId;
                     mTopFocusedWindowToken = topFocusedWindowToken;
+                    if (DEBUG) {
+                        Slogf.d(LOG_TAG, "onWindowsForAccessibilityChanged(): updating windows for "
+                                + "display %d and token %s",
+                                topFocusedDisplayId, topFocusedWindowToken);
+                    }
                     cacheWindows(windows);
                     // Lets the policy update the focused and active windows.
                     updateWindowsLocked(mAccessibilityUserManager.getCurrentUserIdLocked(),
@@ -443,6 +464,11 @@
                     // Someone may be waiting for the windows - advertise it.
                     mLock.notifyAll();
                 }
+                else if (DEBUG) {
+                    Slogf.d(LOG_TAG, "onWindowsForAccessibilityChanged(): NOT updating windows for "
+                            + "display %d and token %s",
+                            topFocusedDisplayId, topFocusedWindowToken);
+                }
             }
         }
 
@@ -472,6 +498,12 @@
             }
 
             final int windowCount = windows.size();
+            if (VERBOSE) {
+                Slogf.v(LOG_TAG,
+                        "shouldUpdateWindowsLocked(): mDisplayId=%d, windowCount=%d, "
+                        + "mCachedWindowInfos.size()=%d, windows.size()=%d", mDisplayId,
+                        windowCount, mCachedWindowInfos.size(), windows.size());
+            }
             // We computed the windows and if they changed notify the client.
             if (mCachedWindowInfos.size() != windowCount) {
                 // Different size means something changed.
@@ -1274,7 +1306,7 @@
      */
     @Nullable
     public RemoteAccessibilityConnection getConnectionLocked(int userId, int windowId) {
-        if (DEBUG) {
+        if (VERBOSE) {
             Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId);
         }
         RemoteAccessibilityConnection connection = mGlobalInteractionConnections.get(windowId);
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 7453743..11d84ff 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -532,14 +532,7 @@
             if (newFocus != null && newFocus != mNavigatingWindow
                     && (newFocus.mActivityRecord == null
                     || (newFocus.mActivityRecord == mNavigatingWindow.mActivityRecord))) {
-                EventLogTags.writeWmBackNaviCanceled("focusWindowChanged");
-                if (isMonitorForRemote()) {
-                    mObserver.sendResult(null /* result */);
-                }
-                if (isMonitorAnimationOrTransition()) {
-                    // transition won't happen, cancel internal status
-                    clearBackAnimations();
-                }
+                cancelBackNavigating("focusWindowChanged");
             }
         }
 
@@ -553,19 +546,12 @@
             }
             final ArrayList<WindowContainer> all = new ArrayList<>(opening);
             all.addAll(closing);
-            for (WindowContainer app : all) {
-                if (app.hasChild(mNavigatingWindow)) {
-                    EventLogTags.writeWmBackNaviCanceled("transitionHappens");
-                    if (isMonitorForRemote()) {
-                        mObserver.sendResult(null /* result */);
-                    }
-                    if (isMonitorAnimationOrTransition()) {
-                        clearBackAnimations();
-                    }
+            for (int i = all.size() - 1; i >= 0; --i) {
+                if (all.get(i).hasChild(mNavigatingWindow)) {
+                    cancelBackNavigating("transitionHappens");
                     break;
                 }
             }
-
         }
 
         private boolean atSameDisplay(WindowState newFocus) {
@@ -575,6 +561,17 @@
             final int navigatingDisplayId = mNavigatingWindow.getDisplayId();
             return newFocus == null || newFocus.getDisplayId() == navigatingDisplayId;
         }
+
+        private void cancelBackNavigating(String reason) {
+            EventLogTags.writeWmBackNaviCanceled(reason);
+            if (isMonitorForRemote()) {
+                mObserver.sendResult(null /* result */);
+            }
+            if (isMonitorAnimationOrTransition()) {
+                clearBackAnimations();
+            }
+            cancelPendingAnimation();
+        }
     }
 
     // For shell transition
@@ -677,12 +674,7 @@
             Slog.w(TAG, "Finished transition didn't include the targets"
                     + " open: " + mPendingAnimationBuilder.mOpenTarget
                     + " close: " + mPendingAnimationBuilder.mCloseTarget);
-            try {
-                mPendingAnimationBuilder.mBackAnimationAdapter.getRunner().onAnimationCancelled();
-            } catch (RemoteException e) {
-                throw new RuntimeException(e);
-            }
-            mPendingAnimationBuilder = null;
+            cancelPendingAnimation();
             return false;
         }
 
@@ -697,6 +689,18 @@
         return true;
     }
 
+    private void cancelPendingAnimation() {
+        if (mPendingAnimationBuilder == null) {
+            return;
+        }
+        try {
+            mPendingAnimationBuilder.mBackAnimationAdapter.getRunner().onAnimationCancelled();
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Remote animation gone", e);
+        }
+        mPendingAnimationBuilder = null;
+    }
+
     /**
      * Create and handling animations status for an open/close animation targets.
      */