Merge "Deny FSI permission for sideloads" into udc-dev
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 37ceb09..526e63c 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -1384,8 +1384,8 @@
          * want to call one of these methods.
          *
          * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
-         * an app must hold the {@link android.Manifest.permission#INTERNET} and
-         * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to
+         * an app must hold the
+         * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permission to
          * schedule a job that requires a network.
          *
          * <p class="note">
@@ -1445,8 +1445,8 @@
          * constraint.
          *
          * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
-         * an app must hold the {@link android.Manifest.permission#INTERNET} and
-         * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to
+         * an app must hold the
+         * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permission to
          * schedule a job that requires a network.
          *
          * @param networkRequest The detailed description of the kind of network
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index a0634f0..dc608e7 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -810,7 +810,7 @@
                 mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable,
                 mRecycledAssignmentInfo, mRecycledPrivilegedState);
 
-        noteConcurrency();
+        noteConcurrency(true);
     }
 
     @VisibleForTesting
@@ -1437,11 +1437,13 @@
         }
     }
 
-    private void noteConcurrency() {
+    private void noteConcurrency(boolean logForHistogram) {
         mService.mJobPackageTracker.noteConcurrency(mRunningJobs.size(),
                 // TODO: log per type instead of only TOP
                 mWorkCountTracker.getRunningJobCount(WORK_TYPE_TOP));
-        sConcurrencyHistogramLogger.logSample(mActiveServices.size());
+        if (logForHistogram) {
+            sConcurrencyHistogramLogger.logSample(mActiveServices.size());
+        }
     }
 
     @GuardedBy("mLock")
@@ -1582,7 +1584,9 @@
         final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
         if (pendingJobQueue.size() == 0) {
             worker.clearPreferredUid();
-            noteConcurrency();
+            // Don't log the drop in concurrency to the histogram, otherwise, we'll end up
+            // overcounting lower concurrency values as jobs end execution.
+            noteConcurrency(false);
             return;
         }
         if (mActiveServices.size() >= mSteadyStateConcurrencyLimit) {
@@ -1612,7 +1616,9 @@
                 // scheduled), but we should
                 // be able to stop the other jobs soon so don't start running anything new until we
                 // get back below the limit.
-                noteConcurrency();
+                // Don't log the drop in concurrency to the histogram, otherwise, we'll end up
+                // overcounting lower concurrency values as jobs end execution.
+                noteConcurrency(false);
                 return;
             }
         }
@@ -1761,7 +1767,9 @@
             }
         }
 
-        noteConcurrency();
+        // Don't log the drop in concurrency to the histogram, otherwise, we'll end up
+        // overcounting lower concurrency values as jobs end execution.
+        noteConcurrency(false);
     }
 
     /**
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index e4e3de2..3f552b6 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -195,7 +195,7 @@
     private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L;
 
     /**
-     * Require the app to have the INTERNET and ACCESS_NETWORK_STATE permissions when scheduling
+     * Require the app to have the ACCESS_NETWORK_STATE permissions when scheduling
      * a job with a connectivity constraint.
      */
     @ChangeId
@@ -1503,6 +1503,16 @@
                     }
 
                     toCancel.enqueueWorkLocked(work);
+                    if (toCancel.getJob().isUserInitiated()) {
+                        // The app is in a state to successfully schedule a UI job. Presumably, the
+                        // user has asked for this additional bit of work, so remove any demotion
+                        // flags. Only do this for UI jobs since they have strict scheduling
+                        // requirements; it's harder to assume other jobs were scheduled due to
+                        // user interaction/request.
+                        toCancel.removeInternalFlags(
+                                JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER
+                                        | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ);
+                    }
                     mJobs.touchJob(toCancel);
                     sEnqueuedJwiHighWaterMarkLogger.logSampleWithUid(uId, toCancel.getWorkCount());
 
@@ -4001,12 +4011,6 @@
             if (job.getRequiredNetwork() != null
                     && CompatChanges.isChangeEnabled(
                             REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) {
-                // All networking, including with the local network and even local to the device,
-                // requires the INTERNET permission.
-                if (!hasPermission(uid, pid, Manifest.permission.INTERNET)) {
-                    throw new SecurityException(Manifest.permission.INTERNET
-                            + " required for jobs with a connectivity constraint");
-                }
                 if (!hasPermission(uid, pid, Manifest.permission.ACCESS_NETWORK_STATE)) {
                     throw new SecurityException(Manifest.permission.ACCESS_NETWORK_STATE
                             + " required for jobs with a connectivity constraint");
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index 2944095e..bf2e456 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -551,9 +551,6 @@
         if (CompatChanges.isChangeEnabled(
                 JobSchedulerService.REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) {
             final String pkgName = job.getServiceComponent().getPackageName();
-            if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.INTERNET)) {
-                return false;
-            }
             if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.ACCESS_NETWORK_STATE)) {
                 return false;
             }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 6445c3b..b5d763c 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -1177,6 +1177,10 @@
         mInternalFlags |= flags;
     }
 
+    public void removeInternalFlags(int flags) {
+        mInternalFlags = mInternalFlags & ~flags;
+    }
+
     int getPreferredConstraintFlags() {
         return mPreferredConstraints;
     }
diff --git a/core/java/android/app/admin/DeviceStateCache.java b/core/java/android/app/admin/DeviceStateCache.java
index d1d130d..f37f541 100644
--- a/core/java/android/app/admin/DeviceStateCache.java
+++ b/core/java/android/app/admin/DeviceStateCache.java
@@ -50,6 +50,14 @@
     public abstract boolean isUserOrganizationManaged(@UserIdInt int userHandle);
 
     /**
+     * Returns whether a user has affiliated IDs.
+     */
+
+    public boolean hasAffiliationWithDevice(int userId) {
+        return false;
+    }
+
+    /**
      * Empty implementation.
      */
     private static class EmptyDeviceStateCache extends DeviceStateCache {
diff --git a/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java b/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java
index 257ad71..5b24fb6 100644
--- a/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java
@@ -218,7 +218,8 @@
             FINGERPRINT_ACQUIRED_UNKNOWN,
             FINGERPRINT_ACQUIRED_IMMOBILE,
             FINGERPRINT_ACQUIRED_TOO_BRIGHT,
-            FINGERPRINT_ACQUIRED_POWER_PRESSED})
+            FINGERPRINT_ACQUIRED_POWER_PRESSED,
+            FINGERPRINT_ACQUIRED_RE_ENROLL})
     @Retention(RetentionPolicy.SOURCE)
     @interface FingerprintAcquired {}
 
@@ -310,6 +311,12 @@
     int FINGERPRINT_ACQUIRED_POWER_PRESSED = 11;
 
     /**
+     * This message is sent to encourage the user to re-enroll their fingerprints.
+     * @hide
+     */
+    int FINGERPRINT_ACQUIRED_RE_ENROLL = 12;
+
+    /**
      * @hide
      */
     int FINGERPRINT_ACQUIRED_VENDOR_BASE = 1000;
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 14f050d..708ebdf 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
 import static android.Manifest.permission.RECORD_AUDIO;
+import static android.service.voice.SoundTriggerFailure.ERROR_CODE_UNKNOWN;
 import static android.service.voice.VoiceInteractionService.MULTIPLE_ACTIVE_HOTWORD_DETECTORS;
 
 import android.annotation.ElapsedRealtimeLong;
@@ -270,6 +271,15 @@
     static final long THROW_ON_INITIALIZE_IF_NO_DSP = 269165460L;
 
     /**
+     * Gates returning {@link Callback#onFailure} and {@link Callback#onUnknownFailure}
+     * when asynchronous exceptions are propagated to the client. If the change is not enabled,
+     * the existing behavior of delivering {@link #STATE_ERROR} is retained.
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    static final long SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS = 280471513L;
+
+    /**
      * Controls the sensitivity threshold adjustment factor for a given model.
      * Negative value corresponds to less sensitive model (high threshold) and
      * a positive value corresponds to a more sensitive model (low threshold).
@@ -313,7 +323,6 @@
     private final Executor mExternalExecutor;
     private final Handler mHandler;
     private final IBinder mBinder = new Binder();
-    private final int mTargetSdkVersion;
     private final boolean mSupportSandboxedDetectionService;
 
     @GuardedBy("mLock")
@@ -856,7 +865,6 @@
                 new Handler(Looper.myLooper()));
         mInternalCallback = new SoundTriggerListener(mHandler);
         mModelManagementService = modelManagementService;
-        mTargetSdkVersion = targetSdkVersion;
         mSupportSandboxedDetectionService = supportSandboxedDetectionService;
     }
 
@@ -1382,6 +1390,7 @@
      *
      * @hide
      */
+    // TODO(b/281608561): remove the enrollment flow from AlwaysOnHotwordDetector
     void onSoundModelsChanged() {
         synchronized (mLock) {
             if (mAvailability == STATE_INVALID
@@ -1401,20 +1410,38 @@
                 return;
             }
 
-            // Stop the recognition before proceeding.
-            // This is done because we want to stop the recognition on an older model if it changed
-            // or was deleted.
-            // The availability change callback should ensure that the client starts recognition
-            // again if needed.
+            // Stop the recognition before proceeding if we are in the enrolled state.
+            // The framework makes the guarantee that an actively used model is present in the
+            // system server's enrollment database. For this reason we much stop an actively running
+            // model when the underlying sound model in enrollment database no longer match.
             if (mAvailability == STATE_KEYPHRASE_ENROLLED) {
+                // A SoundTriggerFailure will be sent to the client if the model state was
+                // changed. This is an overloading of the onFailure usage because we are sending a
+                // callback even in the successful stop case. If stopRecognition is successful,
+                // suggested next action RESTART_RECOGNITION will be sent.
+                // TODO(b/281608561): This code path will be removed with other enrollment flows in
+                //  this class.
                 try {
-                    stopRecognitionLocked();
-                } catch (SecurityException e) {
-                    Slog.w(TAG, "Failed to Stop the recognition", e);
-                    if (mTargetSdkVersion <= Build.VERSION_CODES.R) {
-                        throw e;
+                    int result = stopRecognitionLocked();
+                    if (result == STATUS_OK) {
+                        sendSoundTriggerFailure(new SoundTriggerFailure(ERROR_CODE_UNKNOWN,
+                                "stopped recognition because of enrollment update",
+                                FailureSuggestedAction.RESTART_RECOGNITION));
                     }
-                    updateAndNotifyStateChangedLocked(STATE_ERROR);
+                    // only log to logcat here because many failures can be false positives such as
+                    // calling stopRecognition where there is no started session.
+                    Log.w(TAG, "Failed to stop recognition after enrollment update: code="
+                            + result);
+                } catch (Exception e) {
+                    Slog.w(TAG, "Failed to stop recognition after enrollment update", e);
+                    if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
+                        sendSoundTriggerFailure(new SoundTriggerFailure(ERROR_CODE_UNKNOWN,
+                                "Failed to stop recognition after enrollment update: "
+                                        + Log.getStackTraceString(e),
+                                FailureSuggestedAction.RECREATE_DETECTOR));
+                    } else {
+                        updateAndNotifyStateChangedLocked(STATE_ERROR);
+                    }
                     return;
                 }
             }
@@ -1538,6 +1565,12 @@
 
     @GuardedBy("mLock")
     private void updateAndNotifyStateChangedLocked(int availability) {
+        updateAvailabilityLocked(availability);
+        notifyStateChangedLocked();
+    }
+
+    @GuardedBy("mLock")
+    private void updateAvailabilityLocked(int availability) {
         if (DBG) {
             Slog.d(TAG, "Hotword availability changed from " + mAvailability
                     + " -> " + availability);
@@ -1545,7 +1578,6 @@
         if (!mIsAvailabilityOverriddenByTestApi) {
             mAvailability = availability;
         }
-        notifyStateChangedLocked();
     }
 
     @GuardedBy("mLock")
@@ -1555,6 +1587,18 @@
         message.sendToTarget();
     }
 
+    @GuardedBy("mLock")
+    private void sendUnknownFailure(String failureMessage) {
+        // update but do not call onAvailabilityChanged callback for STATE_ERROR
+        updateAvailabilityLocked(STATE_ERROR);
+        Message.obtain(mHandler, MSG_DETECTION_UNKNOWN_FAILURE, failureMessage).sendToTarget();
+    }
+
+    private void sendSoundTriggerFailure(@NonNull SoundTriggerFailure soundTriggerFailure) {
+        Message.obtain(mHandler, MSG_DETECTION_SOUND_TRIGGER_FAILURE, soundTriggerFailure)
+                .sendToTarget();
+    }
+
     /** @hide */
     static final class SoundTriggerListener extends IHotwordRecognitionStatusCallback.Stub {
         private final Handler mHandler;
@@ -1577,6 +1621,7 @@
                             .build())
                     .sendToTarget();
         }
+
         @Override
         public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
             Slog.w(TAG, "Generic sound trigger event detected at AOHD: " + event);
@@ -1726,6 +1771,7 @@
         }
     }
 
+    // TODO(b/267681692): remove the AsyncTask usage
     class RefreshAvailabilityTask extends AsyncTask<Void, Void, Void> {
 
         @Override
@@ -1744,13 +1790,17 @@
                     }
                     updateAndNotifyStateChangedLocked(availability);
                 }
-            } catch (SecurityException e) {
+            } catch (Exception e) {
+                // Any exception here not caught will crash the process because AsyncTask does not
+                // bubble up the exceptions to the client app, so we must propagate it to the app.
                 Slog.w(TAG, "Failed to refresh availability", e);
-                if (mTargetSdkVersion <= Build.VERSION_CODES.R) {
-                    throw e;
-                }
                 synchronized (mLock) {
-                    updateAndNotifyStateChangedLocked(STATE_ERROR);
+                    if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
+                        sendUnknownFailure(
+                                "Failed to refresh availability: " + Log.getStackTraceString(e));
+                    } else {
+                        updateAndNotifyStateChangedLocked(STATE_ERROR);
+                    }
                 }
             }
 
diff --git a/core/java/android/service/voice/SoundTriggerFailure.java b/core/java/android/service/voice/SoundTriggerFailure.java
index 2ce5e5d..b4b0ffaf 100644
--- a/core/java/android/service/voice/SoundTriggerFailure.java
+++ b/core/java/android/service/voice/SoundTriggerFailure.java
@@ -74,14 +74,22 @@
     public @interface SoundTriggerErrorCode {}
 
     private final int mErrorCode;
+    private final int mSuggestedAction;
     private final String mErrorMessage;
 
     /**
      * @hide
      */
     @TestApi
-    public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode,
-            @NonNull String errorMessage) {
+    public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode, @NonNull String errorMessage) {
+        this(errorCode, errorMessage, getSuggestedActionBasedOnErrorCode(errorCode));
+    }
+
+    /**
+     * @hide
+     */
+    public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode, @NonNull String errorMessage,
+            @FailureSuggestedAction.FailureSuggestedActionDef int suggestedAction) {
         if (TextUtils.isEmpty(errorMessage)) {
             throw new IllegalArgumentException("errorMessage is empty or null.");
         }
@@ -95,7 +103,13 @@
             default:
                 throw new IllegalArgumentException("Invalid ErrorCode: " + errorCode);
         }
+        if (suggestedAction != getSuggestedActionBasedOnErrorCode(errorCode)
+                && errorCode != ERROR_CODE_UNKNOWN) {
+            throw new IllegalArgumentException("Invalid suggested next action: "
+                    + "errorCode=" + errorCode + ", suggestedAction=" + suggestedAction);
+        }
         mErrorMessage = errorMessage;
+        mSuggestedAction = suggestedAction;
     }
 
     /**
@@ -119,7 +133,11 @@
      */
     @FailureSuggestedAction.FailureSuggestedActionDef
     public int getSuggestedAction() {
-        switch (mErrorCode) {
+        return mSuggestedAction;
+    }
+
+    private static int getSuggestedActionBasedOnErrorCode(@SoundTriggerErrorCode int errorCode) {
+        switch (errorCode) {
             case ERROR_CODE_UNKNOWN:
             case ERROR_CODE_MODULE_DIED:
             case ERROR_CODE_UNEXPECTED_PREEMPTION:
@@ -144,8 +162,11 @@
 
     @Override
     public String toString() {
-        return "SoundTriggerFailure { errorCode = " + mErrorCode + ", errorMessage = "
-                + mErrorMessage + " }";
+        return "SoundTriggerFailure {"
+                + " errorCode = " + mErrorCode
+                + ", errorMessage = " + mErrorMessage
+                + ", suggestedNextAction = " + mSuggestedAction
+                + " }";
     }
 
     public static final @NonNull Parcelable.Creator<SoundTriggerFailure> CREATOR =
diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl
index 88447da..ff3c015 100644
--- a/core/java/com/android/internal/app/IAppOpsService.aidl
+++ b/core/java/com/android/internal/app/IAppOpsService.aidl
@@ -52,6 +52,8 @@
     int checkAudioOperation(int code, int usage, int uid, String packageName);
     boolean shouldCollectNotes(int opCode);
     void setCameraAudioRestriction(int mode);
+    void startWatchingModeWithFlags(int op, String packageName, int flags,
+            IAppOpsCallback callback);
     // End of methods also called by native code.
     // Any new method exposed to native must be added after the last one, do not reorder
 
@@ -110,8 +112,6 @@
     void startWatchingStarted(in int[] ops, IAppOpsStartedCallback callback);
     void stopWatchingStarted(IAppOpsStartedCallback callback);
 
-    void startWatchingModeWithFlags(int op, String packageName, int flags, IAppOpsCallback callback);
-
     void startWatchingNoted(in int[] ops, IAppOpsNotedCallback callback);
     void stopWatchingNoted(IAppOpsNotedCallback callback);
 
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index 80f540c..506f19f 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -275,7 +275,7 @@
         }
     }
 
-    @VisibleForTesting
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public Handler getHandler() {
         return mHandler;
     }
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 2b9db70..e530aec 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -772,11 +772,12 @@
         return true;
     }
 
+    @UiThread
     private void putTracker(@CujType int cuj, @NonNull FrameTracker tracker) {
         synchronized (mLock) {
             mRunningTrackers.put(cuj, tracker);
             if (mDebugOverlay != null) {
-                mDebugOverlay.onTrackerAdded(cuj, tracker.getViewRoot());
+                mDebugOverlay.onTrackerAdded(cuj, tracker);
             }
             if (DEBUG) {
                 Log.d(TAG, "Added tracker for " + getNameOfCuj(cuj)
@@ -791,6 +792,7 @@
         }
     }
 
+    @UiThread
     private void removeTracker(@CujType int cuj, int reason) {
         synchronized (mLock) {
             mRunningTrackers.remove(cuj);
@@ -818,7 +820,7 @@
                 SETTINGS_DEBUG_OVERLAY_ENABLED_KEY,
                 DEFAULT_DEBUG_OVERLAY_ENABLED);
         if (debugOverlayEnabled && mDebugOverlay == null) {
-            mDebugOverlay = new InteractionMonitorDebugOverlay(mDebugBgColor, mDebugYOffset);
+            mDebugOverlay = new InteractionMonitorDebugOverlay(mLock, mDebugBgColor, mDebugYOffset);
         } else if (!debugOverlayEnabled && mDebugOverlay != null) {
             mDebugOverlay.dispose();
             mDebugOverlay = null;
diff --git a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java
index 99b9f2f..ef7944c 100644
--- a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java
+++ b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java
@@ -19,25 +19,30 @@
 import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL;
 
 import android.annotation.ColorInt;
+import android.annotation.UiThread;
 import android.app.ActivityThread;
 import android.content.Context;
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.os.Trace;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.view.WindowCallbacks;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.jank.FrameTracker.Reasons;
 import com.android.internal.jank.InteractionJankMonitor.CujType;
 
 /**
  * An overlay that uses WindowCallbacks to draw the names of all running CUJs to the window
  * associated with one of the CUJs being tracked. There's no guarantee which window it will
- * draw to. NOTE: sometimes the CUJ names will remain displayed on the screen longer than they
- * are actually running.
+ * draw to. Traces that use the debug overlay should not be used for performance analysis.
+ * <p>
+ * To enable the overlay, run the following: <code>adb shell device_config put
+ * interaction_jank_monitor debug_overlay_enabled true</code>
  * <p>
  * CUJ names will be drawn as follows:
  * <ul>
@@ -45,12 +50,16 @@
  * <li> Grey text indicates the CUJ ended normally and is no longer running
  * <li> Red text with a strikethrough indicates the CUJ was canceled or ended abnormally
  * </ul>
+ * @hide
  */
 class InteractionMonitorDebugOverlay implements WindowCallbacks {
     private static final int REASON_STILL_RUNNING = -1000;
+    private final Object mLock;
     // Sparse array where the key in the CUJ and the value is the session status, or null if
     // it's currently running
+    @GuardedBy("mLock")
     private final SparseIntArray mRunningCujs = new SparseIntArray();
+    private Handler mHandler = null;
     private FrameTracker.ViewRootWrapper mViewRoot = null;
     private final Paint mDebugPaint;
     private final Paint.FontMetrics mDebugFontMetrics;
@@ -59,8 +68,10 @@
     private final int mBgColor;
     private final double mYOffset;
     private final String mPackageName;
+    private static final String TRACK_NAME = "InteractionJankMonitor";
 
-    InteractionMonitorDebugOverlay(@ColorInt int bgColor, double yOffset) {
+    InteractionMonitorDebugOverlay(Object lock, @ColorInt int bgColor, double yOffset) {
+        mLock = lock;
         mBgColor = bgColor;
         mYOffset = yOffset;
         mDebugPaint = new Paint();
@@ -70,18 +81,30 @@
         mPackageName = context.getPackageName();
     }
 
+    @UiThread
     void dispose() {
-        if (mViewRoot != null) {
-            mViewRoot.removeWindowCallbacks(this);
+        if (mViewRoot != null && mHandler != null) {
+            mHandler.runWithScissors(() ->  mViewRoot.removeWindowCallbacks(this),
+                    InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT);
             forceRedraw();
         }
+        mHandler = null;
         mViewRoot = null;
+        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0);
     }
 
-    private boolean attachViewRootIfNeeded(FrameTracker.ViewRootWrapper viewRoot) {
+    @UiThread
+    private boolean attachViewRootIfNeeded(FrameTracker tracker) {
+        FrameTracker.ViewRootWrapper viewRoot = tracker.getViewRoot();
         if (mViewRoot == null && viewRoot != null) {
+            // Add a trace marker so we can identify traces that were captured while the debug
+            // overlay was enabled. Traces that use the debug overlay should NOT be used for
+            // performance analysis.
+            Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, "DEBUG_OVERLAY_DRAW", 0);
+            mHandler = tracker.getHandler();
             mViewRoot = viewRoot;
-            viewRoot.addWindowCallbacks(this);
+            mHandler.runWithScissors(() -> viewRoot.addWindowCallbacks(this),
+                    InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT);
             forceRedraw();
             return true;
         }
@@ -115,52 +138,61 @@
         }
     }
 
+    @UiThread
     private void forceRedraw() {
-        if (mViewRoot != null) {
-            mViewRoot.requestInvalidateRootRenderNode();
-            mViewRoot.getView().invalidate();
+        if (mViewRoot != null && mHandler != null) {
+            mHandler.runWithScissors(() -> {
+                mViewRoot.requestInvalidateRootRenderNode();
+                mViewRoot.getView().invalidate();
+            }, InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT);
         }
     }
 
+    @UiThread
     void onTrackerRemoved(@CujType int removedCuj, @Reasons int reason,
                           SparseArray<FrameTracker> runningTrackers) {
-        mRunningCujs.put(removedCuj, reason);
-        // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended
-        if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) {
-            mRunningCujs.clear();
-            dispose();
-        } else {
-            boolean needsNewViewRoot = true;
-            if (mViewRoot != null) {
-                // Check to see if this viewroot is still associated with one of the running
-                // trackers
-                for (int i = 0; i < runningTrackers.size(); i++) {
-                    if (mViewRoot.equals(
-                            runningTrackers.valueAt(i).getViewRoot())) {
-                        needsNewViewRoot = false;
-                        break;
-                    }
-                }
-            }
-            if (needsNewViewRoot) {
+        synchronized (mLock) {
+            mRunningCujs.put(removedCuj, reason);
+            // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended
+            if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) {
+                mRunningCujs.clear();
                 dispose();
-                for (int i = 0; i < runningTrackers.size(); i++) {
-                    if (attachViewRootIfNeeded(runningTrackers.valueAt(i).getViewRoot())) {
-                        break;
+            } else {
+                boolean needsNewViewRoot = true;
+                if (mViewRoot != null) {
+                    // Check to see if this viewroot is still associated with one of the running
+                    // trackers
+                    for (int i = 0; i < runningTrackers.size(); i++) {
+                        if (mViewRoot.equals(
+                                runningTrackers.valueAt(i).getViewRoot())) {
+                            needsNewViewRoot = false;
+                            break;
+                        }
                     }
                 }
-            } else {
-                forceRedraw();
+                if (needsNewViewRoot) {
+                    dispose();
+                    for (int i = 0; i < runningTrackers.size(); i++) {
+                        if (attachViewRootIfNeeded(runningTrackers.valueAt(i))) {
+                            break;
+                        }
+                    }
+                } else {
+                    forceRedraw();
+                }
             }
         }
     }
 
-    void onTrackerAdded(@CujType int addedCuj, FrameTracker.ViewRootWrapper viewRoot) {
-        // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ
-        // is still running
-        mRunningCujs.put(addedCuj, REASON_STILL_RUNNING);
-        attachViewRootIfNeeded(viewRoot);
-        forceRedraw();
+    @UiThread
+    void onTrackerAdded(@CujType int addedCuj, FrameTracker tracker) {
+        synchronized (mLock) {
+            // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ
+            // is still running
+            mRunningCujs.put(addedCuj, REASON_STILL_RUNNING);
+            attachViewRootIfNeeded(tracker);
+            forceRedraw();
+        }
     }
 
     @Override
@@ -188,7 +220,6 @@
 
     @Override
     public void onPostDraw(RecordingCanvas canvas) {
-        Trace.beginSection("InteractionJankMonitor#drawDebug");
         final int padding = dipToPx(5);
         final int h = canvas.getHeight();
         final int w = canvas.getWidth();
@@ -235,6 +266,5 @@
             canvas.translate(0, cujNameTextHeight);
             canvas.drawText(cujName, 0, 0, mDebugPaint);
         }
-        Trace.endSection();
     }
 }
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 73e3b41..e08f085 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -675,8 +675,6 @@
   <java-symbol type="string" name="contentServiceSyncNotificationTitle" />
   <java-symbol type="string" name="contentServiceTooManyDeletesNotificationDesc" />
   <java-symbol type="string" name="csd_dose_reached_warning" />
-  <java-symbol type="string" name="csd_dose_repeat_warning" />
-  <java-symbol type="string" name="csd_entering_RS2_warning" />
   <java-symbol type="string" name="csd_momentary_exposure_warning" />
   <java-symbol type="string" name="date_and_time" />
   <java-symbol type="string" name="date_picker_decrement_day_button" />
diff --git a/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml b/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml
index ad888e5..1b80cc6 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml
@@ -20,5 +20,6 @@
     <style name="MainSwitchText.Settingslib" parent="@android:style/TextAppearance.DeviceDefault.Widget.ActionBar.Title.Inverse">
         <item name="android:textSize">20sp</item>
         <item name="android:fontFamily">@string/settingslib_config_headlineFontFamily</item>
+        <item name="android:textColor">@android:color/black</item>
     </style>
 </resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt
index d55a027..b386e5e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt
@@ -36,6 +36,9 @@
     /** Same as [sensorBounds], but in native resolution. */
     val nativeSensorBounds = Rect(sensorBounds).apply { scale(1f / scaleFactor) }
 
+    /** Same as [overlayBounds], but in native resolution. */
+    val nativeOverlayBounds = Rect(overlayBounds).apply { scale(1f / scaleFactor) }
+
     /** See [android.view.DisplayInfo.logicalWidth] */
     val logicalDisplayWidth =
         if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
diff --git a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
index c12bfcc..0e6b281 100644
--- a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
+++ b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
@@ -32,8 +32,8 @@
         <com.airbnb.lottie.LottieAnimationView
             android:id="@+id/rear_display_folded_animation"
             android:importantForAccessibility="no"
-            android:layout_width="@dimen/rear_display_animation_width"
-            android:layout_height="@dimen/rear_display_animation_height"
+            android:layout_width="@dimen/rear_display_animation_width_opened"
+            android:layout_height="@dimen/rear_display_animation_height_opened"
             android:layout_gravity="center"
             android:contentDescription="@string/rear_display_accessibility_unfolded_animation"
             android:scaleType="fitXY"
@@ -49,8 +49,8 @@
         android:text="@string/rear_display_unfolded_bottom_sheet_title"
         android:textAppearance="@style/TextAppearance.Dialog.Title"
         android:lineSpacingExtra="2sp"
-        android:paddingTop="@dimen/rear_display_title_top_padding"
-        android:paddingBottom="@dimen/rear_display_title_bottom_padding"
+        android:paddingTop="@dimen/rear_display_title_top_padding_opened"
+        android:paddingBottom="@dimen/rear_display_title_bottom_padding_opened"
         android:gravity="center_horizontal|center_vertical"
     />
 
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index 908aac4..f277e8a 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -67,6 +67,12 @@
     <dimen name="controls_header_horizontal_padding">12dp</dimen>
     <dimen name="controls_content_margin_horizontal">16dp</dimen>
 
+    <!-- Rear Display Education dimens -->
+    <dimen name="rear_display_animation_width">246dp</dimen>
+    <dimen name="rear_display_animation_height">180dp</dimen>
+    <dimen name="rear_display_title_top_padding">4dp</dimen>
+    <dimen name="rear_display_title_bottom_padding">0dp</dimen>
+
     <!-- Bouncer user switcher margins -->
     <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen>
     <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index c82d054..2024dae 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1772,8 +1772,12 @@
     <!-- Rear Display Education dimens -->
     <dimen name="rear_display_animation_width">273dp</dimen>
     <dimen name="rear_display_animation_height">200dp</dimen>
+    <dimen name="rear_display_animation_width_opened">273dp</dimen>
+    <dimen name="rear_display_animation_height_opened">200dp</dimen>
     <dimen name="rear_display_title_top_padding">24dp</dimen>
     <dimen name="rear_display_title_bottom_padding">16dp</dimen>
+    <dimen name="rear_display_title_top_padding_opened">24dp</dimen>
+    <dimen name="rear_display_title_bottom_padding_opened">16dp</dimen>
 
     <!-- Bouncer user switcher margins -->
     <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 1cbcb9d..5b1edc7 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -257,14 +257,14 @@
          * Authentication has happened and it's time to dismiss keyguard. This function
          * should clean up and inform KeyguardViewMediator.
          *
-         * @param strongAuth whether the user has authenticated with strong authentication like
+         * @param fromPrimaryAuth whether the user has authenticated with primary auth like
          *                   pattern, password or PIN but not by trust agents or fingerprint
          * @param targetUserId a user that needs to be the foreground user at the dismissal
          *                    completion.
          */
         @Override
-        public void finish(boolean strongAuth, int targetUserId) {
-            if (!mKeyguardStateController.canDismissLockScreen() && !strongAuth) {
+        public void finish(boolean fromPrimaryAuth, int targetUserId) {
+            if (!mKeyguardStateController.canDismissLockScreen() && !fromPrimaryAuth) {
                 Log.e(TAG,
                         "Tried to dismiss keyguard when lockscreen is not dismissible and user "
                                 + "was not authenticated with a primary security method "
@@ -283,9 +283,9 @@
             }
             if (mViewMediatorCallback != null) {
                 if (deferKeyguardDone) {
-                    mViewMediatorCallback.keyguardDonePending(strongAuth, targetUserId);
+                    mViewMediatorCallback.keyguardDonePending(fromPrimaryAuth, targetUserId);
                 } else {
-                    mViewMediatorCallback.keyguardDone(strongAuth, targetUserId);
+                    mViewMediatorCallback.keyguardDone(fromPrimaryAuth, targetUserId);
                 }
             }
         }
@@ -603,8 +603,8 @@
     /**
      * Dismiss keyguard due to a user unlock event.
      */
-    public void finish(boolean strongAuth, int currentUser) {
-        mKeyguardSecurityCallback.finish(strongAuth, currentUser);
+    public void finish(boolean primaryAuth, int currentUser) {
+        mKeyguardSecurityCallback.finish(primaryAuth, currentUser);
     }
 
     /**
@@ -736,7 +736,7 @@
         }
 
         boolean finish = false;
-        boolean strongAuth = false;
+        boolean primaryAuth = false;
         int eventSubtype = -1;
         BouncerUiEvent uiEvent = BouncerUiEvent.UNKNOWN;
         if (mUpdateMonitor.getUserHasTrust(targetUserId)) {
@@ -761,7 +761,7 @@
                 case Pattern:
                 case Password:
                 case PIN:
-                    strongAuth = true;
+                    primaryAuth = true;
                     finish = true;
                     eventSubtype = BOUNCER_DISMISS_PASSWORD;
                     uiEvent = BouncerUiEvent.BOUNCER_DISMISS_PASSWORD;
@@ -805,7 +805,7 @@
             mUiEventLogger.log(uiEvent, getSessionId());
         }
         if (finish) {
-            mKeyguardSecurityCallback.finish(strongAuth, targetUserId);
+            mKeyguardSecurityCallback.finish(primaryAuth, targetUserId);
         }
         return finish;
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 77e13ce..95e97ff 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -893,7 +893,7 @@
     }
 
     @VisibleForTesting
-    protected void onFingerprintAuthenticated(int userId, boolean isStrongBiometric) {
+    public void onFingerprintAuthenticated(int userId, boolean isStrongBiometric) {
         Assert.isMainThread();
         Trace.beginSection("KeyGuardUpdateMonitor#onFingerPrintAuthenticated");
         mUserFingerprintAuthenticated.put(userId,
@@ -1169,7 +1169,7 @@
     }
 
     @VisibleForTesting
-    protected void onFaceAuthenticated(int userId, boolean isStrongBiometric) {
+    public void onFaceAuthenticated(int userId, boolean isStrongBiometric) {
         Trace.beginSection("KeyGuardUpdateMonitor#onFaceAuthenticated");
         Assert.isMainThread();
         mUserFaceAuthenticated.put(userId,
diff --git a/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java
index 9308773..50f8f7e 100644
--- a/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java
@@ -29,11 +29,11 @@
     /**
      * Report that the keyguard is done.
      *
-     * @param strongAuth whether the user has authenticated with strong authentication like
+     * @param primaryAuth whether the user has authenticated with primary authentication like
      *                   pattern, password or PIN but not by trust agents or fingerprint
      * @param targetUserId a user that needs to be the foreground user at the completion.
      */
-    void keyguardDone(boolean strongAuth, int targetUserId);
+    void keyguardDone(boolean primaryAuth, int targetUserId);
 
     /**
      * Report that the keyguard is done drawing.
@@ -49,11 +49,11 @@
     /**
      * Report that the keyguard is dismissable, pending the next keyguardDone call.
      *
-     * @param strongAuth whether the user has authenticated with strong authentication like
+     * @param primaryAuth whether the user has authenticated with primary authentication like
      *                   pattern, password or PIN but not by trust agents or fingerprint
      * @param targetUserId a user that needs to be the foreground user at the completion.
      */
-    void keyguardDonePending(boolean strongAuth, int targetUserId);
+    void keyguardDonePending(boolean primaryAuth, int targetUserId);
 
     /**
      * Report when keyguard is actually gone
diff --git a/packages/SystemUI/src/com/android/systemui/LatencyTester.java b/packages/SystemUI/src/com/android/systemui/LatencyTester.java
index 8f41956..39d9714 100644
--- a/packages/SystemUI/src/com/android/systemui/LatencyTester.java
+++ b/packages/SystemUI/src/com/android/systemui/LatencyTester.java
@@ -20,7 +20,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricSourceType;
 import android.os.Build;
 import android.provider.DeviceConfig;
@@ -57,6 +56,7 @@
     private final BiometricUnlockController mBiometricUnlockController;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final DeviceConfigProxy mDeviceConfigProxy;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
 
     private boolean mEnabled;
 
@@ -65,11 +65,13 @@
             BiometricUnlockController biometricUnlockController,
             BroadcastDispatcher broadcastDispatcher,
             DeviceConfigProxy deviceConfigProxy,
-            @Main DelayableExecutor mainExecutor
+            @Main DelayableExecutor mainExecutor,
+            KeyguardUpdateMonitor keyguardUpdateMonitor
     ) {
         mBiometricUnlockController = biometricUnlockController;
         mBroadcastDispatcher = broadcastDispatcher;
         mDeviceConfigProxy = deviceConfigProxy;
+        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
 
         updateEnabled();
         mDeviceConfigProxy.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_LATENCY_TRACKER,
@@ -85,10 +87,13 @@
         if (!mEnabled) {
             return;
         }
-        mBiometricUnlockController.onBiometricAcquired(type,
-                BiometricConstants.BIOMETRIC_ACQUIRED_GOOD);
-        mBiometricUnlockController.onBiometricAuthenticated(
-                KeyguardUpdateMonitor.getCurrentUser(), type, true /* isStrongBiometric */);
+        if (type == BiometricSourceType.FACE) {
+            mKeyguardUpdateMonitor.onFaceAuthenticated(KeyguardUpdateMonitor.getCurrentUser(),
+                    true);
+        } else if (type == BiometricSourceType.FINGERPRINT) {
+            mKeyguardUpdateMonitor.onFingerprintAuthenticated(
+                    KeyguardUpdateMonitor.getCurrentUser(), true);
+        }
     }
 
     private void registerForBroadcasts(boolean register) {
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt
new file mode 100644
index 0000000..7c394a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.authentication
+
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryModule
+import dagger.Module
+
+@Module(
+    includes =
+        [
+            AuthenticationRepositoryModule::class,
+        ],
+)
+object AuthenticationModule
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
new file mode 100644
index 0000000..cd195f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.authentication.data.repository
+
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Defines interface for classes that can access authentication-related application state. */
+interface AuthenticationRepository {
+
+    /**
+     * Whether the device is unlocked.
+     *
+     * A device that is not yet unlocked requires unlocking by completing an authentication
+     * challenge according to the current authentication method.
+     *
+     * Note that this state has no real bearing on whether the lock screen is showing or dismissed.
+     */
+    val isUnlocked: StateFlow<Boolean>
+
+    /**
+     * The currently-configured authentication method. This determines how the authentication
+     * challenge is completed in order to unlock an otherwise locked device.
+     */
+    val authenticationMethod: StateFlow<AuthenticationMethodModel>
+
+    /**
+     * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
+     * dismisses once the authentication challenge is completed. For example, completing a biometric
+     * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
+     * lock screen.
+     */
+    val isBypassEnabled: StateFlow<Boolean>
+
+    /** See [isUnlocked]. */
+    fun setUnlocked(isUnlocked: Boolean)
+
+    /** See [authenticationMethod]. */
+    fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel)
+
+    /** See [isBypassEnabled]. */
+    fun setBypassEnabled(isBypassEnabled: Boolean)
+}
+
+class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository {
+    // TODO(b/280883900): get data from real data sources in SysUI.
+
+    private val _isUnlocked = MutableStateFlow(false)
+    override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()
+
+    private val _authenticationMethod =
+        MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.PIN(1234))
+    override val authenticationMethod: StateFlow<AuthenticationMethodModel> =
+        _authenticationMethod.asStateFlow()
+
+    private val _isBypassEnabled = MutableStateFlow(false)
+    override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
+
+    override fun setUnlocked(isUnlocked: Boolean) {
+        _isUnlocked.value = isUnlocked
+    }
+
+    override fun setBypassEnabled(isBypassEnabled: Boolean) {
+        _isBypassEnabled.value = isBypassEnabled
+    }
+
+    override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
+        _authenticationMethod.value = authenticationMethod
+    }
+}
+
+@Module
+interface AuthenticationRepositoryModule {
+    @Binds fun repository(impl: AuthenticationRepositoryImpl): AuthenticationRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
new file mode 100644
index 0000000..5aea930
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.authentication.domain.interactor
+
+import com.android.systemui.authentication.data.repository.AuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** Hosts application business logic related to authentication. */
+@SysUISingleton
+class AuthenticationInteractor
+@Inject
+constructor(
+    @Application applicationScope: CoroutineScope,
+    private val repository: AuthenticationRepository,
+) {
+    /**
+     * The currently-configured authentication method. This determines how the authentication
+     * challenge is completed in order to unlock an otherwise locked device.
+     */
+    val authenticationMethod: StateFlow<AuthenticationMethodModel> = repository.authenticationMethod
+
+    /**
+     * Whether the device is unlocked.
+     *
+     * A device that is not yet unlocked requires unlocking by completing an authentication
+     * challenge according to the current authentication method.
+     *
+     * Note that this state has no real bearing on whether the lock screen is showing or dismissed.
+     */
+    val isUnlocked: StateFlow<Boolean> =
+        combine(authenticationMethod, repository.isUnlocked) { authMethod, isUnlocked ->
+                isUnlockedWithAuthMethod(
+                    isUnlocked = isUnlocked,
+                    authMethod = authMethod,
+                )
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue =
+                    isUnlockedWithAuthMethod(
+                        isUnlocked = repository.isUnlocked.value,
+                        authMethod = repository.authenticationMethod.value,
+                    )
+            )
+
+    /**
+     * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
+     * dismisses once the authentication challenge is completed. For example, completing a biometric
+     * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
+     * lock screen.
+     */
+    val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
+
+    init {
+        // UNLOCKS WHEN AUTH METHOD REMOVED.
+        //
+        // Unlocks the device if the auth method becomes None.
+        applicationScope.launch {
+            repository.authenticationMethod.collect {
+                if (it is AuthenticationMethodModel.None) {
+                    unlockDevice()
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns `true` if the device currently requires authentication before content can be viewed;
+     * `false` if content can be displayed without unlocking first.
+     */
+    fun isAuthenticationRequired(): Boolean {
+        return !isUnlocked.value && authenticationMethod.value.isSecure
+    }
+
+    /**
+     * Unlocks the device, assuming that the authentication challenge has been completed
+     * successfully.
+     */
+    fun unlockDevice() {
+        repository.setUnlocked(true)
+    }
+
+    /**
+     * Locks the device. From now on, the device will remain locked until [authenticate] is called
+     * with the correct input.
+     */
+    fun lockDevice() {
+        repository.setUnlocked(false)
+    }
+
+    /**
+     * Attempts to authenticate the user and unlock the device.
+     *
+     * @param input The input from the user to try to authenticate with. This can be a list of
+     *   different things, based on the current authentication method.
+     * @return `true` if the authentication succeeded and the device is now unlocked; `false`
+     *   otherwise.
+     */
+    fun authenticate(input: List<Any>): Boolean {
+        val isSuccessful =
+            when (val authMethod = this.authenticationMethod.value) {
+                is AuthenticationMethodModel.PIN -> input.asCode() == authMethod.code
+                is AuthenticationMethodModel.Password -> input.asPassword() == authMethod.password
+                is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates
+                else -> true
+            }
+
+        if (isSuccessful) {
+            repository.setUnlocked(true)
+        }
+
+        return isSuccessful
+    }
+
+    /** Triggers a biometric-powered unlock of the device. */
+    fun biometricUnlock() {
+        // TODO(b/280883900): only allow this if the biometric is enabled and there's a match.
+        repository.setUnlocked(true)
+    }
+
+    /** See [authenticationMethod]. */
+    fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
+        repository.setAuthenticationMethod(authenticationMethod)
+    }
+
+    /** See [isBypassEnabled]. */
+    fun toggleBypassEnabled() {
+        repository.setBypassEnabled(!repository.isBypassEnabled.value)
+    }
+
+    companion object {
+        private fun isUnlockedWithAuthMethod(
+            isUnlocked: Boolean,
+            authMethod: AuthenticationMethodModel,
+        ): Boolean {
+            return if (authMethod is AuthenticationMethodModel.None) {
+                true
+            } else {
+                isUnlocked
+            }
+        }
+
+        /**
+         * Returns a PIN code from the given list. It's assumed the given list elements are all
+         * [Int].
+         */
+        private fun List<Any>.asCode(): Int? {
+            if (isEmpty()) {
+                return null
+            }
+
+            var code = 0
+            map { it as Int }.forEach { integer -> code = code * 10 + integer }
+
+            return code
+        }
+
+        /**
+         * Returns a password from the given list. It's assumed the given list elements are all
+         * [Char].
+         */
+        private fun List<Any>.asPassword(): String {
+            val anyList = this
+            return buildString { anyList.forEach { append(it as Char) } }
+        }
+
+        /**
+         * Returns a list of [AuthenticationMethodModel.Pattern.PatternCoordinate] from the given
+         * list. It's assumed the given list elements are all
+         * [AuthenticationMethodModel.Pattern.PatternCoordinate].
+         */
+        private fun List<Any>.asPattern():
+            List<AuthenticationMethodModel.Pattern.PatternCoordinate> {
+            val anyList = this
+            return buildList {
+                anyList.forEach { add(it as AuthenticationMethodModel.Pattern.PatternCoordinate) }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
new file mode 100644
index 0000000..83250b6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.authentication.shared.model
+
+/** Enumerates all known authentication methods. */
+sealed class AuthenticationMethodModel(
+    /**
+     * Whether the authentication method is considered to be "secure".
+     *
+     * "Secure" authentication methods require authentication to unlock the device. Non-secure auth
+     * methods simply require user dismissal.
+     */
+    open val isSecure: Boolean,
+) {
+    /** There is no authentication method on the device. We shouldn't even show the lock screen. */
+    object None : AuthenticationMethodModel(isSecure = false)
+
+    /** The most basic authentication method. The lock screen can be swiped away when displayed. */
+    object Swipe : AuthenticationMethodModel(isSecure = false)
+
+    data class PIN(val code: Int) : AuthenticationMethodModel(isSecure = true)
+
+    data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)
+
+    data class Pattern(val coordinates: List<PatternCoordinate>) :
+        AuthenticationMethodModel(isSecure = true) {
+
+        data class PatternCoordinate(
+            val x: Int,
+            val y: Int,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java
index 4b17be3..6db266f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java
@@ -29,7 +29,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.hardware.biometrics.BiometricFaceConstants;
-import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.BiometricSourceType;
 import android.os.Handler;
 import android.os.UserHandle;
@@ -43,6 +42,9 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
+
+import java.util.Optional;
+
 import javax.inject.Inject;
 
 /**
@@ -66,6 +68,7 @@
     private final Handler mHandler;
     private final NotificationManager mNotificationManager;
     private final BiometricNotificationBroadcastReceiver mBroadcastReceiver;
+    private final FingerprintReEnrollNotification mFingerprintReEnrollNotification;
     private NotificationChannel mNotificationChannel;
     private boolean mFaceNotificationQueued;
     private boolean mFingerprintNotificationQueued;
@@ -102,8 +105,15 @@
                         Settings.Secure.putIntForUser(mContext.getContentResolver(),
                                 Settings.Secure.FACE_UNLOCK_RE_ENROLL, REENROLL_REQUIRED,
                                 UserHandle.USER_CURRENT);
-                    } else if (msgId == BiometricFingerprintConstants.BIOMETRIC_ERROR_RE_ENROLL
-                            && biometricSourceType == BiometricSourceType.FINGERPRINT) {
+                    }
+                }
+
+                @Override
+                public void onBiometricHelp(int msgId, String helpString,
+                        BiometricSourceType biometricSourceType) {
+                    if (biometricSourceType == BiometricSourceType.FINGERPRINT
+                            && mFingerprintReEnrollNotification.isFingerprintReEnrollRequired(
+                                    msgId)) {
                         mFingerprintReenrollRequired = true;
                     }
                 }
@@ -115,13 +125,16 @@
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             KeyguardStateController keyguardStateController,
             Handler handler, NotificationManager notificationManager,
-            BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver) {
+            BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver,
+            Optional<FingerprintReEnrollNotification> fingerprintReEnrollNotification) {
         mContext = context;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mKeyguardStateController = keyguardStateController;
         mHandler = handler;
         mNotificationManager = notificationManager;
         mBroadcastReceiver = biometricNotificationBroadcastReceiver;
+        mFingerprintReEnrollNotification = fingerprintReEnrollNotification.orElse(
+                new FingerprintReEnrollNotificationImpl());
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java
new file mode 100644
index 0000000..ca94e99
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java
@@ -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.biometrics;
+
+/**
+ * Checks if the fingerprint HAL has sent a re-enrollment request.
+ */
+public interface FingerprintReEnrollNotification {
+    /** Returns true if msgId corresponds to FINGERPRINT_ACQUIRED_RE_ENROLL. */
+    boolean isFingerprintReEnrollRequired(int msgId);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java
new file mode 100644
index 0000000..1f86bc6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java
@@ -0,0 +1,29 @@
+/*
+ * 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.biometrics;
+
+import android.hardware.biometrics.BiometricFingerprintConstants;
+
+/**
+ * Checks if the fingerprint HAL has sent a re-enrollment request.
+ */
+public class FingerprintReEnrollNotificationImpl implements FingerprintReEnrollNotification{
+    @Override
+    public boolean isFingerprintReEnrollRequired(int msgId) {
+        return msgId == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_RE_ENROLL;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index bc44df4..2eb5330 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -466,7 +466,7 @@
         if (!mOnFingerDown) {
             playStartHaptic();
         }
-        mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */);
+        mKeyguardViewManager.notifyKeyguardAuthenticated(false /* primaryAuth */);
         mAttemptedToDismissKeyguard = true;
     }
 
@@ -636,7 +636,7 @@
                     mOverlay.getOverlayView().getViewRootImpl().getInputToken());
         }
 
-        return processedTouch.getTouchData().isWithinSensor(mOverlayParams.getNativeSensorBounds());
+        return processedTouch.getTouchData().isWithinBounds(mOverlayParams.getNativeSensorBounds());
     }
 
     private boolean oldOnTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt
index 79a0acb..cf6044f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt
@@ -22,6 +22,11 @@
 /** Returns whether the touch coordinates are within the sensor's bounding box. */
 @SysUISingleton
 class BoundingBoxOverlapDetector : OverlapDetector {
-    override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean =
-        touchData.isWithinSensor(nativeSensorBounds)
+    override fun isGoodOverlap(
+        touchData: NormalizedTouchData,
+        nativeSensorBounds: Rect,
+        nativeOverlayBounds: Rect,
+    ): Boolean =
+        touchData.isWithinBounds(nativeOverlayBounds) &&
+            touchData.isWithinBounds(nativeSensorBounds)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
index baf8d74..f70e01d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
@@ -30,8 +30,8 @@
     TARGET // Pixel within sensor center target
 }
 
-private val isDebug = true
-private val TAG = "EllipseOverlapDetector"
+private const val isDebug = false
+private const val TAG = "EllipseOverlapDetector"
 
 /**
  * Approximates the touch as an ellipse and determines whether the ellipse has a sufficient overlap
@@ -39,12 +39,21 @@
  */
 @SysUISingleton
 class EllipseOverlapDetector(private val params: EllipseOverlapDetectorParams) : OverlapDetector {
-    override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean {
-        // First, check if touch is within bounding box,
-        if (nativeSensorBounds.contains(touchData.x.toInt(), touchData.y.toInt())) {
+    override fun isGoodOverlap(
+        touchData: NormalizedTouchData,
+        nativeSensorBounds: Rect,
+        nativeOverlayBounds: Rect,
+    ): Boolean {
+        // First, check if touch is within bounding box to exit early
+        if (touchData.isWithinBounds(nativeSensorBounds)) {
             return true
         }
 
+        // Check touch is within overlay bounds, not worth checking if outside
+        if (!touchData.isWithinBounds(nativeOverlayBounds)) {
+            return false
+        }
+
         var isTargetTouched = false
         var sensorPixels = 0
         var coveredPixels = 0
@@ -77,7 +86,7 @@
 
         val percentage: Float = coveredPixels.toFloat() / sensorPixels
         if (isDebug) {
-            Log.v(
+            Log.d(
                 TAG,
                 "covered: $coveredPixels, sensor: $sensorPixels, " +
                     "percentage: $percentage, isCenterTouched: $isTargetTouched"
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt
index 6854b50..1b4062a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt
@@ -51,16 +51,16 @@
 ) {
 
     /**
-     * [nativeSensorBounds] contains the location and dimensions of the sensor area in native
-     * resolution and natural orientation.
+     * [nativeBounds] contains the location and dimensions of the area in native resolution and
+     * natural orientation.
      *
-     * Returns whether the coordinates of the given pointer are within the sensor's bounding box.
+     * Returns whether the coordinates of the given pointer are within the bounding box.
      */
-    fun isWithinSensor(nativeSensorBounds: Rect): Boolean {
-        return nativeSensorBounds.left <= x &&
-            nativeSensorBounds.right >= x &&
-            nativeSensorBounds.top <= y &&
-            nativeSensorBounds.bottom >= y
+    fun isWithinBounds(nativeBounds: Rect): Boolean {
+        return nativeBounds.left <= x &&
+            nativeBounds.right >= x &&
+            nativeBounds.top <= y &&
+            nativeBounds.bottom >= y
     }
 
     @JvmOverloads
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt
index 0fec8ff..f163471 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt
@@ -20,5 +20,9 @@
 
 /** Determines whether the touch has a sufficient overlap with the sensor. */
 interface OverlapDetector {
-    fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean
+    fun isGoodOverlap(
+        touchData: NormalizedTouchData,
+        nativeSensorBounds: Rect,
+        nativeOverlayBounds: Rect
+    ): Boolean
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
index 6c9390d..eeb0f4c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
@@ -46,7 +46,13 @@
             val touchData = List(event.pointerCount) { event.normalize(it, overlayParams) }
             val pointersOnSensor =
                 touchData
-                    .filter { overlapDetector.isGoodOverlap(it, overlayParams.nativeSensorBounds) }
+                    .filter {
+                        overlapDetector.isGoodOverlap(
+                            it,
+                            overlayParams.nativeSensorBounds,
+                            overlayParams.nativeOverlayBounds
+                        )
+                    }
                     .map { it.pointerId }
             return PreprocessedTouch(touchData, previousPointerOnSensorId, pointersOnSensor)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
new file mode 100644
index 0000000..4c817b2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.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.bouncer.data.repo
+
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Provides access to bouncer-related application state. */
+@SysUISingleton
+class BouncerRepository @Inject constructor() {
+    private val _message = MutableStateFlow<String?>(null)
+    /** The user-facing message to show in the bouncer. */
+    val message: StateFlow<String?> = _message.asStateFlow()
+
+    fun setMessage(message: String?) {
+        _message.value = message
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
new file mode 100644
index 0000000..57ce580
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.bouncer.domain.interactor
+
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+/** Encapsulates business logic and application state accessing use-cases. */
+class BouncerInteractor
+@AssistedInject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Application private val applicationContext: Context,
+    private val repository: BouncerRepository,
+    private val authenticationInteractor: AuthenticationInteractor,
+    private val sceneInteractor: SceneInteractor,
+    @Assisted private val containerName: String,
+) {
+
+    /** The user-facing message to show in the bouncer. */
+    val message: StateFlow<String?> = repository.message
+
+    /**
+     * The currently-configured authentication method. This determines how the authentication
+     * challenge is completed in order to unlock an otherwise locked device.
+     */
+    val authenticationMethod: StateFlow<AuthenticationMethodModel> =
+        authenticationInteractor.authenticationMethod
+
+    init {
+        applicationScope.launch {
+            combine(
+                    sceneInteractor.currentScene(containerName),
+                    authenticationInteractor.authenticationMethod,
+                    ::Pair,
+                )
+                .collect { (currentScene, authMethod) ->
+                    if (currentScene.key == SceneKey.Bouncer) {
+                        when (authMethod) {
+                            is AuthenticationMethodModel.None ->
+                                sceneInteractor.setCurrentScene(
+                                    containerName,
+                                    SceneModel(SceneKey.Gone),
+                                )
+                            is AuthenticationMethodModel.Swipe ->
+                                sceneInteractor.setCurrentScene(
+                                    containerName,
+                                    SceneModel(SceneKey.LockScreen),
+                                )
+                            else -> Unit
+                        }
+                    }
+                }
+        }
+    }
+
+    /**
+     * Either shows the bouncer or unlocks the device, if the bouncer doesn't need to be shown.
+     *
+     * @param containerName The name of the scene container to show the bouncer in.
+     * @param message An optional message to show to the user in the bouncer.
+     */
+    fun showOrUnlockDevice(
+        containerName: String,
+        message: String? = null,
+    ) {
+        if (authenticationInteractor.isAuthenticationRequired()) {
+            repository.setMessage(message ?: promptMessage(authenticationMethod.value))
+            sceneInteractor.setCurrentScene(
+                containerName = containerName,
+                scene = SceneModel(SceneKey.Bouncer),
+            )
+        } else {
+            authenticationInteractor.unlockDevice()
+            sceneInteractor.setCurrentScene(
+                containerName = containerName,
+                scene = SceneModel(SceneKey.Gone),
+            )
+        }
+    }
+
+    /**
+     * Resets the user-facing message back to the default according to the current authentication
+     * method.
+     */
+    fun resetMessage() {
+        repository.setMessage(promptMessage(authenticationMethod.value))
+    }
+
+    /** Removes the user-facing message. */
+    fun clearMessage() {
+        repository.setMessage(null)
+    }
+
+    /**
+     * Attempts to authenticate based on the given user input.
+     *
+     * If the input is correct, the device will be unlocked and the lock screen and bouncer will be
+     * dismissed and hidden.
+     */
+    fun authenticate(
+        input: List<Any>,
+    ) {
+        val isAuthenticated = authenticationInteractor.authenticate(input)
+        if (isAuthenticated) {
+            sceneInteractor.setCurrentScene(
+                containerName = containerName,
+                scene = SceneModel(SceneKey.Gone),
+            )
+        } else {
+            repository.setMessage(errorMessage(authenticationMethod.value))
+        }
+    }
+
+    private fun promptMessage(authMethod: AuthenticationMethodModel): String {
+        return when (authMethod) {
+            is AuthenticationMethodModel.PIN ->
+                applicationContext.getString(R.string.keyguard_enter_your_pin)
+            is AuthenticationMethodModel.Password ->
+                applicationContext.getString(R.string.keyguard_enter_your_password)
+            is AuthenticationMethodModel.Pattern ->
+                applicationContext.getString(R.string.keyguard_enter_your_pattern)
+            else -> ""
+        }
+    }
+
+    private fun errorMessage(authMethod: AuthenticationMethodModel): String {
+        return when (authMethod) {
+            is AuthenticationMethodModel.PIN -> applicationContext.getString(R.string.kg_wrong_pin)
+            is AuthenticationMethodModel.Password ->
+                applicationContext.getString(R.string.kg_wrong_password)
+            is AuthenticationMethodModel.Pattern ->
+                applicationContext.getString(R.string.kg_wrong_pattern)
+            else -> ""
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            containerName: String,
+        ): BouncerInteractor
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
new file mode 100644
index 0000000..8a183ae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.dagger.qualifiers.Application
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Holds UI state and handles user input on bouncer UIs. */
+class BouncerViewModel
+@AssistedInject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    interactorFactory: BouncerInteractor.Factory,
+    containerName: String,
+) {
+    private val interactor: BouncerInteractor = interactorFactory.create(containerName)
+
+    /** The user-facing message to show in the bouncer. */
+    val message: StateFlow<String> =
+        interactor.message
+            .map { it ?: "" }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = interactor.message.value ?: "",
+            )
+
+    /** Notifies that the authenticate button was clicked. */
+    fun onAuthenticateButtonClicked() {
+        // TODO(b/280877228): remove this and send the real input.
+        interactor.authenticate(
+            when (interactor.authenticationMethod.value) {
+                is AuthenticationMethodModel.PIN -> listOf(1, 2, 3, 4)
+                is AuthenticationMethodModel.Password -> "password".toList()
+                is AuthenticationMethodModel.Pattern ->
+                    listOf(
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(2, 0),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(2, 1),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(2, 2),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(0, 0),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(0, 1),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(0, 2),
+                    )
+                else -> emptyList()
+            }
+        )
+    }
+
+    /** Notifies that the emergency services button was clicked. */
+    fun onEmergencyServicesButtonClicked() {
+        // TODO(b/280877228): implement this.
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 17cf808..70c859e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -32,8 +32,10 @@
 import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule;
 import com.android.systemui.appops.dagger.AppOpsModule;
 import com.android.systemui.assist.AssistModule;
+import com.android.systemui.authentication.AuthenticationModule;
 import com.android.systemui.biometrics.AlternateUdfpsTouchProvider;
 import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
+import com.android.systemui.biometrics.FingerprintReEnrollNotification;
 import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
 import com.android.systemui.biometrics.dagger.BiometricsModule;
 import com.android.systemui.biometrics.dagger.UdfpsModule;
@@ -152,6 +154,7 @@
             AccessibilityRepositoryModule.class,
             AppOpsModule.class,
             AssistModule.class,
+            AuthenticationModule.class,
             BiometricsModule.class,
             BouncerViewModule.class,
             ClipboardOverlayModule.class,
@@ -289,6 +292,9 @@
     @BindsOptionalOf
     abstract SystemStatusAnimationScheduler optionalSystemStatusAnimationScheduler();
 
+    @BindsOptionalOf
+    abstract FingerprintReEnrollNotification optionalFingerprintReEnrollNotification();
+
     @SysUISingleton
     @Binds
     abstract SystemClock bindSystemClock(SystemClockImpl systemClock);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 46ff0d9..ddc12c9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -79,7 +79,6 @@
 import android.provider.Settings;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
-import android.util.EventLog;
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
@@ -744,7 +743,7 @@
         }
 
         @Override
-        public void keyguardDone(boolean strongAuth, int targetUserId) {
+        public void keyguardDone(boolean primaryAuth, int targetUserId) {
             if (targetUserId != KeyguardUpdateMonitor.getCurrentUser()) {
                 return;
             }
@@ -765,7 +764,7 @@
         }
 
         @Override
-        public void keyguardDonePending(boolean strongAuth, int targetUserId) {
+        public void keyguardDonePending(boolean primaryAuth, int targetUserId) {
             Trace.beginSection("KeyguardViewMediator.mViewMediatorCallback#keyguardDonePending");
             if (DEBUG) Log.d(TAG, "keyguardDonePending");
             if (targetUserId != KeyguardUpdateMonitor.getCurrentUser()) {
@@ -2219,16 +2218,6 @@
         }
     };
 
-    private void keyguardDone() {
-        Trace.beginSection("KeyguardViewMediator#keyguardDone");
-        if (DEBUG) Log.d(TAG, "keyguardDone()");
-        userActivity();
-        EventLog.writeEvent(70000, 2);
-        Message msg = mHandler.obtainMessage(KEYGUARD_DONE);
-        mHandler.sendMessage(msg);
-        Trace.endSection();
-    }
-
     /**
      * This handler will be associated with the policy thread, which will also
      * be the UI thread of the keyguard.  Since the apis of the policy, and therefore
@@ -3124,7 +3113,8 @@
         Trace.beginSection("KeyguardViewMediator#onWakeAndUnlocking");
         mWakeAndUnlocking = true;
 
-        keyguardDone();
+        mKeyguardViewControllerLazy.get().notifyKeyguardAuthenticated(/* primaryAuth */ false);
+        userActivity();
         Trace.endSection();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt
new file mode 100644
index 0000000..6170180
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt
@@ -0,0 +1,186 @@
+/*
+ * 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.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.util.kotlin.pairwise
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** Hosts business and application state accessing logic for the lock screen scene. */
+class LockScreenSceneInteractor
+@AssistedInject
+constructor(
+    @Application applicationScope: CoroutineScope,
+    private val authenticationInteractor: AuthenticationInteractor,
+    bouncerInteractorFactory: BouncerInteractor.Factory,
+    private val sceneInteractor: SceneInteractor,
+    @Assisted private val containerName: String,
+) {
+    private val bouncerInteractor: BouncerInteractor =
+        bouncerInteractorFactory.create(containerName)
+
+    /** Whether the device is currently locked. */
+    val isDeviceLocked: StateFlow<Boolean> =
+        authenticationInteractor.isUnlocked
+            .map { !it }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = !authenticationInteractor.isUnlocked.value,
+            )
+
+    /** Whether it's currently possible to swipe up to dismiss the lock screen. */
+    val isSwipeToDismissEnabled: StateFlow<Boolean> =
+        combine(
+                authenticationInteractor.isUnlocked,
+                authenticationInteractor.authenticationMethod,
+            ) { isUnlocked, authMethod ->
+                isSwipeToUnlockEnabled(
+                    isUnlocked = isUnlocked,
+                    authMethod = authMethod,
+                )
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue =
+                    isSwipeToUnlockEnabled(
+                        isUnlocked = authenticationInteractor.isUnlocked.value,
+                        authMethod = authenticationInteractor.authenticationMethod.value,
+                    ),
+            )
+
+    init {
+        // LOCKING SHOWS LOCK SCREEN.
+        //
+        // Move to the lock screen scene if the device becomes locked while in any scene.
+        applicationScope.launch {
+            authenticationInteractor.isUnlocked
+                .map { !it }
+                .distinctUntilChanged()
+                .collect { isLocked ->
+                    if (isLocked) {
+                        sceneInteractor.setCurrentScene(
+                            containerName = containerName,
+                            scene = SceneModel(SceneKey.LockScreen),
+                        )
+                    }
+                }
+        }
+
+        // BYPASS UNLOCK.
+        //
+        // Moves to the gone scene if bypass is enabled and the device becomes unlocked while in the
+        // lock screen scene.
+        applicationScope.launch {
+            combine(
+                    authenticationInteractor.isBypassEnabled,
+                    authenticationInteractor.isUnlocked,
+                    sceneInteractor.currentScene(containerName),
+                    ::Triple,
+                )
+                .collect { (isBypassEnabled, isUnlocked, currentScene) ->
+                    if (isBypassEnabled && isUnlocked && currentScene.key == SceneKey.LockScreen) {
+                        sceneInteractor.setCurrentScene(
+                            containerName = containerName,
+                            scene = SceneModel(SceneKey.Gone),
+                        )
+                    }
+                }
+        }
+
+        // SWIPE TO DISMISS LOCK SCREEN.
+        //
+        // If switched from the lock screen to the gone scene and the auth method was a swipe,
+        // unlocks the device.
+        applicationScope.launch {
+            combine(
+                    authenticationInteractor.authenticationMethod,
+                    sceneInteractor.currentScene(containerName).pairwise(),
+                    ::Pair,
+                )
+                .collect { (authMethod, scenes) ->
+                    val (previousScene, currentScene) = scenes
+                    if (
+                        authMethod is AuthenticationMethodModel.Swipe &&
+                            previousScene.key == SceneKey.LockScreen &&
+                            currentScene.key == SceneKey.Gone
+                    ) {
+                        authenticationInteractor.unlockDevice()
+                    }
+                }
+        }
+
+        // DISMISS LOCK SCREEN IF AUTH METHOD IS REMOVED.
+        //
+        // If the auth method becomes None while on the lock screen scene, dismisses the lock
+        // screen.
+        applicationScope.launch {
+            combine(
+                    authenticationInteractor.authenticationMethod,
+                    sceneInteractor.currentScene(containerName),
+                    ::Pair,
+                )
+                .collect { (authMethod, scene) ->
+                    if (
+                        scene.key == SceneKey.LockScreen &&
+                            authMethod == AuthenticationMethodModel.None
+                    ) {
+                        sceneInteractor.setCurrentScene(
+                            containerName = containerName,
+                            scene = SceneModel(SceneKey.Gone),
+                        )
+                    }
+                }
+        }
+    }
+
+    /** Attempts to dismiss the lock screen. This will cause the bouncer to show, if needed. */
+    fun dismissLockScreen() {
+        bouncerInteractor.showOrUnlockDevice(containerName = containerName)
+    }
+
+    private fun isSwipeToUnlockEnabled(
+        isUnlocked: Boolean,
+        authMethod: AuthenticationMethodModel,
+    ): Boolean {
+        return !isUnlocked && authMethod is AuthenticationMethodModel.Swipe
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            containerName: String,
+        ): LockScreenSceneInteractor
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt
new file mode 100644
index 0000000..08b9613
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Models UI state and handles user input for the lock screen scene. */
+class LockScreenSceneViewModel
+@AssistedInject
+constructor(
+    @Application applicationScope: CoroutineScope,
+    interactorFactory: LockScreenSceneInteractor.Factory,
+    @Assisted containerName: String,
+) {
+    private val interactor: LockScreenSceneInteractor = interactorFactory.create(containerName)
+
+    /** The icon for the "lock" button on the lock screen. */
+    val lockButtonIcon: StateFlow<Icon> =
+        interactor.isDeviceLocked
+            .map { isLocked -> lockIcon(isLocked = isLocked) }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = lockIcon(isLocked = interactor.isDeviceLocked.value),
+            )
+
+    /** The key of the scene we should switch to when swiping up. */
+    val upDestinationSceneKey: StateFlow<SceneKey> =
+        interactor.isSwipeToDismissEnabled
+            .map { isSwipeToUnlockEnabled -> upDestinationSceneKey(isSwipeToUnlockEnabled) }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = upDestinationSceneKey(interactor.isSwipeToDismissEnabled.value),
+            )
+
+    /** Notifies that the lock button on the lock screen was clicked. */
+    fun onLockButtonClicked() {
+        interactor.dismissLockScreen()
+    }
+
+    /** Notifies that some content on the lock screen was clicked. */
+    fun onContentClicked() {
+        interactor.dismissLockScreen()
+    }
+
+    private fun upDestinationSceneKey(
+        isSwipeToUnlockEnabled: Boolean,
+    ): SceneKey {
+        return if (isSwipeToUnlockEnabled) SceneKey.Gone else SceneKey.Bouncer
+    }
+
+    private fun lockIcon(
+        isLocked: Boolean,
+    ): Icon {
+        return Icon.Resource(
+            res =
+                if (isLocked) {
+                    R.drawable.ic_device_lock_on
+                } else {
+                    R.drawable.ic_device_lock_off
+                },
+            contentDescription =
+                ContentDescription.Resource(
+                    res =
+                        if (isLocked) {
+                            R.string.accessibility_lock_icon
+                        } else {
+                            R.string.accessibility_unlock_button
+                        }
+                )
+        )
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            containerName: String,
+        ): LockScreenSceneViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
new file mode 100644
index 0000000..6525a98
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.qs.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** Models UI state and handles user input for the quick settings scene. */
+class QuickSettingsSceneViewModel
+@AssistedInject
+constructor(
+    lockScreenSceneInteractorFactory: LockScreenSceneInteractor.Factory,
+    @Assisted containerName: String,
+) {
+    private val lockScreenSceneInteractor: LockScreenSceneInteractor =
+        lockScreenSceneInteractorFactory.create(containerName)
+
+    /** Notifies that some content in quick settings was clicked. */
+    fun onContentClicked() {
+        lockScreenSceneInteractor.dismissLockScreen()
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            containerName: String,
+        ): QuickSettingsSceneViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
index dc3c820..6912114 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
@@ -16,12 +16,16 @@
 
 package com.android.systemui.reardisplay;
 
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.TestApi;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.devicestate.DeviceStateManagerGlobal;
 import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.LinearLayout;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.CoreStartable;
@@ -70,6 +74,7 @@
 
     @VisibleForTesting
     SystemUIDialog mRearDisplayEducationDialog;
+    @Nullable LinearLayout mDialogViewContainer;
 
     @Inject
     public RearDisplayDialogController(Context context, CommandQueue commandQueue,
@@ -90,26 +95,51 @@
         createAndShowDialog();
     }
 
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        if (mRearDisplayEducationDialog != null && mRearDisplayEducationDialog.isShowing()
+                && mDialogViewContainer != null) {
+            // Refresh the dialog view when configuration is changed.
+            Context dialogContext = mRearDisplayEducationDialog.getContext();
+            View dialogView = createDialogView(dialogContext);
+            mDialogViewContainer.removeAllViews();
+            mDialogViewContainer.addView(dialogView);
+        }
+    }
+
     private void createAndShowDialog() {
         mServiceNotified = false;
         Context dialogContext = mRearDisplayEducationDialog.getContext();
 
+        View dialogView = createDialogView(dialogContext);
+
+        mDialogViewContainer = new LinearLayout(dialogContext);
+        mDialogViewContainer.setLayoutParams(
+                new LinearLayout.LayoutParams(
+                        LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+        mDialogViewContainer.setOrientation(LinearLayout.VERTICAL);
+        mDialogViewContainer.addView(dialogView);
+
+        mRearDisplayEducationDialog.setView(mDialogViewContainer);
+
+        configureDialogButtons();
+
+        mRearDisplayEducationDialog.show();
+    }
+
+    private View createDialogView(Context context) {
         View dialogView;
         if (mStartedFolded) {
-            dialogView = View.inflate(dialogContext,
+            dialogView = View.inflate(context,
                     R.layout.activity_rear_display_education, null);
         } else {
-            dialogView = View.inflate(dialogContext,
+            dialogView = View.inflate(context,
                     R.layout.activity_rear_display_education_opened, null);
         }
         LottieAnimationView animationView = dialogView.findViewById(
                 R.id.rear_display_folded_animation);
         animationView.setRepeatCount(mAnimationRepeatCount);
-        mRearDisplayEducationDialog.setView(dialogView);
-
-        configureDialogButtons();
-
-        mRearDisplayEducationDialog.show();
+        return dialogView;
     }
 
     /**
@@ -164,6 +194,7 @@
         mServiceNotified = true;
         mDeviceStateManagerGlobal.unregisterDeviceStateCallback(mDeviceStateManagerCallback);
         mDeviceStateManagerGlobal.onStateRequestOverlayDismissed(shouldCancelRequest);
+        mDialogViewContainer = null;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 46f1210..0c800d4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -333,7 +333,7 @@
                 mLpChanged.preferredMinDisplayRefreshRate = 0;
             }
             Trace.setCounter("display_set_preferred_refresh_rate",
-                    (long) mKeyguardPreferredRefreshRate);
+                    (long) mLpChanged.preferredMaxDisplayRefreshRate);
         } else if (mKeyguardMaxRefreshRate > 0) {
             boolean bypassOnKeyguard = mKeyguardBypassController.getBypassEnabled()
                     && state.statusBarState == StatusBarState.KEYGUARD
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
new file mode 100644
index 0000000..dcae258
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.shade.ui.viewmodel
+
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Models UI state and handles user input for the shade scene. */
+class ShadeSceneViewModel
+@AssistedInject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    lockScreenSceneInteractorFactory: LockScreenSceneInteractor.Factory,
+    @Assisted private val containerName: String,
+) {
+    private val lockScreenInteractor: LockScreenSceneInteractor =
+        lockScreenSceneInteractorFactory.create(containerName)
+
+    /** The key of the scene we should switch to when swiping up. */
+    val upDestinationSceneKey: StateFlow<SceneKey> =
+        lockScreenInteractor.isDeviceLocked
+            .map { isLocked -> upDestinationSceneKey(isLocked = isLocked) }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue =
+                    upDestinationSceneKey(
+                        isLocked = lockScreenInteractor.isDeviceLocked.value,
+                    ),
+            )
+
+    /** Notifies that some content in the shade was clicked. */
+    fun onContentClicked() {
+        lockScreenInteractor.dismissLockScreen()
+    }
+
+    private fun upDestinationSceneKey(
+        isLocked: Boolean,
+    ): SceneKey {
+        return if (isLocked) SceneKey.LockScreen else SceneKey.Gone
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            containerName: String,
+        ): ShadeSceneViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index bd7840d..2d8f371 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -469,13 +469,13 @@
             case MODE_DISMISS_BOUNCER:
                 Trace.beginSection("MODE_DISMISS_BOUNCER");
                 mKeyguardViewController.notifyKeyguardAuthenticated(
-                        false /* strongAuth */);
+                        false /* primaryAuth */);
                 Trace.endSection();
                 break;
             case MODE_UNLOCK_COLLAPSING:
                 Trace.beginSection("MODE_UNLOCK_COLLAPSING");
                 mKeyguardViewController.notifyKeyguardAuthenticated(
-                        false /* strongAuth */);
+                        false /* primaryAuth */);
                 Trace.endSection();
                 break;
             case MODE_SHOW_BOUNCER:
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
new file mode 100644
index 0000000..2e62beb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -0,0 +1,291 @@
+/*
+ * 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.authentication.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepository
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class AuthenticationInteractorTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val repository: AuthenticationRepository = AuthenticationRepositoryImpl()
+    private val underTest =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = repository,
+        )
+
+    @Test
+    fun authMethod() =
+        testScope.runTest {
+            val authMethod by collectLastValue(underTest.authenticationMethod)
+            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.PIN(1234))
+
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password("password"))
+        }
+
+    @Test
+    fun isUnlocked_whenAuthMethodIsNone_isTrue() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun unlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+
+            underTest.unlockDevice()
+            runCurrent()
+
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun biometricUnlock() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+
+            underTest.biometricUnlock()
+            runCurrent()
+
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun toggleBypassEnabled() =
+        testScope.runTest {
+            val isBypassEnabled by collectLastValue(underTest.isBypassEnabled)
+            assertThat(isBypassEnabled).isFalse()
+
+            underTest.toggleBypassEnabled()
+            assertThat(isBypassEnabled).isTrue()
+
+            underTest.toggleBypassEnabled()
+            assertThat(isBypassEnabled).isFalse()
+        }
+
+    @Test
+    fun isAuthenticationRequired_lockedAndSecured_true() =
+        testScope.runTest {
+            underTest.lockDevice()
+            runCurrent()
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+
+            assertThat(underTest.isAuthenticationRequired()).isTrue()
+        }
+
+    @Test
+    fun isAuthenticationRequired_lockedAndNotSecured_false() =
+        testScope.runTest {
+            underTest.lockDevice()
+            runCurrent()
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+
+            assertThat(underTest.isAuthenticationRequired()).isFalse()
+        }
+
+    @Test
+    fun isAuthenticationRequired_unlockedAndSecured_false() =
+        testScope.runTest {
+            underTest.unlockDevice()
+            runCurrent()
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+
+            assertThat(underTest.isAuthenticationRequired()).isFalse()
+        }
+
+    @Test
+    fun isAuthenticationRequired_unlockedAndNotSecured_false() =
+        testScope.runTest {
+            underTest.unlockDevice()
+            runCurrent()
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+
+            assertThat(underTest.isAuthenticationRequired()).isFalse()
+        }
+
+    @Test
+    fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(underTest.authenticate("password".toList())).isTrue()
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(underTest.authenticate("alohomora".toList())).isFalse()
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(
+                AuthenticationMethodModel.Pattern(
+                    listOf(
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 0,
+                        ),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 1,
+                        ),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 2,
+                        ),
+                    )
+                )
+            )
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(
+                    underTest.authenticate(
+                        listOf(
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 0,
+                                y = 0,
+                            ),
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 0,
+                                y = 1,
+                            ),
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 0,
+                                y = 2,
+                            ),
+                        )
+                    )
+                )
+                .isTrue()
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(
+                AuthenticationMethodModel.Pattern(
+                    listOf(
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 0,
+                        ),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 1,
+                        ),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 2,
+                        ),
+                    )
+                )
+            )
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(
+                    underTest.authenticate(
+                        listOf(
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 2,
+                                y = 0,
+                            ),
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 2,
+                                y = 1,
+                            ),
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 2,
+                                y = 2,
+                            ),
+                        )
+                    )
+                )
+                .isFalse()
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun unlocksDevice_whenAuthMethodBecomesNone() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+
+            repository.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+            assertThat(isUnlocked).isTrue()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java
index b8bca3a..38c9caf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java
@@ -29,7 +29,6 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.hardware.biometrics.BiometricFaceConstants;
-import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.BiometricSourceType;
 import android.os.Handler;
 import android.testing.AndroidTestingRunner;
@@ -51,6 +50,8 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.util.Optional;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -64,11 +65,16 @@
     KeyguardStateController mKeyguardStateController;
     @Mock
     NotificationManager mNotificationManager;
+    @Mock
+    Optional<FingerprintReEnrollNotification> mFingerprintReEnrollNotificationOptional;
+    @Mock
+    FingerprintReEnrollNotification mFingerprintReEnrollNotification;
 
     private static final String TAG = "BiometricNotificationService";
     private static final int FACE_NOTIFICATION_ID = 1;
     private static final int FINGERPRINT_NOTIFICATION_ID = 2;
     private static final long SHOW_NOTIFICATION_DELAY_MS = 5_000L; // 5 seconds
+    private static final int FINGERPRINT_ACQUIRED_RE_ENROLL = 0;
 
     private final ArgumentCaptor<Notification> mNotificationArgumentCaptor =
             ArgumentCaptor.forClass(Notification.class);
@@ -78,6 +84,11 @@
 
     @Before
     public void setUp() {
+        when(mFingerprintReEnrollNotificationOptional.orElse(any()))
+                .thenReturn(mFingerprintReEnrollNotification);
+        when(mFingerprintReEnrollNotification.isFingerprintReEnrollRequired(
+                FINGERPRINT_ACQUIRED_RE_ENROLL)).thenReturn(true);
+
         mLooper = TestableLooper.get(this);
         Handler handler = new Handler(mLooper.getLooper());
         BiometricNotificationDialogFactory dialogFactory = new BiometricNotificationDialogFactory();
@@ -87,7 +98,8 @@
                 new BiometricNotificationService(mContext,
                         mKeyguardUpdateMonitor, mKeyguardStateController, handler,
                         mNotificationManager,
-                        broadcastReceiver);
+                        broadcastReceiver,
+                        mFingerprintReEnrollNotificationOptional);
         biometricNotificationService.start();
 
         ArgumentCaptor<KeyguardUpdateMonitorCallback> updateMonitorCallbackArgumentCaptor =
@@ -108,8 +120,8 @@
     public void testShowFingerprintReEnrollNotification() {
         when(mKeyguardStateController.isShowing()).thenReturn(false);
 
-        mKeyguardUpdateMonitorCallback.onBiometricError(
-                BiometricFingerprintConstants.BIOMETRIC_ERROR_RE_ENROLL,
+        mKeyguardUpdateMonitorCallback.onBiometricHelp(
+                FINGERPRINT_ACQUIRED_RE_ENROLL,
                 "Testing Fingerprint Re-enrollment" /* errString */,
                 BiometricSourceType.FINGERPRINT
         );
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt
index 4f89b69..da55d5a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt
@@ -33,7 +33,7 @@
     @Test
     fun isGoodOverlap() {
         val touchData = TOUCH_DATA.copy(x = testCase.x.toFloat(), y = testCase.y.toFloat())
-        val actual = underTest.isGoodOverlap(touchData, SENSOR)
+        val actual = underTest.isGoodOverlap(touchData, SENSOR, OVERLAY)
 
         assertThat(actual).isEqualTo(testCase.expected)
     }
@@ -50,8 +50,10 @@
                         validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY())
                     ),
                     genNegativeTestCases(
-                        invalidXs = listOf(SENSOR.left - 1, SENSOR.right + 1),
-                        invalidYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1),
+                        invalidXs =
+                            listOf(SENSOR.left - 1, SENSOR.right + 1, OVERLAY.left, OVERLAY.right),
+                        invalidYs =
+                            listOf(SENSOR.top - 1, SENSOR.bottom + 1, OVERLAY.top, OVERLAY.bottom),
                         validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()),
                         validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY())
                     )
@@ -82,6 +84,7 @@
     )
 
 private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 500 /* bottom */)
+private val OVERLAY = Rect(0 /* left */, 100 /* top */, 400 /* right */, 600 /* bottom */)
 
 private fun genTestCases(
     xs: List<Int>,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
index fb3c185..317141b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
@@ -43,7 +43,7 @@
                 minor = testCase.minor,
                 major = testCase.major
             )
-        val actual = underTest.isGoodOverlap(touchData, SENSOR)
+        val actual = underTest.isGoodOverlap(touchData, SENSOR, OVERLAY)
 
         assertThat(actual).isEqualTo(testCase.expected)
     }
@@ -71,8 +71,10 @@
                         expected = true
                     ),
                     genNegativeTestCase(
-                        outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1),
-                        outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1),
+                        outerXs =
+                            listOf(SENSOR.left - 1, SENSOR.right + 1, OVERLAY.left, OVERLAY.right),
+                        outerYs =
+                            listOf(SENSOR.top - 1, SENSOR.bottom + 1, OVERLAY.top, OVERLAY.bottom),
                         minor = 100f,
                         major = 100f,
                         expected = false
@@ -104,6 +106,7 @@
     )
 
 private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 400 /* bottom */)
+private val OVERLAY = Rect(0 /* left */, 100 /* top */, 400 /* right */, 600 /* bottom */)
 
 private fun genPositiveTestCases(
     innerXs: List<Int>,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt
index 834d0a6..3e5c43a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt
@@ -16,7 +16,7 @@
     @Test
     fun isWithinSensor() {
         val touchData = TOUCH_DATA.copy(x = testCase.x.toFloat(), y = testCase.y.toFloat())
-        val actual = touchData.isWithinSensor(SENSOR)
+        val actual = touchData.isWithinBounds(SENSOR)
 
         assertThat(actual).isEqualTo(testCase.expected)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
new file mode 100644
index 0000000..7dd376e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -0,0 +1,223 @@
+/*
+ * 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.bouncer.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class BouncerInteractorTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val authenticationInteractor =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = AuthenticationRepositoryImpl(),
+        )
+    private val sceneInteractor =
+        SceneInteractor(
+            repository = fakeSceneContainerRepository(),
+        )
+    private val underTest =
+        BouncerInteractor(
+            applicationScope = testScope.backgroundScope,
+            applicationContext = context,
+            repository = BouncerRepository(),
+            authenticationInteractor = authenticationInteractor,
+            sceneInteractor = sceneInteractor,
+            containerName = "container1",
+        )
+
+    @Before
+    fun setUp() {
+        overrideResource(R.string.keyguard_enter_your_pin, MESSAGE_ENTER_YOUR_PIN)
+        overrideResource(R.string.keyguard_enter_your_password, MESSAGE_ENTER_YOUR_PASSWORD)
+        overrideResource(R.string.keyguard_enter_your_pattern, MESSAGE_ENTER_YOUR_PATTERN)
+        overrideResource(R.string.kg_wrong_pin, MESSAGE_WRONG_PIN)
+        overrideResource(R.string.kg_wrong_password, MESSAGE_WRONG_PASSWORD)
+        overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN)
+    }
+
+    @Test
+    fun pinAuthMethod() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            val message by collectLastValue(underTest.message)
+
+            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            authenticationInteractor.lockDevice()
+            underTest.showOrUnlockDevice("container1")
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
+
+            underTest.clearMessage()
+            assertThat(message).isNull()
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
+
+            // Wrong input.
+            underTest.authenticate(listOf(9, 8, 7))
+            assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
+
+            // Correct input.
+            underTest.authenticate(listOf(1, 2, 3, 4))
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun passwordAuthMethod() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            val message by collectLastValue(underTest.message)
+            authenticationInteractor.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+            authenticationInteractor.lockDevice()
+            underTest.showOrUnlockDevice("container1")
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
+
+            underTest.clearMessage()
+            assertThat(message).isNull()
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
+
+            // Wrong input.
+            underTest.authenticate("alohamora".toList())
+            assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
+
+            // Correct input.
+            underTest.authenticate("password".toList())
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun patternAuthMethod() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            val message by collectLastValue(underTest.message)
+            authenticationInteractor.setAuthenticationMethod(
+                AuthenticationMethodModel.Pattern(emptyList())
+            )
+            authenticationInteractor.lockDevice()
+            underTest.showOrUnlockDevice("container1")
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
+
+            underTest.clearMessage()
+            assertThat(message).isNull()
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
+
+            // Wrong input.
+            underTest.authenticate(
+                listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(3, 4))
+            )
+            assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
+
+            // Correct input.
+            underTest.authenticate(emptyList())
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun showOrUnlockDevice_notLocked_switchesToGoneScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            authenticationInteractor.unlockDevice()
+            runCurrent()
+
+            underTest.showOrUnlockDevice("container1")
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun showOrUnlockDevice_authMethodNotSecure_switchesToGoneScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            authenticationInteractor.lockDevice()
+
+            underTest.showOrUnlockDevice("container1")
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun showOrUnlockDevice_customMessageShown() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            val message by collectLastValue(underTest.message)
+            authenticationInteractor.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+            authenticationInteractor.lockDevice()
+
+            val customMessage = "Hello there!"
+            underTest.showOrUnlockDevice("container1", customMessage)
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(message).isEqualTo(customMessage)
+        }
+
+    companion object {
+        private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
+        private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
+        private const val MESSAGE_ENTER_YOUR_PATTERN = "Enter your pattern"
+        private const val MESSAGE_WRONG_PIN = "Wrong PIN"
+        private const val MESSAGE_WRONG_PASSWORD = "Wrong password"
+        private const val MESSAGE_WRONG_PATTERN = "Wrong pattern"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 83c89f1..489dc4d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -519,6 +519,12 @@
     }
 
     @Test
+    public void testWakeAndUnlocking() {
+        mViewMediator.onWakeAndUnlocking();
+        verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean());
+    }
+
+    @Test
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
     public void testDoKeyguardWhileInteractive_resets() {
         mViewMediator.setShowingLocked(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt
new file mode 100644
index 0000000..749e7a0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt
@@ -0,0 +1,270 @@
+/*
+ * 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 androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class LockScreenSceneInteractorTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val sceneInteractor =
+        SceneInteractor(
+            repository = fakeSceneContainerRepository(),
+        )
+    private val mAuthenticationInteractor =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = AuthenticationRepositoryImpl(),
+        )
+    private val underTest =
+        LockScreenSceneInteractor(
+            applicationScope = testScope.backgroundScope,
+            authenticationInteractor = mAuthenticationInteractor,
+            bouncerInteractorFactory =
+                object : BouncerInteractor.Factory {
+                    override fun create(containerName: String): BouncerInteractor {
+                        return BouncerInteractor(
+                            applicationScope = testScope.backgroundScope,
+                            applicationContext = context,
+                            repository = BouncerRepository(),
+                            authenticationInteractor = mAuthenticationInteractor,
+                            sceneInteractor = sceneInteractor,
+                            containerName = containerName,
+                        )
+                    }
+                },
+            sceneInteractor = sceneInteractor,
+            containerName = CONTAINER_NAME,
+        )
+
+    @Test
+    fun isDeviceLocked() =
+        testScope.runTest {
+            val isDeviceLocked by collectLastValue(underTest.isDeviceLocked)
+
+            mAuthenticationInteractor.lockDevice()
+            assertThat(isDeviceLocked).isTrue()
+
+            mAuthenticationInteractor.unlockDevice()
+            assertThat(isDeviceLocked).isFalse()
+        }
+
+    @Test
+    fun isSwipeToDismissEnabled_deviceLockedAndAuthMethodSwipe_true() =
+        testScope.runTest {
+            val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
+
+            mAuthenticationInteractor.lockDevice()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+
+            assertThat(isSwipeToDismissEnabled).isTrue()
+        }
+
+    @Test
+    fun isSwipeToDismissEnabled_deviceUnlockedAndAuthMethodSwipe_false() =
+        testScope.runTest {
+            val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
+
+            mAuthenticationInteractor.unlockDevice()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+
+            assertThat(isSwipeToDismissEnabled).isFalse()
+        }
+
+    @Test
+    fun dismissLockScreen_deviceLockedWithSecureAuthMethod_switchesToBouncer() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.lockDevice()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            underTest.dismissLockScreen()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    @Test
+    fun dismissLockScreen_deviceUnlocked_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.unlockDevice()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            underTest.dismissLockScreen()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun dismissLockScreen_deviceLockedWithInsecureAuthMethod_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.lockDevice()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            underTest.dismissLockScreen()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun deviceLockedInNonLockScreenScene_switchesToLockScreenScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            runCurrent()
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
+            runCurrent()
+            mAuthenticationInteractor.unlockDevice()
+            runCurrent()
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+
+            mAuthenticationInteractor.lockDevice()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+        }
+
+    @Test
+    fun deviceBiometricUnlockedInLockScreen_bypassEnabled_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.lockDevice()
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
+            if (!mAuthenticationInteractor.isBypassEnabled.value) {
+                mAuthenticationInteractor.toggleBypassEnabled()
+            }
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            mAuthenticationInteractor.biometricUnlock()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun deviceBiometricUnlockedInLockScreen_bypassNotEnabled_doesNotSwitch() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.lockDevice()
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
+            if (mAuthenticationInteractor.isBypassEnabled.value) {
+                mAuthenticationInteractor.toggleBypassEnabled()
+            }
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            mAuthenticationInteractor.biometricUnlock()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+        }
+
+    @Test
+    fun switchFromLockScreenToGone_authMethodSwipe_unlocksDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            assertThat(isUnlocked).isFalse()
+
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
+
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(isUnlocked).isFalse()
+
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
+
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun switchFromNonLockScreenToGone_authMethodSwipe_doesNotUnlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
+            runCurrent()
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Shade))
+            runCurrent()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            runCurrent()
+            assertThat(isUnlocked).isFalse()
+
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
+
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun authMethodChangedToNone_onLockScreenScene_dismissesLockScreen() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun authMethodChangedToNone_notOnLockScreenScene_doesNotDismissLockScreen() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            runCurrent()
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.QuickSettings))
+            runCurrent()
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
+
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
+        }
+
+    companion object {
+        private const val CONTAINER_NAME = "container1"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt
new file mode 100644
index 0000000..d335b09
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt
@@ -0,0 +1,190 @@
+/*
+ * 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.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class LockScreenSceneViewModelTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val sceneInteractor =
+        SceneInteractor(
+            repository = fakeSceneContainerRepository(),
+        )
+    private val mAuthenticationInteractor =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = AuthenticationRepositoryImpl(),
+        )
+
+    private val underTest =
+        LockScreenSceneViewModel(
+            applicationScope = testScope.backgroundScope,
+            interactorFactory =
+                object : LockScreenSceneInteractor.Factory {
+                    override fun create(containerName: String): LockScreenSceneInteractor {
+                        return LockScreenSceneInteractor(
+                            applicationScope = testScope.backgroundScope,
+                            authenticationInteractor = mAuthenticationInteractor,
+                            bouncerInteractorFactory =
+                                object : BouncerInteractor.Factory {
+                                    override fun create(containerName: String): BouncerInteractor {
+                                        return BouncerInteractor(
+                                            applicationScope = testScope.backgroundScope,
+                                            applicationContext = context,
+                                            repository = BouncerRepository(),
+                                            authenticationInteractor = mAuthenticationInteractor,
+                                            sceneInteractor = sceneInteractor,
+                                            containerName = containerName,
+                                        )
+                                    }
+                                },
+                            sceneInteractor = sceneInteractor,
+                            containerName = CONTAINER_NAME,
+                        )
+                    }
+                },
+            containerName = CONTAINER_NAME
+        )
+
+    @Test
+    fun lockButtonIcon_whenLocked() =
+        testScope.runTest {
+            val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
+            mAuthenticationInteractor.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+            mAuthenticationInteractor.lockDevice()
+
+            assertThat((lockButtonIcon as? Icon.Resource)?.res)
+                .isEqualTo(R.drawable.ic_device_lock_on)
+        }
+
+    @Test
+    fun lockButtonIcon_whenUnlocked() =
+        testScope.runTest {
+            val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
+            mAuthenticationInteractor.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+            mAuthenticationInteractor.unlockDevice()
+
+            assertThat((lockButtonIcon as? Icon.Resource)?.res)
+                .isEqualTo(R.drawable.ic_device_lock_off)
+        }
+
+    @Test
+    fun upTransitionSceneKey_swipeToUnlockedEnabled_gone() =
+        testScope.runTest {
+            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            mAuthenticationInteractor.lockDevice()
+
+            assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
+        }
+
+    @Test
+    fun upTransitionSceneKey_swipeToUnlockedNotEnabled_bouncer() =
+        testScope.runTest {
+            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+
+            assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer)
+        }
+
+    @Test
+    fun onLockButtonClicked_deviceLockedSecurely_switchesToBouncer() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+            runCurrent()
+
+            underTest.onLockButtonClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    @Test
+    fun onContentClicked_deviceUnlocked_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.unlockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    @Test
+    fun onLockButtonClicked_deviceUnlocked_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.unlockDevice()
+            runCurrent()
+
+            underTest.onLockButtonClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    companion object {
+        private const val CONTAINER_NAME = "container1"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
new file mode 100644
index 0000000..e8875be
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.qs.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class QuickSettingsSceneViewModelTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val sceneInteractor =
+        SceneInteractor(
+            repository = fakeSceneContainerRepository(),
+        )
+    private val mAuthenticationInteractor =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = AuthenticationRepositoryImpl(),
+        )
+
+    private val underTest =
+        QuickSettingsSceneViewModel(
+            lockScreenSceneInteractorFactory =
+                object : LockScreenSceneInteractor.Factory {
+                    override fun create(containerName: String): LockScreenSceneInteractor {
+                        return LockScreenSceneInteractor(
+                            applicationScope = testScope.backgroundScope,
+                            authenticationInteractor = mAuthenticationInteractor,
+                            bouncerInteractorFactory =
+                                object : BouncerInteractor.Factory {
+                                    override fun create(containerName: String): BouncerInteractor {
+                                        return BouncerInteractor(
+                                            applicationScope = testScope.backgroundScope,
+                                            applicationContext = context,
+                                            repository = BouncerRepository(),
+                                            authenticationInteractor = mAuthenticationInteractor,
+                                            sceneInteractor = sceneInteractor,
+                                            containerName = containerName,
+                                        )
+                                    }
+                                },
+                            sceneInteractor = sceneInteractor,
+                            containerName = CONTAINER_NAME,
+                        )
+                    }
+                },
+            containerName = CONTAINER_NAME
+        )
+
+    @Test
+    fun onContentClicked_deviceUnlocked_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.unlockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    companion object {
+        private const val CONTAINER_NAME = "container1"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
index 9acd47e..55813f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
@@ -17,8 +17,10 @@
 package com.android.systemui.reardisplay;
 
 import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotSame;
 import static junit.framework.Assert.assertTrue;
 
+import android.content.res.Configuration;
 import android.hardware.devicestate.DeviceStateManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -68,6 +70,27 @@
     }
 
     @Test
+    public void testClosedDialogIsRefreshedOnConfigurationChange() {
+        RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
+                mCommandQueue, mFakeExecutor);
+        controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
+        controller.setFoldedStates(new int[]{0});
+        controller.setAnimationRepeatCount(0);
+
+        controller.showRearDisplayDialog(CLOSED_BASE_STATE);
+        assertTrue(controller.mRearDisplayEducationDialog.isShowing());
+        TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+                R.id.rear_display_title_text_view);
+
+        controller.onConfigurationChanged(new Configuration());
+        assertTrue(controller.mRearDisplayEducationDialog.isShowing());
+        TextView deviceClosedTitleTextView2 = controller.mRearDisplayEducationDialog.findViewById(
+                R.id.rear_display_title_text_view);
+
+        assertNotSame(deviceClosedTitleTextView, deviceClosedTitleTextView2);
+    }
+
+    @Test
     public void testOpenDialogIsShown() {
         RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
                 mCommandQueue, mFakeExecutor);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
new file mode 100644
index 0000000..688cce8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.shade.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ShadeSceneViewModelTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val sceneInteractor =
+        SceneInteractor(
+            repository = fakeSceneContainerRepository(),
+        )
+    private val mAuthenticationInteractor =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = AuthenticationRepositoryImpl(),
+        )
+
+    private val underTest =
+        ShadeSceneViewModel(
+            applicationScope = testScope.backgroundScope,
+            lockScreenSceneInteractorFactory =
+                object : LockScreenSceneInteractor.Factory {
+                    override fun create(containerName: String): LockScreenSceneInteractor {
+                        return LockScreenSceneInteractor(
+                            applicationScope = testScope.backgroundScope,
+                            authenticationInteractor = mAuthenticationInteractor,
+                            bouncerInteractorFactory =
+                                object : BouncerInteractor.Factory {
+                                    override fun create(containerName: String): BouncerInteractor {
+                                        return BouncerInteractor(
+                                            applicationScope = testScope.backgroundScope,
+                                            applicationContext = context,
+                                            repository = BouncerRepository(),
+                                            authenticationInteractor = mAuthenticationInteractor,
+                                            sceneInteractor = sceneInteractor,
+                                            containerName = containerName,
+                                        )
+                                    }
+                                },
+                            sceneInteractor = sceneInteractor,
+                            containerName = CONTAINER_NAME,
+                        )
+                    }
+                },
+            containerName = CONTAINER_NAME
+        )
+
+    @Test
+    fun upTransitionSceneKey_deviceLocked_lockScreen() =
+        testScope.runTest {
+            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+
+            assertThat(upTransitionSceneKey).isEqualTo(SceneKey.LockScreen)
+        }
+
+    @Test
+    fun upTransitionSceneKey_deviceUnlocked_gone() =
+        testScope.runTest {
+            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.unlockDevice()
+
+            assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
+        }
+
+    @Test
+    fun onContentClicked_deviceUnlocked_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.unlockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    companion object {
+        private const val CONTAINER_NAME = "container1"
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt
index 1bdee36..e3e7933 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt
@@ -21,7 +21,11 @@
 class FakeOverlapDetector : OverlapDetector {
     var shouldReturn: Map<Int, Boolean> = mapOf()
 
-    override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean {
+    override fun isGoodOverlap(
+        touchData: NormalizedTouchData,
+        nativeSensorBounds: Rect,
+        nativeOverlayBounds: Rect
+    ): Boolean {
         return shouldReturn[touchData.pointerId]
             ?: error("Unexpected PointerId not declared in TestCase currentPointers")
     }
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index af6b24e..2d60716 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -558,10 +558,14 @@
         try {
             final Intent onClickIntent;
 
-            if (provider.maskedBySuspendedPackage) {
+            if (provider.maskedByQuietProfile) {
+                showBadge = true;
+                onClickIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(appUserId);
+            } else if (provider.maskedBySuspendedPackage) {
                 showBadge = mUserManager.hasBadge(appUserId);
                 final String suspendingPackage = mPackageManagerInternal.getSuspendingPackage(
                         appInfo.packageName, appUserId);
+                // TODO(b/281839596): don't rely on platform always meaning suspended by admin.
                 if (PLATFORM_PACKAGE_NAME.equals(suspendingPackage)) {
                     onClickIntent = mDevicePolicyManagerInternal.createShowAdminSupportIntent(
                             appUserId, true);
@@ -575,9 +579,6 @@
                             appInfo.packageName, suspendingPackage, dialogInfo, null, null,
                             appUserId);
                 }
-            } else if (provider.maskedByQuietProfile) {
-                showBadge = true;
-                onClickIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(appUserId);
             } else /* provider.maskedByLockedProfile */ {
                 showBadge = true;
                 onClickIntent = mKeyguardManager
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
index fb978b2..b474cad 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -555,7 +555,7 @@
         for (BiometricSchedulerOperation pendingOperation : mPendingOperations) {
             Slog.d(getTag(), "[Watchdog cancelling pending] "
                     + pendingOperation.getClientMonitor());
-            pendingOperation.markCanceling();
+            pendingOperation.markCancelingForWatchdog();
         }
         Slog.d(getTag(), "[Watchdog cancelling current] "
                 + mCurrentOperation.getClientMonitor());
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
index 4825f1d..57feedc 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
@@ -267,13 +267,17 @@
 
     /** Flags this operation as canceled, if possible, but does not cancel it until started. */
     public boolean markCanceling() {
-        if (mState == STATE_WAITING_IN_QUEUE) {
+        if (mState == STATE_WAITING_IN_QUEUE && isInterruptable()) {
             mState = STATE_WAITING_IN_QUEUE_CANCELING;
             return true;
         }
         return false;
     }
 
+    @VisibleForTesting void markCancelingForWatchdog() {
+        mState = STATE_WAITING_IN_QUEUE_CANCELING;
+    }
+
     /**
      * Cancel the operation now.
      *
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 02b7053..47f6485 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -33,6 +33,8 @@
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -74,6 +76,7 @@
 import android.view.KeyEvent;
 
 import com.android.server.LocalServices;
+import com.android.server.uri.UriGrantsManagerInternal;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -101,6 +104,10 @@
     static final long THROW_FOR_INVALID_BROADCAST_RECEIVER = 270049379L;
 
     private static final String TAG = "MediaSessionRecord";
+    private static final String[] ART_URIS = new String[] {
+            MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
+            MediaMetadata.METADATA_KEY_ART_URI,
+            MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI};
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     /**
@@ -154,6 +161,7 @@
     private final SessionStub mSession;
     private final SessionCb mSessionCb;
     private final MediaSessionService mService;
+    private final UriGrantsManagerInternal mUgmInternal;
     private final Context mContext;
     private final boolean mVolumeAdjustmentForRemoteGroupSessions;
 
@@ -215,6 +223,7 @@
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
         mAudioAttrs = DEFAULT_ATTRIBUTES;
         mPolicies = policies;
+        mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class);
         mVolumeAdjustmentForRemoteGroupSessions = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
 
@@ -1080,21 +1089,45 @@
         public void setMetadata(MediaMetadata metadata, long duration, String metadataDescription)
                 throws RemoteException {
             synchronized (mLock) {
-                MediaMetadata temp = metadata == null ? null : new MediaMetadata.Builder(metadata)
-                        .build();
-                // This is to guarantee that the underlying bundle is unparceled
-                // before we set it to prevent concurrent reads from throwing an
-                // exception
-                if (temp != null) {
-                    temp.size();
-                }
-                mMetadata = temp;
                 mDuration = duration;
                 mMetadataDescription = metadataDescription;
+                mMetadata = sanitizeMediaMetadata(metadata);
             }
             mHandler.post(MessageHandler.MSG_UPDATE_METADATA);
         }
 
+        private MediaMetadata sanitizeMediaMetadata(MediaMetadata metadata) {
+            if (metadata == null) {
+                return null;
+            }
+            MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder(metadata);
+            for (String key: ART_URIS) {
+                String uriString = metadata.getString(key);
+                if (TextUtils.isEmpty(uriString)) {
+                    continue;
+                }
+                Uri uri = Uri.parse(uriString);
+                if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
+                    continue;
+                }
+                try {
+                    mUgmInternal.checkGrantUriPermission(getUid(),
+                            getPackageName(),
+                            ContentProvider.getUriWithoutUserId(uri),
+                            Intent.FLAG_GRANT_READ_URI_PERMISSION,
+                            ContentProvider.getUserIdFromUri(uri, getUserId()));
+                } catch (SecurityException e) {
+                    metadataBuilder.putString(key, null);
+                }
+            }
+            MediaMetadata sanitizedMetadata = metadataBuilder.build();
+            // sanitizedMetadata.size() guarantees that the underlying bundle is unparceled
+            // before we set it to prevent concurrent reads from throwing an
+            // exception
+            sanitizedMetadata.size();
+            return sanitizedMetadata;
+        }
+
         @Override
         public void setPlaybackState(PlaybackState state) throws RemoteException {
             int oldState = mPlaybackState == null
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index a694f5f..ecd5bd2 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -274,13 +274,14 @@
             mHandler = new SensorPrivacyHandler(FgThread.get().getLooper(), mContext);
             mSensorPrivacyStateController = SensorPrivacyStateController.getInstance();
 
+            correctStateIfNeeded();
+
             int[] micAndCameraOps = new int[]{OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE,
                     OP_CAMERA, OP_PHONE_CALL_CAMERA, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO};
             mAppOpsManager.startWatchingNoted(micAndCameraOps, this);
             mAppOpsManager.startWatchingStarted(micAndCameraOps, this);
 
 
-
             mContext.registerReceiver(new BroadcastReceiver() {
                 @Override
                 public void onReceive(Context context, Intent intent) {
@@ -313,6 +314,20 @@
                             userId, toggleType, sensor, state.isEnabled()));
         }
 
+        // If sensor privacy is enabled for a sensor, but the device doesn't support sensor privacy
+        // for that sensor, then disable privacy
+        private void correctStateIfNeeded() {
+            mSensorPrivacyStateController.forEachState((type, user, sensor, state) -> {
+                if (type != TOGGLE_TYPE_SOFTWARE) {
+                    return;
+                }
+                if (!supportsSensorToggle(TOGGLE_TYPE_SOFTWARE, sensor) && state.isEnabled()) {
+                    setToggleSensorPrivacyUnchecked(
+                            TOGGLE_TYPE_SOFTWARE, user, OTHER, sensor, false);
+                }
+            });
+        }
+
         @Override
         public void onUserRestrictionsChanged(int userId, Bundle newRestrictions,
                 Bundle prevRestrictions) {
@@ -721,15 +736,30 @@
             if (userId == UserHandle.USER_CURRENT) {
                 userId = mCurrentUser;
             }
+
             if (!canChangeToggleSensorPrivacy(userId, sensor)) {
                 return;
             }
+            if (enable && !supportsSensorToggle(TOGGLE_TYPE_SOFTWARE, sensor)) {
+                // Do not enable sensor privacy if the device doesn't support it
+                return;
+            }
 
             setToggleSensorPrivacyUnchecked(TOGGLE_TYPE_SOFTWARE, userId, source, sensor, enable);
         }
 
         private void setToggleSensorPrivacyUnchecked(int toggleType, int userId, int source,
                 int sensor, boolean enable) {
+            if (DEBUG) {
+                Log.d(TAG, "callingUid=" + Binder.getCallingUid()
+                        + " callingPid=" + Binder.getCallingPid()
+                        + " setToggleSensorPrivacyUnchecked("
+                        + "userId=" + userId
+                        + " source=" + source
+                        + " sensor=" + sensor
+                        + " enable=" + enable
+                        + ")");
+            }
             final long[] lastChange = new long[1];
             mSensorPrivacyStateController.atomic(() -> {
                 SensorState sensorState = mSensorPrivacyStateController
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index 7926216..9f738ed 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -585,6 +585,11 @@
     public abstract void setDeviceOwnerUid(int uid);
 
     /**
+     * Called by DevicePolicyManagerService to set the uids of the profile owners.
+     */
+    public abstract void setProfileOwnerUids(Set<Integer> uids);
+
+    /**
      * Set all associated companion app that belongs to a userId.
      * @param userId
      * @param companionAppUids ActivityTaskManager will take ownership of this Set, the caller
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index f35343c..48569f6 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -158,6 +158,7 @@
 import android.app.ProfilerInfo;
 import android.app.WaitResult;
 import android.app.admin.DevicePolicyCache;
+import android.app.admin.DeviceStateCache;
 import android.app.assist.ActivityId;
 import android.app.assist.AssistContent;
 import android.app.assist.AssistStructure;
@@ -783,6 +784,8 @@
 
     private int mDeviceOwnerUid = Process.INVALID_UID;
 
+    private Set<Integer> mProfileOwnerUids = new ArraySet<Integer>();
+
     private final class SettingObserver extends ContentObserver {
         private final Uri mFontScaleUri = Settings.System.getUriFor(FONT_SCALE);
         private final Uri mHideErrorDialogsUri = Settings.Global.getUriFor(HIDE_ERROR_DIALOGS);
@@ -5360,6 +5363,15 @@
         mDeviceOwnerUid = uid;
     }
 
+    boolean isAffiliatedProfileOwner(int uid) {
+        return uid >= 0 && mProfileOwnerUids.contains(uid)
+            && DeviceStateCache.getInstance().hasAffiliationWithDevice(UserHandle.getUserId(uid));
+    }
+
+    void setProfileOwnerUids(Set<Integer> uids) {
+        mProfileOwnerUids = uids;
+    }
+
     /**
      * Saves the current activity manager state and includes the saved state in the next dump of
      * activity manager.
@@ -6916,6 +6928,13 @@
         }
 
         @Override
+        public void setProfileOwnerUids(Set<Integer> uids) {
+            synchronized (mGlobalLock) {
+                ActivityTaskManagerService.this.setProfileOwnerUids(uids);
+            }
+        }
+
+        @Override
         public void setCompanionAppUids(int userId, Set<Integer> companionAppUids) {
             synchronized (mGlobalLock) {
                 mCompanionAppUidsMap.put(userId, companionAppUids);
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 1a3d673..dc49e8c 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -342,6 +342,14 @@
                     /*background*/ true, callingUid, realCallingUid,
                     intent, "Device Owner");
             }
+            // don't abort if the callingUid is a affiliated profile owner
+            if (mService.isAffiliatedProfileOwner(callingUid)) {
+                return logStartAllowedAndReturnCode(
+                    BAL_ALLOW_ALLOWLISTED_COMPONENT,
+                    resultIfPiSenderAllowsBal, balAllowedByPiSender,
+                    /*background*/ true, callingUid, realCallingUid,
+                    intent, "Affiliated Profile Owner");
+            }
             // don't abort if the callingUid has companion device
             final int callingUserId = UserHandle.getUserId(callingUid);
             if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) {
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index f95ed6a..e820d8a 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -158,7 +158,7 @@
 
     @Override
     public void onUiCancellation(boolean isUserCancellation) {
-        String exception = GetCredentialException.TYPE_NO_CREDENTIAL;
+        String exception = GetCredentialException.TYPE_USER_CANCELED;
         String message = "User cancelled the selector";
         if (!isUserCancellation) {
             exception = GetCredentialException.TYPE_INTERRUPTED;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 187e51a..9d1c77d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -10329,6 +10329,7 @@
         policy.mSecondaryLockscreenEnabled = false;
         policy.mUserProvisioningState = DevicePolicyManager.STATE_USER_UNMANAGED;
         policy.mAffiliationIds.clear();
+        resetAffiliationCacheLocked();
         policy.mLockTaskPackages.clear();
         if (!isPolicyEngineForFinanceFlagEnabled()) {
             updateLockTaskPackagesLocked(mContext, policy.mLockTaskPackages, userId);
@@ -18022,10 +18023,16 @@
         synchronized (getLockObject()) {
             getUserData(callingUserId).mAffiliationIds = affiliationIds;
             saveSettingsLocked(callingUserId);
-            if (callingUserId != UserHandle.USER_SYSTEM && isDeviceOwner(admin, callingUserId)) {
+            mStateCache.setHasAffiliationWithDevice(callingUserId,
+                    isUserAffiliatedWithDeviceLocked(callingUserId));
+            if (callingUserId == UserHandle.USER_SYSTEM) {
+                resetAffiliationCacheLocked();
+            } else if (callingUserId != UserHandle.USER_SYSTEM && isDeviceOwner(admin,
+                    callingUserId)) {
                 // Affiliation ids specified by the device owner are additionally stored in
                 // UserHandle.USER_SYSTEM's DevicePolicyData.
                 getUserData(UserHandle.USER_SYSTEM).mAffiliationIds = affiliationIds;
+                mStateCache.setHasAffiliationWithDevice(UserHandle.USER_SYSTEM, true);
                 saveSettingsLocked(UserHandle.USER_SYSTEM);
             }
 
@@ -18039,6 +18046,15 @@
         }
     }
 
+    private void resetAffiliationCacheLocked() {
+        mInjector.binderWithCleanCallingIdentity(() -> {
+            for (UserInfo user : mUserManager.getUsers()) {
+                mStateCache.setHasAffiliationWithDevice(user.id,
+                        isUserAffiliatedWithDeviceLocked(user.id));
+            }
+        });
+    }
+
     @Override
     public List<String> getAffiliationIds(ComponentName admin) {
         if (!mHasFeature) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java
index 011a282..47607d7 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java
@@ -42,6 +42,8 @@
 
     private AtomicInteger mDeviceOwnerType = new AtomicInteger(NO_DEVICE_OWNER);
     private Map<Integer, Boolean> mHasProfileOwner = new ConcurrentHashMap<>();
+    private Map<Integer, Boolean> mAffiliationWithDevice = new ConcurrentHashMap<>();
+
 
     @GuardedBy("mLock")
     private boolean mIsDeviceProvisioned = false;
@@ -70,6 +72,19 @@
         }
     }
 
+    void setHasAffiliationWithDevice(int userId, Boolean hasAffiliateProfileOwner) {
+        if (hasAffiliateProfileOwner) {
+            mAffiliationWithDevice.put(userId, true);
+        } else {
+            mAffiliationWithDevice.remove(userId);
+        }
+    }
+
+    @Override
+    public boolean hasAffiliationWithDevice(int userId) {
+        return mAffiliationWithDevice.getOrDefault(userId, false);
+    }
+
     @Override
     public boolean isUserOrganizationManaged(@UserIdInt int userHandle) {
         if (mHasProfileOwner.getOrDefault(userHandle, false)
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
index 194647fd..0c1c406 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
@@ -131,7 +131,8 @@
             }
 
             notifyChangeLocked();
-            pushToActivityTaskManagerLocked();
+            pushDeviceOwnerUidToActivityTaskManagerLocked();
+            pushProfileOwnerUidsToActivityTaskManagerLocked();
         }
     }
 
@@ -163,11 +164,16 @@
     }
 
     @GuardedBy("mData")
-    private void pushToActivityTaskManagerLocked() {
+    private void pushDeviceOwnerUidToActivityTaskManagerLocked() {
         mActivityTaskManagerInternal.setDeviceOwnerUid(getDeviceOwnerUidLocked());
     }
 
     @GuardedBy("mData")
+    private void pushProfileOwnerUidsToActivityTaskManagerLocked() {
+        mActivityTaskManagerInternal.setProfileOwnerUids(getProfileOwnerUidsLocked());
+    }
+
+    @GuardedBy("mData")
     private void pushToActivityManagerLocked() {
         mActivityManagerInternal.setDeviceOwnerUid(getDeviceOwnerUidLocked());
 
@@ -196,6 +202,11 @@
         }
     }
 
+    @GuardedBy("mData")
+    Set<Integer> getProfileOwnerUidsLocked() {
+        return mData.mProfileOwners.keySet();
+    }
+
     String getDeviceOwnerPackageName() {
         synchronized (mData) {
             return mData.mDeviceOwner != null ? mData.mDeviceOwner.packageName : null;
@@ -263,7 +274,7 @@
             }
 
             notifyChangeLocked();
-            pushToActivityTaskManagerLocked();
+            pushDeviceOwnerUidToActivityTaskManagerLocked();
         }
     }
 
@@ -282,7 +293,7 @@
                 mUserManagerInternal.setDeviceManaged(false);
             }
             notifyChangeLocked();
-            pushToActivityTaskManagerLocked();
+            pushDeviceOwnerUidToActivityTaskManagerLocked();
         }
     }
 
@@ -302,6 +313,7 @@
                 mUserManagerInternal.setUserManaged(userId, true);
             }
             notifyChangeLocked();
+            pushProfileOwnerUidsToActivityTaskManagerLocked();
         }
     }
 
@@ -317,6 +329,7 @@
                 mUserManagerInternal.setUserManaged(userId, false);
             }
             notifyChangeLocked();
+            pushProfileOwnerUidsToActivityTaskManagerLocked();
         }
     }
 
@@ -328,6 +341,7 @@
                     ownerInfo.isOrganizationOwnedDevice);
             mData.mProfileOwners.put(userId, newOwnerInfo);
             notifyChangeLocked();
+            pushProfileOwnerUidsToActivityTaskManagerLocked();
         }
     }
 
@@ -345,7 +359,7 @@
                         mData.mDeviceOwner.packageName, previousDeviceOwnerType);
             }
             notifyChangeLocked();
-            pushToActivityTaskManagerLocked();
+            pushDeviceOwnerUidToActivityTaskManagerLocked();
         }
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
index c040b19..05780eb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
@@ -582,6 +582,49 @@
     }
 
     @Test
+    public void testModifyingInternalFlags() {
+        final JobInfo jobInfo =
+                new JobInfo.Builder(101, new ComponentName("foo", "bar"))
+                        .setExpedited(true)
+                        .build();
+        JobStatus job = createJobStatus(jobInfo);
+
+        assertEquals(0, job.getInternalFlags());
+
+        // Add single flag
+        job.addInternalFlags(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION);
+        assertEquals(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION, job.getInternalFlags());
+
+        // Add multiple flags
+        job.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER
+                | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ);
+        assertEquals(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION
+                        | JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER
+                        | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ,
+                job.getInternalFlags());
+
+        // Add flag that's already set
+        job.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ);
+        assertEquals(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION
+                        | JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER
+                        | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ,
+                job.getInternalFlags());
+
+        // Remove multiple
+        job.removeInternalFlags(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION
+                | JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER);
+        assertEquals(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ, job.getInternalFlags());
+
+        // Remove one that isn't set
+        job.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER);
+        assertEquals(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ, job.getInternalFlags());
+
+        // Remove final flag.
+        job.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ);
+        assertEquals(0, job.getInternalFlags());
+    }
+
+    @Test
     public void testShouldTreatAsUserInitiated() {
         JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
                 .setUserInitiated(false)
@@ -619,6 +662,9 @@
         rescheduledJob = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME,
                 0, 0, 0, 0, 0);
         assertFalse(rescheduledJob.shouldTreatAsUserInitiatedJob());
+
+        rescheduledJob.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER);
+        assertTrue(rescheduledJob.shouldTreatAsUserInitiatedJob());
     }
 
     @Test
@@ -641,6 +687,9 @@
         rescheduledJob = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME,
                 0, 0, 0, 0, 0);
         assertFalse(rescheduledJob.shouldTreatAsUserInitiatedJob());
+
+        rescheduledJob.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ);
+        assertTrue(rescheduledJob.shouldTreatAsUserInitiatedJob());
     }
 
     /**
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
index 3c77a35..527bc5b 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
@@ -62,18 +62,15 @@
             super(null, null, null, null, 0, null, 0, 0,
                     mock(BiometricLogger.class), mock(BiometricContext.class));
         }
-
-        @Override
-        public boolean isInterruptable() {
-            return true;
-        }
     }
 
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
 
     @Mock
-    private InterruptableMonitor<FakeHal> mClientMonitor;
+    private InterruptableMonitor<FakeHal> mInterruptableClientMonitor;
+    @Mock
+    private BaseClientMonitor mNonInterruptableClientMonitor;
     @Mock
     private ClientMonitorCallback mClientCallback;
     @Mock
@@ -84,149 +81,159 @@
     ArgumentCaptor<ClientMonitorCallback> mStartedCallbackCaptor;
 
     private Handler mHandler;
-    private BiometricSchedulerOperation mOperation;
+    private BiometricSchedulerOperation mInterruptableOperation;
+    private BiometricSchedulerOperation mNonInterruptableOperation;
     private boolean mIsDebuggable;
 
     @Before
     public void setUp() {
         mHandler = new Handler(TestableLooper.get(this).getLooper());
         mIsDebuggable = false;
-        mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback,
-                () -> mIsDebuggable);
+        mInterruptableOperation = new BiometricSchedulerOperation(mInterruptableClientMonitor,
+                mClientCallback, () -> mIsDebuggable);
+        mNonInterruptableOperation = new BiometricSchedulerOperation(mNonInterruptableClientMonitor,
+                mClientCallback, () -> mIsDebuggable);
+
+        when(mInterruptableClientMonitor.isInterruptable()).thenReturn(true);
+        when(mNonInterruptableClientMonitor.isInterruptable()).thenReturn(false);
     }
 
     @Test
     public void testStartWithCookie() {
         final int cookie = 200;
-        when(mClientMonitor.getCookie()).thenReturn(cookie);
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        assertThat(mOperation.isReadyToStart(mOnStartCallback)).isEqualTo(cookie);
-        assertThat(mOperation.isStarted()).isFalse();
-        assertThat(mOperation.isCanceling()).isFalse();
-        assertThat(mOperation.isFinished()).isFalse();
-        verify(mClientMonitor).waitForCookie(any());
+        assertThat(mInterruptableOperation.isReadyToStart(mOnStartCallback)).isEqualTo(cookie);
+        assertThat(mInterruptableOperation.isStarted()).isFalse();
+        assertThat(mInterruptableOperation.isCanceling()).isFalse();
+        assertThat(mInterruptableOperation.isFinished()).isFalse();
+        verify(mInterruptableClientMonitor).waitForCookie(any());
 
-        final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie);
+        final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie);
 
         assertThat(started).isTrue();
-        verify(mClientMonitor).start(mStartedCallbackCaptor.capture());
-        mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor);
-        assertThat(mOperation.isStarted()).isTrue();
+        verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture());
+        mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor);
+        assertThat(mInterruptableOperation.isStarted()).isTrue();
     }
 
     @Test
     public void testNoStartWithoutCookie() {
         final int goodCookie = 20;
         final int badCookie = 22;
-        when(mClientMonitor.getCookie()).thenReturn(goodCookie);
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getCookie()).thenReturn(goodCookie);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        assertThat(mOperation.isReadyToStart(mOnStartCallback)).isEqualTo(goodCookie);
-        final boolean started = mOperation.startWithCookie(mOnStartCallback, badCookie);
+        assertThat(mInterruptableOperation.isReadyToStart(mOnStartCallback)).isEqualTo(goodCookie);
+        final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback,
+                badCookie);
 
         assertThat(started).isFalse();
-        assertThat(mOperation.isStarted()).isFalse();
-        assertThat(mOperation.isCanceling()).isFalse();
-        assertThat(mOperation.isFinished()).isFalse();
-        verify(mClientMonitor).waitForCookie(any());
-        verify(mClientMonitor, never()).start(any());
+        assertThat(mInterruptableOperation.isStarted()).isFalse();
+        assertThat(mInterruptableOperation.isCanceling()).isFalse();
+        assertThat(mInterruptableOperation.isFinished()).isFalse();
+        verify(mInterruptableClientMonitor).waitForCookie(any());
+        verify(mInterruptableClientMonitor, never()).start(any());
     }
 
     @Test
     public void testSecondStartWithCookieCrashesWhenDebuggable() {
         final int cookie = 5;
         mIsDebuggable = true;
-        when(mClientMonitor.getCookie()).thenReturn(cookie);
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie);
+        final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie);
         assertThat(started).isTrue();
 
         assertThrows(IllegalStateException.class,
-                () -> mOperation.startWithCookie(mOnStartCallback, cookie));
+                () -> mInterruptableOperation.startWithCookie(mOnStartCallback, cookie));
     }
 
     @Test
     public void testSecondStartWithCookieFailsNicelyWhenNotDebuggable() {
         final int cookie = 5;
         mIsDebuggable = false;
-        when(mClientMonitor.getCookie()).thenReturn(cookie);
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie);
+        final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie);
         assertThat(started).isTrue();
 
-        final boolean startedAgain = mOperation.startWithCookie(mOnStartCallback, cookie);
+        final boolean startedAgain = mInterruptableOperation.startWithCookie(mOnStartCallback,
+                cookie);
         assertThat(startedAgain).isFalse();
     }
 
     @Test
     public void startsWhenReadyAndHalAvailable() {
-        when(mClientMonitor.getCookie()).thenReturn(0);
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getCookie()).thenReturn(0);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        mOperation.start(mOnStartCallback);
-        verify(mClientMonitor).start(mStartedCallbackCaptor.capture());
-        mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor);
+        mInterruptableOperation.start(mOnStartCallback);
+        verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture());
+        mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor);
 
-        assertThat(mOperation.isStarted()).isTrue();
-        assertThat(mOperation.isCanceling()).isFalse();
-        assertThat(mOperation.isFinished()).isFalse();
+        assertThat(mInterruptableOperation.isStarted()).isTrue();
+        assertThat(mInterruptableOperation.isCanceling()).isFalse();
+        assertThat(mInterruptableOperation.isFinished()).isFalse();
 
-        verify(mClientCallback).onClientStarted(eq(mClientMonitor));
-        verify(mOnStartCallback).onClientStarted(eq(mClientMonitor));
+        verify(mClientCallback).onClientStarted(eq(mInterruptableClientMonitor));
+        verify(mOnStartCallback).onClientStarted(eq(mInterruptableClientMonitor));
         verify(mClientCallback, never()).onClientFinished(any(), anyBoolean());
         verify(mOnStartCallback, never()).onClientFinished(any(), anyBoolean());
 
-        mStartedCallbackCaptor.getValue().onClientFinished(mClientMonitor, true);
+        mStartedCallbackCaptor.getValue().onClientFinished(mInterruptableClientMonitor,
+                true);
 
-        assertThat(mOperation.isFinished()).isTrue();
-        assertThat(mOperation.isCanceling()).isFalse();
-        verify(mClientMonitor).destroy();
-        verify(mOnStartCallback).onClientFinished(eq(mClientMonitor), eq(true));
+        assertThat(mInterruptableOperation.isFinished()).isTrue();
+        assertThat(mInterruptableOperation.isCanceling()).isFalse();
+        verify(mInterruptableClientMonitor).destroy();
+        verify(mOnStartCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(true));
     }
 
     @Test
     public void startFailsWhenReadyButHalNotAvailable() {
-        when(mClientMonitor.getCookie()).thenReturn(0);
-        when(mClientMonitor.getFreshDaemon()).thenReturn(null);
+        when(mInterruptableClientMonitor.getCookie()).thenReturn(0);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(null);
 
-        mOperation.start(mOnStartCallback);
-        verify(mClientMonitor, never()).start(any());
+        mInterruptableOperation.start(mOnStartCallback);
+        verify(mInterruptableClientMonitor, never()).start(any());
 
-        assertThat(mOperation.isStarted()).isFalse();
-        assertThat(mOperation.isCanceling()).isFalse();
-        assertThat(mOperation.isFinished()).isTrue();
+        assertThat(mInterruptableOperation.isStarted()).isFalse();
+        assertThat(mInterruptableOperation.isCanceling()).isFalse();
+        assertThat(mInterruptableOperation.isFinished()).isTrue();
 
-        verify(mClientCallback, never()).onClientStarted(eq(mClientMonitor));
-        verify(mOnStartCallback, never()).onClientStarted(eq(mClientMonitor));
-        verify(mClientCallback).onClientFinished(eq(mClientMonitor), eq(false));
-        verify(mOnStartCallback).onClientFinished(eq(mClientMonitor), eq(false));
+        verify(mClientCallback, never()).onClientStarted(eq(mInterruptableClientMonitor));
+        verify(mOnStartCallback, never()).onClientStarted(eq(mInterruptableClientMonitor));
+        verify(mClientCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(false));
+        verify(mOnStartCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(false));
     }
 
     @Test
     public void secondStartCrashesWhenDebuggable() {
         mIsDebuggable = true;
-        when(mClientMonitor.getCookie()).thenReturn(0);
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getCookie()).thenReturn(0);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        final boolean started = mOperation.start(mOnStartCallback);
+        final boolean started = mInterruptableOperation.start(mOnStartCallback);
         assertThat(started).isTrue();
 
-        assertThrows(IllegalStateException.class, () -> mOperation.start(mOnStartCallback));
+        assertThrows(IllegalStateException.class, () -> mInterruptableOperation.start(
+                mOnStartCallback));
     }
 
     @Test
     public void secondStartFailsNicelyWhenNotDebuggable() {
         mIsDebuggable = false;
-        when(mClientMonitor.getCookie()).thenReturn(0);
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getCookie()).thenReturn(0);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        final boolean started = mOperation.start(mOnStartCallback);
+        final boolean started = mInterruptableOperation.start(mOnStartCallback);
         assertThat(started).isTrue();
 
-        final boolean startedAgain = mOperation.start(mOnStartCallback);
+        final boolean startedAgain = mInterruptableOperation.start(mOnStartCallback);
         assertThat(startedAgain).isFalse();
     }
 
@@ -234,77 +241,78 @@
     public void doesNotStartWithCookie() {
         // This class only throws exceptions when debuggable.
         mIsDebuggable = true;
-        when(mClientMonitor.getCookie()).thenReturn(9);
+        when(mInterruptableClientMonitor.getCookie()).thenReturn(9);
         assertThrows(IllegalStateException.class,
-                () -> mOperation.start(mock(ClientMonitorCallback.class)));
+                () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class)));
     }
 
     @Test
     public void cannotRestart() {
         // This class only throws exceptions when debuggable.
         mIsDebuggable = true;
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        mOperation.start(mOnStartCallback);
+        mInterruptableOperation.start(mOnStartCallback);
 
         assertThrows(IllegalStateException.class,
-                () -> mOperation.start(mock(ClientMonitorCallback.class)));
+                () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class)));
     }
 
     @Test
     public void abortsNotRunning() {
         // This class only throws exceptions when debuggable.
         mIsDebuggable = true;
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        mOperation.abort();
+        mInterruptableOperation.abort();
 
-        assertThat(mOperation.isFinished()).isTrue();
-        verify(mClientMonitor).unableToStart();
-        verify(mClientMonitor).destroy();
+        assertThat(mInterruptableOperation.isFinished()).isTrue();
+        verify(mInterruptableClientMonitor).unableToStart();
+        verify(mInterruptableClientMonitor).destroy();
         assertThrows(IllegalStateException.class,
-                () -> mOperation.start(mock(ClientMonitorCallback.class)));
+                () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class)));
     }
 
     @Test
     public void abortCrashesWhenDebuggableIfOperationIsRunning() {
         mIsDebuggable = true;
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        mOperation.start(mOnStartCallback);
+        mInterruptableOperation.start(mOnStartCallback);
 
-        assertThrows(IllegalStateException.class, () -> mOperation.abort());
+        assertThrows(IllegalStateException.class, () -> mInterruptableOperation.abort());
     }
 
     @Test
     public void abortFailsNicelyWhenNotDebuggableIfOperationIsRunning() {
         mIsDebuggable = false;
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        mOperation.start(mOnStartCallback);
+        mInterruptableOperation.start(mOnStartCallback);
 
-        mOperation.abort();
+        mInterruptableOperation.abort();
     }
 
     @Test
     public void cancel() {
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
         final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
-        mOperation.start(mOnStartCallback);
-        verify(mClientMonitor).start(mStartedCallbackCaptor.capture());
-        mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor);
-        mOperation.cancel(mHandler, cancelCb);
+        mInterruptableOperation.start(mOnStartCallback);
+        verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture());
+        mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor);
+        mInterruptableOperation.cancel(mHandler, cancelCb);
 
-        assertThat(mOperation.isCanceling()).isTrue();
-        verify(mClientMonitor).cancel();
-        verify(mClientMonitor, never()).destroy();
+        assertThat(mInterruptableOperation.isCanceling()).isTrue();
+        verify(mInterruptableClientMonitor).cancel();
+        verify(mInterruptableClientMonitor, never()).destroy();
 
-        mStartedCallbackCaptor.getValue().onClientFinished(mClientMonitor, true);
+        mStartedCallbackCaptor.getValue().onClientFinished(mInterruptableClientMonitor,
+                true);
 
-        assertThat(mOperation.isFinished()).isTrue();
-        assertThat(mOperation.isCanceling()).isFalse();
-        verify(mClientMonitor).destroy();
+        assertThat(mInterruptableOperation.isFinished()).isTrue();
+        assertThat(mInterruptableOperation.isCanceling()).isFalse();
+        verify(mInterruptableClientMonitor).destroy();
 
         // should be unused since the operation was started
         verify(cancelCb, never()).onClientStarted(any());
@@ -313,61 +321,84 @@
 
     @Test
     public void cancelWithoutStarting() {
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
         final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
-        mOperation.cancel(mHandler, cancelCb);
+        mInterruptableOperation.cancel(mHandler, cancelCb);
 
-        assertThat(mOperation.isCanceling()).isTrue();
+        assertThat(mInterruptableOperation.isCanceling()).isTrue();
         ArgumentCaptor<ClientMonitorCallback> cbCaptor =
                 ArgumentCaptor.forClass(ClientMonitorCallback.class);
-        verify(mClientMonitor).cancelWithoutStarting(cbCaptor.capture());
+        verify(mInterruptableClientMonitor).cancelWithoutStarting(cbCaptor.capture());
 
-        cbCaptor.getValue().onClientFinished(mClientMonitor, true);
-        verify(cancelCb).onClientFinished(eq(mClientMonitor), eq(true));
-        verify(mClientMonitor, never()).start(any());
-        verify(mClientMonitor, never()).cancel();
-        verify(mClientMonitor).destroy();
+        cbCaptor.getValue().onClientFinished(mInterruptableClientMonitor, true);
+        verify(cancelCb).onClientFinished(eq(mInterruptableClientMonitor), eq(true));
+        verify(mInterruptableClientMonitor, never()).start(any());
+        verify(mInterruptableClientMonitor, never()).cancel();
+        verify(mInterruptableClientMonitor).destroy();
     }
 
     @Test
     public void cancelCrashesWhenDebuggableIfOperationIsFinished() {
         mIsDebuggable = true;
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        mOperation.abort();
-        assertThat(mOperation.isFinished()).isTrue();
+        mInterruptableOperation.abort();
+        assertThat(mInterruptableOperation.isFinished()).isTrue();
 
         final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
-        assertThrows(IllegalStateException.class, () -> mOperation.cancel(mHandler, cancelCb));
+        assertThrows(IllegalStateException.class, () -> mInterruptableOperation.cancel(mHandler,
+                cancelCb));
     }
 
     @Test
     public void cancelFailsNicelyWhenNotDebuggableIfOperationIsFinished() {
         mIsDebuggable = false;
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        mOperation.abort();
-        assertThat(mOperation.isFinished()).isTrue();
+        mInterruptableOperation.abort();
+        assertThat(mInterruptableOperation.isFinished()).isTrue();
 
         final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
-        mOperation.cancel(mHandler, cancelCb);
+        mInterruptableOperation.cancel(mHandler, cancelCb);
     }
 
     @Test
-    public void markCanceling() {
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+    public void markCanceling_interruptableClient() {
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        mOperation.markCanceling();
+        mInterruptableOperation.markCanceling();
 
-        assertThat(mOperation.isMarkedCanceling()).isTrue();
-        assertThat(mOperation.isCanceling()).isFalse();
-        assertThat(mOperation.isFinished()).isFalse();
-        verify(mClientMonitor, never()).start(any());
-        verify(mClientMonitor, never()).cancel();
-        verify(mClientMonitor, never()).cancelWithoutStarting(any());
-        verify(mClientMonitor, never()).unableToStart();
-        verify(mClientMonitor, never()).destroy();
+        assertThat(mInterruptableOperation.isMarkedCanceling()).isTrue();
+        assertThat(mInterruptableOperation.isCanceling()).isFalse();
+        assertThat(mInterruptableOperation.isFinished()).isFalse();
+        verify(mInterruptableClientMonitor, never()).start(any());
+        verify(mInterruptableClientMonitor, never()).cancel();
+        verify(mInterruptableClientMonitor, never()).cancelWithoutStarting(any());
+        verify(mInterruptableClientMonitor, never()).unableToStart();
+        verify(mInterruptableClientMonitor, never()).destroy();
+    }
+
+    @Test
+    public void markCanceling_nonInterruptableClient() {
+        mNonInterruptableOperation.markCanceling();
+
+        assertThat(mNonInterruptableOperation.isMarkedCanceling()).isFalse();
+        assertThat(mNonInterruptableOperation.isCanceling()).isFalse();
+        assertThat(mNonInterruptableOperation.isFinished()).isFalse();
+        verify(mNonInterruptableClientMonitor, never()).start(any());
+        verify(mNonInterruptableClientMonitor, never()).cancel();
+        verify(mNonInterruptableClientMonitor, never()).cancelWithoutStarting(any());
+        verify(mNonInterruptableClientMonitor, never()).destroy();
+    }
+
+    @Test
+    public void markCancelingForWatchdog() {
+        mNonInterruptableOperation.markCancelingForWatchdog();
+        mInterruptableOperation.markCancelingForWatchdog();
+
+        assertThat(mInterruptableOperation.isMarkedCanceling()).isTrue();
+        assertThat(mNonInterruptableOperation.isMarkedCanceling()).isTrue();
     }
 
     @Test
@@ -381,26 +412,26 @@
     }
 
     private void markCancellingAndStart(Integer withCookie) {
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
         if (withCookie != null) {
-            when(mClientMonitor.getCookie()).thenReturn(withCookie);
+            when(mInterruptableClientMonitor.getCookie()).thenReturn(withCookie);
         }
 
-        mOperation.markCanceling();
+        mInterruptableOperation.markCanceling();
         final ClientMonitorCallback cb = mock(ClientMonitorCallback.class);
         if (withCookie != null) {
-            mOperation.startWithCookie(cb, withCookie);
+            mInterruptableOperation.startWithCookie(cb, withCookie);
         } else {
-            mOperation.start(cb);
+            mInterruptableOperation.start(cb);
         }
 
-        assertThat(mOperation.isFinished()).isTrue();
-        verify(cb).onClientFinished(eq(mClientMonitor), eq(true));
-        verify(mClientMonitor, never()).start(any());
-        verify(mClientMonitor, never()).cancel();
-        verify(mClientMonitor, never()).cancelWithoutStarting(any());
-        verify(mClientMonitor, never()).unableToStart();
-        verify(mClientMonitor).destroy();
+        assertThat(mInterruptableOperation.isFinished()).isTrue();
+        verify(cb).onClientFinished(eq(mInterruptableClientMonitor), eq(true));
+        verify(mInterruptableClientMonitor, never()).start(any());
+        verify(mInterruptableClientMonitor, never()).cancel();
+        verify(mInterruptableClientMonitor, never()).cancelWithoutStarting(any());
+        verify(mInterruptableClientMonitor, never()).unableToStart();
+        verify(mInterruptableClientMonitor).destroy();
     }
 
     @Test
@@ -414,23 +445,23 @@
     }
 
     private void cancelWatchdog(boolean start) {
-        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        mOperation.start(mOnStartCallback);
+        mInterruptableOperation.start(mOnStartCallback);
         if (start) {
-            verify(mClientMonitor).start(mStartedCallbackCaptor.capture());
-            mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor);
+            verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture());
+            mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor);
         }
-        mOperation.cancel(mHandler, mock(ClientMonitorCallback.class));
+        mInterruptableOperation.cancel(mHandler, mock(ClientMonitorCallback.class));
 
-        assertThat(mOperation.isCanceling()).isTrue();
+        assertThat(mInterruptableOperation.isCanceling()).isTrue();
 
         // omit call to onClientFinished and trigger watchdog
-        mOperation.mCancelWatchdog.run();
+        mInterruptableOperation.mCancelWatchdog.run();
 
-        assertThat(mOperation.isFinished()).isTrue();
-        assertThat(mOperation.isCanceling()).isFalse();
-        verify(mOnStartCallback).onClientFinished(eq(mClientMonitor), eq(false));
-        verify(mClientMonitor).destroy();
+        assertThat(mInterruptableOperation.isFinished()).isTrue();
+        assertThat(mInterruptableOperation.isCanceling()).isFalse();
+        verify(mOnStartCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(false));
+        verify(mInterruptableClientMonitor).destroy();
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 95fc0fa..2671e77 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -645,7 +645,7 @@
         runAndVerifyBackgroundActivityStartsSubtest("allowed_noStartsAborted", false,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, false, false, false);
+                false, false, false, false, false, false, false, false);
     }
 
     /**
@@ -659,7 +659,7 @@
                 "disallowed_unsupportedUsecase_aborted", true,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, false, false, false);
+                false, false, false, false, false, false, false, false);
     }
 
     /**
@@ -673,7 +673,7 @@
                 "disallowed_callingUidProcessStateTop_aborted", true,
                 UNIMPORTANT_UID, false, PROCESS_STATE_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, false, false, false);
+                false, false, false, false, false, false, false, false);
     }
 
     /**
@@ -687,7 +687,7 @@
                 "disallowed_realCallingUidProcessStateTop_aborted", true,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_TOP,
-                false, false, false, false, false, false, false);
+                false, false, false, false, false, false, false, false);
     }
 
     /**
@@ -701,7 +701,7 @@
                 "disallowed_hasForegroundActivities_aborted", true,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                true, false, false, false, false, false, false);
+                true, false, false, false, false, false, false, false);
     }
 
     /**
@@ -715,7 +715,7 @@
                 "disallowed_pinned_singleinstance_aborted", true,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, false, true, false);
+                false, false, false, false, false, false, true, false);
     }
 
     /**
@@ -729,7 +729,7 @@
         runAndVerifyBackgroundActivityStartsSubtest("disallowed_rootUid_notAborted", false,
                 Process.ROOT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, false, false, false);
+                false, false, false, false, false, false, false, false);
     }
 
     /**
@@ -743,7 +743,7 @@
         runAndVerifyBackgroundActivityStartsSubtest("disallowed_systemUid_notAborted", false,
                 Process.SYSTEM_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, false, false, false);
+                false, false, false, false, false, false, false, false);
     }
 
     /**
@@ -757,7 +757,7 @@
         runAndVerifyBackgroundActivityStartsSubtest("disallowed_nfcUid_notAborted", false,
                 Process.NFC_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, false, false, false);
+                false, false, false, false, false, false, false, false);
     }
 
     /**
@@ -772,7 +772,7 @@
                 "disallowed_callingUidHasVisibleWindow_notAborted", false,
                 UNIMPORTANT_UID, true, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, false, false, false);
+                false, false, false, false, false, false, false, false);
     }
 
     /**
@@ -788,7 +788,7 @@
                 "disallowed_realCallingUidHasVisibleWindow_abortedInU", true,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, true, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, false, false, false);
+                false, false, false, false, false, false, false, false);
     }
 
     /**
@@ -803,7 +803,7 @@
                 "disallowed_callerIsRecents_notAborted", false,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, true, false, false, false, false, false);
+                false, true, false, false, false, false, false, false);
     }
 
     /**
@@ -818,7 +818,7 @@
                 "disallowed_callerIsAllowed_notAborted", false,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, true, false, false, false, false);
+                false, false, true, false, false, false, false, false);
     }
 
     /**
@@ -834,7 +834,7 @@
                 false,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, true, false, false, false);
+                false, false, false, true, false, false, false, false);
     }
 
     /**
@@ -850,7 +850,23 @@
                 "disallowed_callingPackageNameIsDeviceOwner_notAborted", false,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, true, false, false);
+                false, false, false, false, true, false, false, false);
+    }
+
+    /**
+     * This test ensures that supported usecases aren't aborted when background starts are
+     * disallowed. Each scenarios tests one condition that makes them supported in isolation. In
+     * this case the caller is a affiliated profile owner.
+     */
+    @Test
+    public void
+            testBackgroundActivityStartsDisallowed_isAffiliatedProfileOwnerNotAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
+        runAndVerifyBackgroundActivityStartsSubtest(
+                "disallowed_callingUidIsAffiliatedProfileOwner_notAborted", false,
+                UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
+                false, false, false, false, false, true, false, false);
     }
 
     /**
@@ -865,7 +881,7 @@
                 "disallowed_callerHasSystemExemptAppOpNotAborted", false,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, false, false, true);
+                false, false, false, false, false, false, false, true);
     }
 
     /**
@@ -881,7 +897,7 @@
                 "disallowed_callingPackageNameIsIme_notAborted", false,
                 CURRENT_IME_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, false, false, false, false, false, false);
+                false, false, false, false, false, false, false, false);
     }
 
     /**
@@ -902,7 +918,7 @@
                 "allowed_notAborted", false,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
-                false, true, false, false, false, false, false);
+                false, true, false, false, false, false, false, false);
         verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
                 "",  // activity name
                 BackgroundActivityStartController.BAL_ALLOW_PERMISSION,
@@ -933,7 +949,7 @@
                 "allowed_notAborted", false,
                 UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 Process.SYSTEM_UID, true, PROCESS_STATE_BOUND_TOP,
-                false, true, false, false, false, false, false);
+                false, true, false, false, false, false, false, false);
         verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
                 DEFAULT_COMPONENT_PACKAGE_NAME + "/" + DEFAULT_COMPONENT_PACKAGE_NAME,
                 BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT,
@@ -949,6 +965,7 @@
             boolean callerIsTempAllowed,
             boolean callerIsInstrumentingWithBackgroundActivityStartPrivileges,
             boolean isCallingUidDeviceOwner,
+            boolean isCallingUidAffiliatedProfileOwner,
             boolean isPinnedSingleInstance,
             boolean hasSystemExemptAppOp) {
         // window visibility
@@ -982,6 +999,9 @@
                 callerIsInstrumentingWithBackgroundActivityStartPrivileges);
         // callingUid is the device owner
         doReturn(isCallingUidDeviceOwner).when(mAtm).isDeviceOwner(callingUid);
+        // callingUid is the affiliated profile owner
+        doReturn(isCallingUidAffiliatedProfileOwner).when(mAtm)
+            .isAffiliatedProfileOwner(callingUid);
 
         // caller has OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop
         doReturn(hasSystemExemptAppOp ? AppOpsManager.MODE_ALLOWED
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
index cd29dac..3a65104 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
@@ -47,7 +47,6 @@
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_UNEXPECTED_CALLBACK;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECT_UNEXPECTED_CALLBACK;
 import static com.android.server.voiceinteraction.HotwordDetectionConnection.ENFORCE_HOTWORD_PHRASE_ID;
-import static com.android.server.voiceinteraction.SoundTriggerSessionPermissionsDecorator.enforcePermissionForPreflight;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -743,7 +742,14 @@
     void enforcePermissionsForDataDelivery() {
         Binder.withCleanCallingIdentity(() -> {
             synchronized (mLock) {
-                enforcePermissionForPreflight(mContext, mVoiceInteractorIdentity, RECORD_AUDIO);
+                int result = PermissionChecker.checkPermissionForPreflight(
+                        mContext, RECORD_AUDIO, /* pid */ -1, mVoiceInteractorIdentity.uid,
+                        mVoiceInteractorIdentity.packageName);
+                if (result != PermissionChecker.PERMISSION_GRANTED) {
+                    throw new SecurityException(
+                        "Failed to obtain permission RECORD_AUDIO for identity "
+                        + mVoiceInteractorIdentity);
+                }
                 int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
                 mAppOpsManager.noteOpNoThrow(hotwordOp,
                         mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
@@ -770,7 +776,7 @@
             throw new SecurityException(
                     TextUtils.formatSimple("Failed to obtain permission %s for identity %s",
                             permission,
-                            SoundTriggerSessionPermissionsDecorator.toString(identity)));
+                            identity));
         }
     }
 
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java
deleted file mode 100644
index 0ef2f06..0000000
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.voiceinteraction;
-
-import android.hardware.soundtrigger.SoundTrigger;
-import android.os.RemoteException;
-
-import com.android.internal.app.IHotwordRecognitionStatusCallback;
-import com.android.internal.app.IVoiceInteractionSoundTriggerSession;
-
-/**
- * A remote object that simply proxies calls to a real {@link IVoiceInteractionSoundTriggerSession}
- * implementation. This design pattern allows us to add decorators to the core implementation
- * (simply wrapping a binder object does not work).
- */
-final class SoundTriggerSessionBinderProxy extends IVoiceInteractionSoundTriggerSession.Stub {
-
-    private final IVoiceInteractionSoundTriggerSession mDelegate;
-
-    SoundTriggerSessionBinderProxy(IVoiceInteractionSoundTriggerSession delegate) {
-        mDelegate = delegate;
-    }
-
-    @Override
-    public SoundTrigger.ModuleProperties getDspModuleProperties() throws RemoteException {
-        return mDelegate.getDspModuleProperties();
-    }
-
-    @Override
-    public int startRecognition(int i, String s,
-            IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback,
-            SoundTrigger.RecognitionConfig recognitionConfig, boolean b) throws RemoteException {
-        return mDelegate.startRecognition(i, s, iHotwordRecognitionStatusCallback,
-                recognitionConfig, b);
-    }
-
-    @Override
-    public int stopRecognition(int i,
-            IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback)
-            throws RemoteException {
-        return mDelegate.stopRecognition(i, iHotwordRecognitionStatusCallback);
-    }
-
-    @Override
-    public int setParameter(int i, int i1, int i2) throws RemoteException {
-        return mDelegate.setParameter(i, i1, i2);
-    }
-
-    @Override
-    public int getParameter(int i, int i1) throws RemoteException {
-        return mDelegate.getParameter(i, i1);
-    }
-
-    @Override
-    public SoundTrigger.ModelParamRange queryParameter(int i, int i1) throws RemoteException {
-        return mDelegate.queryParameter(i, i1);
-    }
-
-    @Override
-    public void detach() throws RemoteException {
-        mDelegate.detach();
-    }
-}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java
deleted file mode 100644
index 0f8a945..0000000
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.voiceinteraction;
-
-import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
-import static android.Manifest.permission.RECORD_AUDIO;
-
-import static com.android.server.voiceinteraction.HotwordDetectionConnection.DEBUG;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.content.PermissionChecker;
-import android.hardware.soundtrigger.SoundTrigger;
-import android.media.permission.Identity;
-import android.media.permission.PermissionUtil;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.text.TextUtils;
-import android.util.Slog;
-
-import com.android.internal.app.IHotwordRecognitionStatusCallback;
-import com.android.internal.app.IVoiceInteractionSoundTriggerSession;
-
-/**
- * Decorates {@link IVoiceInteractionSoundTriggerSession} with permission checks for {@link
- * android.Manifest.permission#RECORD_AUDIO} and
- * {@link android.Manifest.permission#CAPTURE_AUDIO_HOTWORD}.
- * <p>
- * Does not implement {@link #asBinder()} as it's intended to be wrapped by an
- * {@link IVoiceInteractionSoundTriggerSession.Stub} object.
- */
-final class SoundTriggerSessionPermissionsDecorator implements
-        IVoiceInteractionSoundTriggerSession {
-    static final String TAG = "SoundTriggerSessionPermissionsDecorator";
-
-    private final IVoiceInteractionSoundTriggerSession mDelegate;
-    private final Context mContext;
-    private final Identity mOriginatorIdentity;
-
-    SoundTriggerSessionPermissionsDecorator(IVoiceInteractionSoundTriggerSession delegate,
-            Context context, Identity originatorIdentity) {
-        mDelegate = delegate;
-        mContext = context;
-        mOriginatorIdentity = originatorIdentity;
-    }
-
-    @Override
-    public SoundTrigger.ModuleProperties getDspModuleProperties() throws RemoteException {
-        // No permission needed here (the app must have the Assistant Role to retrieve the session).
-        return mDelegate.getDspModuleProperties();
-    }
-
-    @Override
-    public int startRecognition(int i, String s,
-            IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback,
-            SoundTrigger.RecognitionConfig recognitionConfig, boolean b) throws RemoteException {
-        if (DEBUG) {
-            Slog.d(TAG, "startRecognition");
-        }
-        if (!isHoldingPermissions()) {
-            return SoundTrigger.STATUS_PERMISSION_DENIED;
-        }
-        return mDelegate.startRecognition(i, s, iHotwordRecognitionStatusCallback,
-                recognitionConfig, b);
-    }
-
-    @Override
-    public int stopRecognition(int i,
-            IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback)
-            throws RemoteException {
-        // Stopping a model does not require special permissions. Having a handle to the session is
-        // sufficient.
-        return mDelegate.stopRecognition(i, iHotwordRecognitionStatusCallback);
-    }
-
-    @Override
-    public int setParameter(int i, int i1, int i2) throws RemoteException {
-        if (!isHoldingPermissions()) {
-            return SoundTrigger.STATUS_PERMISSION_DENIED;
-        }
-        return mDelegate.setParameter(i, i1, i2);
-    }
-
-    @Override
-    public int getParameter(int i, int i1) throws RemoteException {
-        // No permission needed here (the app must have the Assistant Role to retrieve the session).
-        return mDelegate.getParameter(i, i1);
-    }
-
-    @Override
-    public SoundTrigger.ModelParamRange queryParameter(int i, int i1) throws RemoteException {
-        // No permission needed here (the app must have the Assistant Role to retrieve the session).
-        return mDelegate.queryParameter(i, i1);
-    }
-
-    @Override
-    public IBinder asBinder() {
-        throw new UnsupportedOperationException(
-                "This object isn't intended to be used as a Binder.");
-    }
-
-    @Override
-    public void detach() {
-        try {
-            mDelegate.detach();
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
-        }
-    }
-
-    // TODO: Share this code with SoundTriggerMiddlewarePermission.
-    private boolean isHoldingPermissions() {
-        try {
-            enforcePermissionForPreflight(mContext, mOriginatorIdentity, RECORD_AUDIO);
-            enforcePermissionForPreflight(mContext, mOriginatorIdentity, CAPTURE_AUDIO_HOTWORD);
-            return true;
-        } catch (SecurityException e) {
-            Slog.e(TAG, e.toString());
-            return false;
-        }
-    }
-
-    /**
-     * Throws a {@link SecurityException} if originator permanently doesn't have the given
-     * permission.
-     * Soft (temporary) denials are considered OK for preflight purposes.
-     *
-     * @param context    A {@link Context}, used for permission checks.
-     * @param identity   The identity to check.
-     * @param permission The identifier of the permission we want to check.
-     */
-    static void enforcePermissionForPreflight(@NonNull Context context,
-            @NonNull Identity identity, @NonNull String permission) {
-        final int status = PermissionUtil.checkPermissionForPreflight(context, identity,
-                permission);
-        switch (status) {
-            case PermissionChecker.PERMISSION_GRANTED:
-            case PermissionChecker.PERMISSION_SOFT_DENIED:
-                return;
-            case PermissionChecker.PERMISSION_HARD_DENIED:
-                throw new SecurityException(
-                        TextUtils.formatSimple("Failed to obtain permission %s for identity %s",
-                                permission, toString(identity)));
-            default:
-                throw new RuntimeException("Unexpected permission check result.");
-        }
-    }
-
-    static String toString(Identity identity) {
-        return "{uid=" + identity.uid
-                + " pid=" + identity.pid
-                + " packageName=" + identity.packageName
-                + " attributionTag=" + identity.attributionTag
-                + "}";
-    }
-
-    // Temporary hack for using the same status code as SoundTrigger, so we don't change behavior.
-    // TODO: Reuse SoundTrigger code so we don't need to do this.
-    private static final int TEMPORARY_PERMISSION_DENIED = 3;
-}
diff --git a/telecomm/java/android/telecom/CallAttributes.java b/telecomm/java/android/telecom/CallAttributes.java
index 52ff90f..b1a7d81 100644
--- a/telecomm/java/android/telecom/CallAttributes.java
+++ b/telecomm/java/android/telecom/CallAttributes.java
@@ -59,6 +59,9 @@
     public static final String CALL_CAPABILITIES_KEY = "TelecomCapabilities";
 
     /** @hide **/
+    public static final String DISPLAY_NAME_KEY = "DisplayName";
+
+    /** @hide **/
     public static final String CALLER_PID_KEY = "CallerPid";
 
     /** @hide **/