Merge "Fix the exception type set on ui cancellation" 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/app/admin/PolicyKey.java b/core/java/android/app/admin/PolicyKey.java
index 3544c19..9b12e59 100644
--- a/core/java/android/app/admin/PolicyKey.java
+++ b/core/java/android/app/admin/PolicyKey.java
@@ -25,6 +25,7 @@
 
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import android.util.Log;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -41,6 +42,9 @@
 @SuppressLint({"ParcelNotFinal", "ParcelCreator"})
 @SystemApi
 public abstract class PolicyKey implements Parcelable {
+
+    static final String TAG = "PolicyKey";
+
     /**
      * @hide
      */
@@ -76,9 +80,14 @@
     /**
      * @hide
      */
+    @Nullable
     public static PolicyKey readGenericPolicyKeyFromXml(TypedXmlPullParser parser) {
         String identifier = parser.getAttributeValue(
                 /* namespace= */ null, ATTR_POLICY_IDENTIFIER);
+        if (identifier == null) {
+            Log.wtf(TAG, "Error parsing generic policy key, identifier is null.");
+            return null;
+        }
         return new NoArgsPolicyKey(identifier);
     }
 
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/java/com/android/internal/os/anr/AnrLatencyTracker.java b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
index 6fa6fa5..3ba4ea5 100644
--- a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
+++ b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
@@ -118,6 +118,9 @@
     private boolean mIsSkipped = false;
     private boolean mCopyingFirstPidSucceeded = false;
 
+    private long mPreDumpIfLockTooSlowStartUptime;
+    private long mPreDumpIfLockTooSlowDuration = 0;
+
     private final int mAnrRecordPlacedOnQueueCookie =
             sNextAnrRecordPlacedOnQueueCookieGenerator.incrementAndGet();
 
@@ -401,6 +404,17 @@
         Trace.traceCounter(TRACE_TAG_ACTIVITY_MANAGER, "anrRecordsQueueSize", queueSize);
     }
 
+    /** Records the start of AnrController#preDumpIfLockTooSlow. */
+    public void preDumpIfLockTooSlowStarted() {
+        mPreDumpIfLockTooSlowStartUptime = getUptimeMillis();
+    }
+
+    /** Records the end of AnrController#preDumpIfLockTooSlow. */
+    public void preDumpIfLockTooSlowEnded() {
+        mPreDumpIfLockTooSlowDuration +=
+                getUptimeMillis() - mPreDumpIfLockTooSlowStartUptime;
+    }
+
     /** Records a skipped ANR in ProcessErrorStateRecord#appNotResponding. */
     public void anrSkippedProcessErrorStateRecordAppNotResponding() {
         anrSkipped("appNotResponding");
@@ -415,7 +429,7 @@
      * Returns latency data as a comma separated value string for inclusion in ANR report.
      */
     public String dumpAsCommaSeparatedArrayWithHeader() {
-        return "DurationsV3: " + mAnrTriggerUptime
+        return "DurationsV4: " + mAnrTriggerUptime
                 /* triggering_to_app_not_responding_duration = */
                 + "," + (mAppNotRespondingStartUptime -  mAnrTriggerUptime)
                 /* app_not_responding_duration = */
@@ -464,6 +478,8 @@
                 + "," + mEarlyDumpStatus
                 /* copying_first_pid_succeeded = */
                 + "," + (mCopyingFirstPidSucceeded ? 1 : 0)
+                /* preDumpIfLockTooSlow_duration = */
+                + "," + mPreDumpIfLockTooSlowDuration
                 + "\n\n";
 
     }
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/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 7c2759a..92c0dab 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -3865,6 +3865,12 @@
       "group": "WM_DEBUG_ADD_REMOVE",
       "at": "com\/android\/server\/wm\/WindowState.java"
     },
+    "1511273241": {
+      "message": "Refreshing activity for camera compatibility treatment, activityRecord=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+    },
     "1518495446": {
       "message": "removeWindowToken: Attempted to remove non-existing token: %s",
       "level": "WARN",
@@ -4297,12 +4303,6 @@
       "group": "WM_DEBUG_REMOTE_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RemoteAnimationController.java"
     },
-    "1967643923": {
-      "message": "Refershing activity for camera compatibility treatment, activityRecord=%s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_STATES",
-      "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
-    },
     "1967975839": {
       "message": "Changing app %s visible=%b performLayout=%b",
       "level": "VERBOSE",
diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp
index 9a06be0..701a87f 100644
--- a/libs/hwui/hwui/ImageDecoder.cpp
+++ b/libs/hwui/hwui/ImageDecoder.cpp
@@ -51,6 +51,9 @@
 sk_sp<SkColorSpace> ImageDecoder::getDefaultColorSpace() const {
     const skcms_ICCProfile* encodedProfile = mCodec->getICCProfile();
     if (encodedProfile) {
+        if (encodedProfile->has_CICP) {
+            return mCodec->computeOutputColorSpace(kN32_SkColorType);
+        }
         // If the profile maps directly to an SkColorSpace, that SkColorSpace
         // will be returned. Otherwise, nullptr will be returned. In either
         // case, using this SkColorSpace results in doing no color correction.
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 4759689..b1d2e33 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -3730,12 +3730,7 @@
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @RequiresPermission(Manifest.permission.BLUETOOTH_STACK)
     public void setA2dpSuspended(boolean enable) {
-        final IAudioService service = getService();
-        try {
-            service.setA2dpSuspended(enable);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        AudioSystem.setParameters("A2dpSuspended=" + enable);
     }
 
     /**
@@ -3748,12 +3743,7 @@
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @RequiresPermission(Manifest.permission.BLUETOOTH_STACK)
     public void setLeAudioSuspended(boolean enable) {
-        final IAudioService service = getService();
-        try {
-            service.setLeAudioSuspended(enable);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        AudioSystem.setParameters("LeAudioSuspended=" + enable);
     }
 
     /**
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 7ce189b..fe5afc5 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -231,12 +231,6 @@
 
     void setBluetoothScoOn(boolean on);
 
-    @EnforcePermission("BLUETOOTH_STACK")
-    void setA2dpSuspended(boolean on);
-
-    @EnforcePermission("BLUETOOTH_STACK")
-    void setLeAudioSuspended(boolean enable);
-
     boolean isBluetoothScoOn();
 
     void setBluetoothA2dpOn(boolean on);
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/layout/ongoing_call_chip.xml b/packages/SystemUI/res/layout/ongoing_call_chip.xml
index 238fc84..6a0217ec 100644
--- a/packages/SystemUI/res/layout/ongoing_call_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_call_chip.xml
@@ -23,7 +23,7 @@
     android:layout_gravity="center_vertical|start"
     android:layout_marginStart="5dp"
 >
-    <com.android.systemui.animation.view.LaunchableLinearLayout
+    <com.android.systemui.statusbar.phone.ongoingcall.OngoingCallBackgroundContainer
         android:id="@+id/ongoing_call_chip_background"
         android:layout_width="wrap_content"
         android:layout_height="@dimen/ongoing_appops_chip_height"
@@ -55,5 +55,5 @@
             android:textColor="?android:attr/colorPrimary"
         />
 
-    </com.android.systemui.animation.view.LaunchableLinearLayout>
+    </com.android.systemui.statusbar.phone.ongoingcall.OngoingCallBackgroundContainer>
 </FrameLayout>
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 bf0b8a6..2024dae 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -617,8 +617,8 @@
     <dimen name="qs_header_height">120dp</dimen>
     <dimen name="qs_header_row_min_height">48dp</dimen>
 
-    <dimen name="qs_header_non_clickable_element_height">24dp</dimen>
-    <dimen name="new_qs_header_non_clickable_element_height">24dp</dimen>
+    <dimen name="qs_header_non_clickable_element_height">24sp</dimen>
+    <dimen name="new_qs_header_non_clickable_element_height">24sp</dimen>
 
     <dimen name="qs_footer_padding">20dp</dimen>
     <dimen name="qs_security_footer_height">88dp</dimen>
@@ -1042,7 +1042,7 @@
     <dimen name="display_cutout_margin_consumption">0px</dimen>
 
     <!-- Height of the Ongoing App Ops chip -->
-    <dimen name="ongoing_appops_chip_height">24dp</dimen>
+    <dimen name="ongoing_appops_chip_height">24sp</dimen>
     <!-- Side padding between background of Ongoing App Ops chip and content -->
     <dimen name="ongoing_appops_chip_side_padding">8dp</dimen>
     <!-- Margin between icons of Ongoing App Ops chip -->
@@ -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/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainer.kt
new file mode 100644
index 0000000..ce88a5f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainer.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.statusbar.phone.ongoingcall
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.systemui.animation.view.LaunchableLinearLayout
+
+/**
+ * A container view for the ongoing call chip background. Needed so that we can limit the height of
+ * the background when the font size is very large (200%), in which case the background would go
+ * past the bounds of the status bar.
+ */
+class OngoingCallBackgroundContainer(context: Context, attrs: AttributeSet) :
+    LaunchableLinearLayout(context, attrs) {
+
+    /** Sets where this view should fetch its max height from. */
+    var maxHeightFetcher: (() -> Int)? = null
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+
+        val maxHeight = maxHeightFetcher?.invoke()
+        val chosenHeight =
+            if (maxHeight != null) {
+                // Give 1 extra px of space (without it, the background could still be cut off)
+                measuredHeight.coerceAtMost(maxHeight - 1)
+            } else {
+                measuredHeight
+            }
+        setMeasuredDimension(measuredWidth, chosenHeight)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index c73cc9e..b3af91d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -136,6 +136,9 @@
     fun setChipView(chipView: View) {
         tearDownChipView()
         this.chipView = chipView
+        val backgroundView: OngoingCallBackgroundContainer? =
+            chipView.findViewById(R.id.ongoing_call_chip_background)
+        backgroundView?.maxHeightFetcher = { statusBarWindowController.get().statusBarHeight }
         if (hasOngoingCall()) {
             updateChip()
         }
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/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
index 47df64f..0a9618c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
@@ -113,8 +113,8 @@
 
     @After
     fun tearDown() {
-        keyguardUnlockAnimationController.surfaceBehindEntryAnimator.cancel()
-        keyguardUnlockAnimationController.surfaceBehindAlphaAnimator.cancel()
+        keyguardUnlockAnimationController.notifyFinishedKeyguardExitAnimation(true)
+        keyguardUnlockAnimationController.wallpaperAlphaAnimator.cancel()
     }
 
     /**
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/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainerTest.kt
new file mode 100644
index 0000000..ec074d7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainerTest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.statusbar.phone.ongoingcall
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.LayoutInflater
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class OngoingCallBackgroundContainerTest : SysuiTestCase() {
+
+    private lateinit var underTest: OngoingCallBackgroundContainer
+
+    @Before
+    fun setUp() {
+        allowTestableLooperAsMainThread()
+        TestableLooper.get(this).runWithLooper {
+            val chipView = LayoutInflater.from(context).inflate(R.layout.ongoing_call_chip, null)
+            underTest = chipView.requireViewById(R.id.ongoing_call_chip_background)
+        }
+    }
+
+    @Test
+    fun onMeasure_maxHeightFetcherNotSet_usesDesired() {
+        underTest.maxHeightFetcher = null
+
+        underTest.measure(
+            WIDTH_SPEC,
+            View.MeasureSpec.makeMeasureSpec(123, View.MeasureSpec.EXACTLY),
+        )
+
+        assertThat(underTest.measuredHeight).isEqualTo(123)
+    }
+
+    @Test
+    fun onMeasure_maxLargerThanDesired_usesDesired() {
+        underTest.maxHeightFetcher = { 234 }
+
+        underTest.measure(
+            WIDTH_SPEC,
+            View.MeasureSpec.makeMeasureSpec(123, View.MeasureSpec.EXACTLY),
+        )
+
+        assertThat(underTest.measuredHeight).isEqualTo(123)
+    }
+
+    @Test
+    fun onMeasure_desiredLargerThanMax_usesMaxMinusOne() {
+        underTest.maxHeightFetcher = { 234 }
+
+        underTest.measure(
+            WIDTH_SPEC,
+            View.MeasureSpec.makeMeasureSpec(567, View.MeasureSpec.EXACTLY),
+        )
+
+        // We use the max - 1 to give a bit extra space
+        assertThat(underTest.measuredHeight).isEqualTo(233)
+    }
+
+    private companion object {
+        val WIDTH_SPEC = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+    }
+}
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/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index bc4e8df..71401f4 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -209,7 +209,6 @@
     private void init() {
         setupMessaging(mContext);
 
-        initAudioHalBluetoothState();
         initRoutingStrategyIds();
         mPreferredCommunicationDevice = null;
         updateActiveCommunicationDevice();
@@ -865,100 +864,10 @@
         }
     }
 
-    // Current Bluetooth SCO audio active state indicated by BtHelper via setBluetoothScoOn().
-    @GuardedBy("mDeviceStateLock")
+    /**
+     * Current Bluetooth SCO audio active state indicated by BtHelper via setBluetoothScoOn().
+     */
     private boolean mBluetoothScoOn;
-    // value of BT_SCO parameter currently applied to audio HAL.
-    @GuardedBy("mDeviceStateLock")
-    private boolean mBluetoothScoOnApplied;
-
-    // A2DP suspend state requested by AudioManager.setA2dpSuspended() API.
-    @GuardedBy("mDeviceStateLock")
-    private boolean mBluetoothA2dpSuspendedExt;
-    // A2DP suspend state requested by AudioDeviceInventory.
-    @GuardedBy("mDeviceStateLock")
-    private boolean mBluetoothA2dpSuspendedInt;
-    // value of BT_A2dpSuspendedSCO parameter currently applied to audio HAL.
-    @GuardedBy("mDeviceStateLock")
-    private boolean mBluetoothA2dpSuspendedApplied;
-
-    // LE Audio suspend state requested by AudioManager.setLeAudioSuspended() API.
-    @GuardedBy("mDeviceStateLock")
-    private boolean mBluetoothLeSuspendedExt;
-    // LE Audio suspend state requested by AudioDeviceInventory.
-    @GuardedBy("mDeviceStateLock")
-    private boolean mBluetoothLeSuspendedInt;
-    // value of LeAudioSuspended parameter currently applied to audio HAL.
-    @GuardedBy("mDeviceStateLock")
-    private boolean mBluetoothLeSuspendedApplied;
-
-    private void initAudioHalBluetoothState() {
-        mBluetoothScoOnApplied = false;
-        AudioSystem.setParameters("BT_SCO=off");
-        mBluetoothA2dpSuspendedApplied = false;
-        AudioSystem.setParameters("A2dpSuspended=false");
-        mBluetoothLeSuspendedApplied = false;
-        AudioSystem.setParameters("LeAudioSuspended=false");
-    }
-
-    @GuardedBy("mDeviceStateLock")
-    private void updateAudioHalBluetoothState() {
-        if (mBluetoothScoOn != mBluetoothScoOnApplied) {
-            if (AudioService.DEBUG_COMM_RTE) {
-                Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothScoOn: "
-                        + mBluetoothScoOn + ", mBluetoothScoOnApplied: " + mBluetoothScoOnApplied);
-            }
-            if (mBluetoothScoOn) {
-                if (!mBluetoothA2dpSuspendedApplied) {
-                    AudioSystem.setParameters("A2dpSuspended=true");
-                    mBluetoothA2dpSuspendedApplied = true;
-                }
-                if (!mBluetoothLeSuspendedApplied) {
-                    AudioSystem.setParameters("LeAudioSuspended=true");
-                    mBluetoothLeSuspendedApplied = true;
-                }
-                AudioSystem.setParameters("BT_SCO=on");
-            } else {
-                AudioSystem.setParameters("BT_SCO=off");
-            }
-            mBluetoothScoOnApplied = mBluetoothScoOn;
-        }
-        if (!mBluetoothScoOnApplied) {
-            if ((mBluetoothA2dpSuspendedExt || mBluetoothA2dpSuspendedInt)
-                    != mBluetoothA2dpSuspendedApplied) {
-                if (AudioService.DEBUG_COMM_RTE) {
-                    Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothA2dpSuspendedExt: "
-                            + mBluetoothA2dpSuspendedExt
-                            + ", mBluetoothA2dpSuspendedInt: " + mBluetoothA2dpSuspendedInt
-                            + ", mBluetoothA2dpSuspendedApplied: "
-                            + mBluetoothA2dpSuspendedApplied);
-                }
-                mBluetoothA2dpSuspendedApplied =
-                        mBluetoothA2dpSuspendedExt || mBluetoothA2dpSuspendedInt;
-                if (mBluetoothA2dpSuspendedApplied) {
-                    AudioSystem.setParameters("A2dpSuspended=true");
-                } else {
-                    AudioSystem.setParameters("A2dpSuspended=false");
-                }
-            }
-            if ((mBluetoothLeSuspendedExt || mBluetoothLeSuspendedInt)
-                    != mBluetoothLeSuspendedApplied) {
-                if (AudioService.DEBUG_COMM_RTE) {
-                    Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothLeSuspendedExt: "
-                            + mBluetoothLeSuspendedExt
-                            + ", mBluetoothLeSuspendedInt: " + mBluetoothLeSuspendedInt
-                            + ", mBluetoothLeSuspendedApplied: " + mBluetoothLeSuspendedApplied);
-                }
-                mBluetoothLeSuspendedApplied =
-                        mBluetoothLeSuspendedExt || mBluetoothLeSuspendedInt;
-                if (mBluetoothLeSuspendedApplied) {
-                    AudioSystem.setParameters("LeAudioSuspended=true");
-                } else {
-                    AudioSystem.setParameters("LeAudioSuspended=false");
-                }
-            }
-        }
-    }
 
     /*package*/ void setBluetoothScoOn(boolean on, String eventSource) {
         if (AudioService.DEBUG_COMM_RTE) {
@@ -966,76 +875,10 @@
         }
         synchronized (mDeviceStateLock) {
             mBluetoothScoOn = on;
-            updateAudioHalBluetoothState();
             postUpdateCommunicationRouteClient(eventSource);
         }
     }
 
-    /*package*/ void postSetA2dpSuspended(boolean enable, String eventSource) {
-        sendILMsgNoDelay(MSG_IL_SET_A2DP_SUSPENDED, SENDMSG_QUEUE, (enable ? 1 : 0), eventSource);
-    }
-
-    /*package*/ void setA2dpSuspended(boolean enable, boolean internal, String eventSource) {
-        if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "setA2dpSuspended source: " + eventSource + ", enable: "
-                    + enable + ", internal: " + internal
-                    + ", mBluetoothA2dpSuspendedInt: " + mBluetoothA2dpSuspendedInt
-                    + ", mBluetoothA2dpSuspendedExt: " + mBluetoothA2dpSuspendedExt);
-        }
-        synchronized (mDeviceStateLock) {
-            if (internal) {
-                mBluetoothA2dpSuspendedInt = enable;
-            } else {
-                mBluetoothA2dpSuspendedExt = enable;
-            }
-            updateAudioHalBluetoothState();
-        }
-    }
-
-    /*package*/ void clearA2dpSuspended() {
-        if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "clearA2dpSuspended");
-        }
-        synchronized (mDeviceStateLock) {
-            mBluetoothA2dpSuspendedInt = false;
-            mBluetoothA2dpSuspendedExt = false;
-            updateAudioHalBluetoothState();
-        }
-    }
-
-    /*package*/ void postSetLeAudioSuspended(boolean enable, String eventSource) {
-        sendILMsgNoDelay(
-                MSG_IL_SET_LEAUDIO_SUSPENDED, SENDMSG_QUEUE, (enable ? 1 : 0), eventSource);
-    }
-
-    /*package*/ void setLeAudioSuspended(boolean enable, boolean internal, String eventSource) {
-        if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "setLeAudioSuspended source: " + eventSource + ", enable: "
-                    + enable + ", internal: " + internal
-                    + ", mBluetoothLeSuspendedInt: " + mBluetoothA2dpSuspendedInt
-                    + ", mBluetoothLeSuspendedExt: " + mBluetoothA2dpSuspendedExt);
-        }
-        synchronized (mDeviceStateLock) {
-            if (internal) {
-                mBluetoothLeSuspendedInt = enable;
-            } else {
-                mBluetoothLeSuspendedExt = enable;
-            }
-            updateAudioHalBluetoothState();
-        }
-    }
-
-    /*package*/ void clearLeAudioSuspended() {
-        if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "clearLeAudioSuspended");
-        }
-        synchronized (mDeviceStateLock) {
-            mBluetoothLeSuspendedInt = false;
-            mBluetoothLeSuspendedExt = false;
-            updateAudioHalBluetoothState();
-        }
-    }
-
     /*package*/ AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) {
         synchronized (mDeviceStateLock) {
             return mDeviceInventory.startWatchingRoutes(observer);
@@ -1821,12 +1664,6 @@
                     final int capturePreset = msg.arg1;
                     mDeviceInventory.onSaveClearPreferredDevicesForCapturePreset(capturePreset);
                 } break;
-                case MSG_IL_SET_A2DP_SUSPENDED: {
-                    setA2dpSuspended((msg.arg1 == 1), false /*internal*/, (String) msg.obj);
-                } break;
-                case MSG_IL_SET_LEAUDIO_SUSPENDED: {
-                    setLeAudioSuspended((msg.arg1 == 1), false /*internal*/, (String) msg.obj);
-                } break;
                 case MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED: {
                     final BluetoothDevice btDevice = (BluetoothDevice) msg.obj;
                     BtHelper.onNotifyPreferredAudioProfileApplied(btDevice);
@@ -1905,8 +1742,6 @@
     private static final int MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY = 47;
     private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48;
     private static final int MSG_IL_BTLEAUDIO_TIMEOUT = 49;
-    private static final int MSG_IL_SET_A2DP_SUSPENDED = 50;
-    private static final int MSG_IL_SET_LEAUDIO_SUSPENDED = 51;
 
     private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52;
 
@@ -2185,6 +2020,12 @@
                 "updateCommunicationRoute, preferredCommunicationDevice: "
                 + preferredCommunicationDevice + " eventSource: " + eventSource)));
 
+        if (preferredCommunicationDevice == null
+                || preferredCommunicationDevice.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
+            AudioSystem.setParameters("BT_SCO=off");
+        } else {
+            AudioSystem.setParameters("BT_SCO=on");
+        }
         if (preferredCommunicationDevice == null) {
             AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice();
             if (defaultDevice != null) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 1d8bef1..b70c3e4 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -1478,7 +1478,7 @@
         }
 
         // Reset A2DP suspend state each time a new sink is connected
-        mDeviceBroker.clearA2dpSuspended();
+        mAudioSystem.setParameters("A2dpSuspended=false");
 
         // The convention for head tracking sensors associated with A2DP devices is to
         // use a UUID derived from the MAC address as follows:
@@ -1751,8 +1751,7 @@
     private void makeA2dpDeviceUnavailableLater(String address, int delayMs) {
         // prevent any activity on the A2DP audio output to avoid unwanted
         // reconnection of the sink.
-        mDeviceBroker.setA2dpSuspended(
-                true /*enable*/, true /*internal*/, "makeA2dpDeviceUnavailableLater");
+        mAudioSystem.setParameters("A2dpSuspended=true");
         // retrieve DeviceInfo before removing device
         final String deviceKey =
                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
@@ -1899,7 +1898,7 @@
                         "LE Audio device addr=" + address + " now available").printLog(TAG));
             }
             // Reset LEA suspend state each time a new sink is connected
-            mDeviceBroker.clearLeAudioSuspended();
+            mAudioSystem.setParameters("LeAudioSuspended=false");
 
             UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada);
             mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address),
@@ -1954,8 +1953,7 @@
     private void makeLeAudioDeviceUnavailableLater(String address, int device, int delayMs) {
         // prevent any activity on the LEA output to avoid unwanted
         // reconnection of the sink.
-        mDeviceBroker.setLeAudioSuspended(
-                true /*enable*/, true /*internal*/, "makeLeAudioDeviceUnavailableLater");
+        mAudioSystem.setParameters("LeAudioSuspended=true");
         // the device will be made unavailable later, so consider it disconnected right away
         mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address));
         // send the delayed message to make the device unavailable later
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index a721873..24eba76 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -6412,26 +6412,6 @@
         mDeviceBroker.setBluetoothScoOn(on, eventSource);
     }
 
-    /** @see AudioManager#setA2dpSuspended(boolean) */
-    @android.annotation.EnforcePermission(android.Manifest.permission.BLUETOOTH_STACK)
-    public void setA2dpSuspended(boolean enable) {
-        super.setA2dpSuspended_enforcePermission();
-        final String eventSource = new StringBuilder("setA2dpSuspended(").append(enable)
-                .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
-                .append(Binder.getCallingPid()).toString();
-        mDeviceBroker.postSetA2dpSuspended(enable, eventSource);
-    }
-
-    /** @see AudioManager#setA2dpSuspended(boolean) */
-    @android.annotation.EnforcePermission(android.Manifest.permission.BLUETOOTH_STACK)
-    public void setLeAudioSuspended(boolean enable) {
-        super.setLeAudioSuspended_enforcePermission();
-        final String eventSource = new StringBuilder("setLeAudioSuspended(").append(enable)
-                .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
-                .append(Binder.getCallingPid()).toString();
-        mDeviceBroker.postSetLeAudioSuspended(enable, eventSource);
-    }
-
     /** @see AudioManager#isBluetoothScoOn()
      * Note that it doesn't report internal state, but state seen by apps (which may have
      * called setBluetoothScoOn() */
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index e46c3cc..bfa6c36e 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -445,8 +445,8 @@
     /*package*/ synchronized void resetBluetoothSco() {
         mScoAudioState = SCO_STATE_INACTIVE;
         broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
-        mDeviceBroker.clearA2dpSuspended();
-        mDeviceBroker.clearLeAudioSuspended();
+        AudioSystem.setParameters("A2dpSuspended=false");
+        AudioSystem.setParameters("LeAudioSuspended=false");
         mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco");
     }
 
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/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index c678a92..4208a12 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -3677,8 +3677,11 @@
                     encapType = IkeSessionParams.ESP_ENCAP_TYPE_NONE;
                     break;
                 default:
-                    ipVersion = IkeSessionParams.ESP_IP_VERSION_AUTO;
-                    encapType = IkeSessionParams.ESP_ENCAP_TYPE_AUTO;
+                    // By default, PREFERRED_IKE_PROTOCOL_IPV4_UDP is used for safety. This is
+                    // because some carriers' networks do not support IPv6 very well, and using
+                    // IPv4 can help to prevent problems.
+                    ipVersion = IkeSessionParams.ESP_IP_VERSION_IPV4;
+                    encapType = IkeSessionParams.ESP_ENCAP_TYPE_UDP;
                     break;
             }
             return new CarrierConfigInfo(mccMnc, natKeepalive, encapType, ipVersion);
diff --git a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
index d7563e0..d764ec4 100644
--- a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
+++ b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
@@ -17,11 +17,14 @@
 package com.android.server.hdmi;
 
 /**
- * Action to query and track the audio status of the System Audio device when enabling or using
- * Absolute Volume Control. Must be removed when AVC is disabled. Performs two main functions:
- * 1. When enabling AVC: queries the starting audio status of the System Audio device and
- *    enables the feature upon receiving a response.
- * 2. While AVC is enabled: monitors <Report Audio Status> messages from the System Audio device and
+ * Action to query and track the audio status of the System Audio device when using
+ * absolute volume behavior, or adjust-only absolute volume behavior. Must be removed when
+ * neither behavior is used.
+ *
+ * Performs two main functions:
+ * 1. When enabling AVB: queries the starting audio status of the System Audio device and
+ *    adopts the appropriate volume behavior upon receiving a response.
+ * 2. While AVB is enabled: monitors <Report Audio Status> messages from the System Audio device and
  *    notifies AudioService if the audio status changes.
  */
 final class AbsoluteVolumeAudioStatusAction extends HdmiCecFeatureAction {
@@ -74,16 +77,23 @@
 
         boolean mute = HdmiUtils.isAudioStatusMute(cmd);
         int volume = HdmiUtils.getAudioStatusVolume(cmd);
+
+        // If the volume is out of range, report it as handled and ignore the message.
+        // According to the spec, such values are either reserved or indicate an unknown volume.
+        if (volume == Constants.UNKNOWN_VOLUME) {
+            return true;
+        }
+
         AudioStatus audioStatus = new AudioStatus(volume, mute);
         if (mState == STATE_WAIT_FOR_INITIAL_AUDIO_STATUS) {
-            localDevice().getService().enableAbsoluteVolumeControl(audioStatus);
+            localDevice().getService().enableAbsoluteVolumeBehavior(audioStatus);
             mState = STATE_MONITOR_AUDIO_STATUS;
         } else if (mState == STATE_MONITOR_AUDIO_STATUS) {
             if (audioStatus.getVolume() != mLastAudioStatus.getVolume()) {
-                localDevice().getService().notifyAvcVolumeChange(audioStatus.getVolume());
+                localDevice().getService().notifyAvbVolumeChange(audioStatus.getVolume());
             }
             if (audioStatus.getMute() != mLastAudioStatus.getMute()) {
-                localDevice().getService().notifyAvcMuteChange(audioStatus.getMute());
+                localDevice().getService().notifyAvbMuteChange(audioStatus.getMute());
             }
         }
         mLastAudioStatus = audioStatus;
diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
index 438c1ea..9484204 100644
--- a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
+++ b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
@@ -16,9 +16,11 @@
 
 package com.android.server.hdmi;
 
+import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener;
+import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
-import android.content.Context;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceVolumeManager;
 import android.media.VolumeInfo;
@@ -26,42 +28,48 @@
 import java.util.concurrent.Executor;
 
 /**
- * Wrapper for {@link AudioDeviceVolumeManager}. Creates an instance of the class and directly
- * passes method calls to that instance.
+ * Interface with the methods from {@link AudioDeviceVolumeManager} used by the HDMI framework.
+ * Allows the class to be faked for tests.
+ *
+ * See implementations {@link DefaultAudioDeviceVolumeManagerWrapper} and
+ * {@link FakeAudioFramework.FakeAudioDeviceVolumeManagerWrapper}.
  */
-public class AudioDeviceVolumeManagerWrapper
-        implements AudioDeviceVolumeManagerWrapperInterface {
+public interface AudioDeviceVolumeManagerWrapper {
 
-    private static final String TAG = "AudioDeviceVolumeManagerWrapper";
-
-    private final AudioDeviceVolumeManager mAudioDeviceVolumeManager;
-
-    public AudioDeviceVolumeManagerWrapper(Context context) {
-        mAudioDeviceVolumeManager = new AudioDeviceVolumeManager(context);
-    }
-
-    @Override
-    public void addOnDeviceVolumeBehaviorChangedListener(
+    /**
+     * Wrapper for {@link AudioDeviceVolumeManager#addOnDeviceVolumeBehaviorChangedListener(
+     * Executor, OnDeviceVolumeBehaviorChangedListener)}
+     */
+    void addOnDeviceVolumeBehaviorChangedListener(
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener)
-            throws SecurityException {
-        mAudioDeviceVolumeManager.addOnDeviceVolumeBehaviorChangedListener(executor, listener);
-    }
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener);
 
-    @Override
-    public void removeOnDeviceVolumeBehaviorChangedListener(
-            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener) {
-        mAudioDeviceVolumeManager.removeOnDeviceVolumeBehaviorChangedListener(listener);
-    }
+    /**
+     * Wrapper for {@link AudioDeviceVolumeManager#removeOnDeviceVolumeBehaviorChangedListener(
+     * OnDeviceVolumeBehaviorChangedListener)}
+     */
+    void removeOnDeviceVolumeBehaviorChangedListener(
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener);
 
-    @Override
-    public void setDeviceAbsoluteVolumeBehavior(
+    /**
+     * Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior(
+     * AudioDeviceAttributes, VolumeInfo, Executor, OnAudioDeviceVolumeChangedListener, boolean)}
+     */
+    void setDeviceAbsoluteVolumeBehavior(
             @NonNull AudioDeviceAttributes device,
             @NonNull VolumeInfo volume,
             @NonNull @CallbackExecutor Executor executor,
             @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
-            boolean handlesVolumeAdjustment) {
-        mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, executor,
-                vclistener, handlesVolumeAdjustment);
-    }
+            boolean handlesVolumeAdjustment);
+
+    /**
+     * Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeAdjustOnlyBehavior(
+     * AudioDeviceAttributes, VolumeInfo, Executor, OnAudioDeviceVolumeChangedListener, boolean)}
+     */
+    void setDeviceAbsoluteVolumeAdjustOnlyBehavior(
+            @NonNull AudioDeviceAttributes device,
+            @NonNull VolumeInfo volume,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
+            boolean handlesVolumeAdjustment);
 }
diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java
deleted file mode 100644
index 1a1d4c1..0000000
--- a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2022 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.hdmi;
-
-import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener;
-import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener;
-
-import android.annotation.CallbackExecutor;
-import android.annotation.NonNull;
-import android.media.AudioDeviceAttributes;
-import android.media.AudioDeviceVolumeManager;
-import android.media.VolumeInfo;
-
-import java.util.concurrent.Executor;
-
-/**
- * Interface with the methods from {@link AudioDeviceVolumeManager} used by the HDMI framework.
- * Allows the class to be faked for tests.
- */
-public interface AudioDeviceVolumeManagerWrapperInterface {
-
-    /**
-     * Wrapper for {@link AudioDeviceVolumeManager#addOnDeviceVolumeBehaviorChangedListener(
-     * Executor, OnDeviceVolumeBehaviorChangedListener)}
-     */
-    void addOnDeviceVolumeBehaviorChangedListener(
-            @NonNull @CallbackExecutor Executor executor,
-            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener);
-
-    /**
-     * Wrapper for {@link AudioDeviceVolumeManager#removeOnDeviceVolumeBehaviorChangedListener(
-     * OnDeviceVolumeBehaviorChangedListener)}
-     */
-    void removeOnDeviceVolumeBehaviorChangedListener(
-            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener);
-
-    /**
-     * Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior(
-     * AudioDeviceAttributes, VolumeInfo, Executor, OnAudioDeviceVolumeChangedListener, boolean)}
-     */
-    void setDeviceAbsoluteVolumeBehavior(
-            @NonNull AudioDeviceAttributes device,
-            @NonNull VolumeInfo volume,
-            @NonNull @CallbackExecutor Executor executor,
-            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
-            boolean handlesVolumeAdjustment);
-}
diff --git a/services/core/java/com/android/server/hdmi/AudioManagerWrapper.java b/services/core/java/com/android/server/hdmi/AudioManagerWrapper.java
new file mode 100644
index 0000000..fd4dd51
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/AudioManagerWrapper.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.hdmi;
+
+import android.annotation.NonNull;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceVolumeManager;
+import android.media.AudioManager;
+
+import java.util.List;
+
+/**
+ * Interface with the methods from {@link AudioDeviceVolumeManager} used by the HDMI control
+ * framework. Allows the class to be faked for tests.
+ *
+ * See implementations {@link DefaultAudioManagerWrapper} and
+ * {@link FakeAudioFramework.FakeAudioManagerWrapper}.
+ */
+public interface AudioManagerWrapper {
+
+    /**
+     * Wraps {@link AudioManager#adjustStreamVolume(int, int, int)}
+     */
+    void adjustStreamVolume(int streamType, int direction,
+            @AudioManager.PublicVolumeFlags int flags);
+
+    /**
+     * Wraps {@link AudioManager#setStreamVolume(int, int, int)}
+     */
+    void setStreamVolume(int streamType, int index, @AudioManager.PublicVolumeFlags int flags);
+
+    /**
+     * Wraps {@link AudioManager#getStreamVolume(int)}
+     */
+    int getStreamVolume(int streamType);
+
+    /**
+     * Wraps {@link AudioManager#getStreamMinVolume(int)}
+     */
+    int getStreamMinVolume(int streamType);
+
+    /**
+     * Wraps {@link AudioManager#getStreamMaxVolume(int)}
+     */
+    int getStreamMaxVolume(int streamType);
+
+    /**
+     * Wraps {@link AudioManager#isStreamMute(int)}
+     */
+    boolean isStreamMute(int streamType);
+
+    /**
+     * Wraps {@link AudioManager#setStreamMute(int, boolean)}
+     */
+    void setStreamMute(int streamType, boolean state);
+
+    /**
+     * Wraps {@link AudioManager#setHdmiSystemAudioSupported(boolean)}
+     */
+    int setHdmiSystemAudioSupported(boolean on);
+
+    /**
+     * Wraps {@link AudioManager#setWiredDeviceConnectionState(AudioDeviceAttributes, int)}
+     */
+    void setWiredDeviceConnectionState(AudioDeviceAttributes attributes, int state);
+
+    /**
+     * Wraps {@link AudioManager#setWiredDeviceConnectionState(int, int, String, String)}
+     */
+    void setWiredDeviceConnectionState(int device, int state, String address, String name);
+
+    /**
+     * Wraps {@link AudioManager#getDeviceVolumeBehavior(AudioDeviceAttributes)}
+     */
+    @AudioManager.DeviceVolumeBehavior
+    int getDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device);
+
+    /**
+     * Wraps {@link AudioManager#setDeviceVolumeBehavior(AudioDeviceAttributes, int)}
+     */
+    void setDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device,
+            @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior);
+
+    /**
+     * Wraps {@link AudioManager#getDevicesForAttributes(AudioAttributes)}
+     */
+    @NonNull
+    List<AudioDeviceAttributes> getDevicesForAttributes(
+            @NonNull AudioAttributes attributes);
+}
diff --git a/services/core/java/com/android/server/hdmi/AudioStatus.java b/services/core/java/com/android/server/hdmi/AudioStatus.java
index a884ffb..6242c45 100644
--- a/services/core/java/com/android/server/hdmi/AudioStatus.java
+++ b/services/core/java/com/android/server/hdmi/AudioStatus.java
@@ -23,6 +23,8 @@
 /**
  * Immutable representation of the information in the [Audio Status] operand:
  * volume status (0 <= N <= 100) and mute status (muted or unmuted).
+ * The volume level is limited to the range [0, 100] upon construction.
+ * This object cannot represent an audio status where the volume is unknown, or out of bounds.
  */
 public class AudioStatus {
     public static final int MAX_VOLUME = 100;
@@ -32,7 +34,7 @@
     boolean mMute;
 
     public AudioStatus(int volume, boolean mute) {
-        mVolume = volume;
+        mVolume = Math.max(Math.min(volume, MAX_VOLUME), MIN_VOLUME);
         mMute = mute;
     }
 
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index c235299..090d728 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -631,10 +631,14 @@
     static final String DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX = "enable_earc_tx";
     static final String DEVICE_CONFIG_FEATURE_FLAG_TRANSITION_ARC_TO_EARC_TX =
             "transition_arc_to_earc_tx";
+    // Name is abbreviated slightly to avoid line length issues
+    static final String DEVICE_CONFIG_FEATURE_FLAG_TV_NUMERIC_SOUNDBAR_VOLUME_UI =
+            "enable_numeric_soundbar_volume_ui_on_tv";
     @StringDef({
             DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE,
             DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX,
-            DEVICE_CONFIG_FEATURE_FLAG_TRANSITION_ARC_TO_EARC_TX
+            DEVICE_CONFIG_FEATURE_FLAG_TRANSITION_ARC_TO_EARC_TX,
+            DEVICE_CONFIG_FEATURE_FLAG_TV_NUMERIC_SOUNDBAR_VOLUME_UI
     })
     @interface FeatureFlag {}
 
diff --git a/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java
new file mode 100644
index 0000000..ff99ace
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.hdmi;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceVolumeManager;
+import android.media.VolumeInfo;
+
+import java.util.concurrent.Executor;
+
+/**
+ * "Default" wrapper for {@link AudioDeviceVolumeManager}, as opposed to a "Fake" wrapper for
+ * testing - see {@link FakeAudioFramework.FakeAudioDeviceVolumeManagerWrapper}.
+ *
+ * Creates an instance of {@link AudioDeviceVolumeManager} and directly passes method calls
+ * to that instance.
+ */
+public class DefaultAudioDeviceVolumeManagerWrapper
+        implements AudioDeviceVolumeManagerWrapper {
+
+    private static final String TAG = "AudioDeviceVolumeManagerWrapper";
+
+    private final AudioDeviceVolumeManager mAudioDeviceVolumeManager;
+
+    public DefaultAudioDeviceVolumeManagerWrapper(Context context) {
+        mAudioDeviceVolumeManager = new AudioDeviceVolumeManager(context);
+    }
+
+    @Override
+    public void addOnDeviceVolumeBehaviorChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener)
+            throws SecurityException {
+        mAudioDeviceVolumeManager.addOnDeviceVolumeBehaviorChangedListener(executor, listener);
+    }
+
+    @Override
+    public void removeOnDeviceVolumeBehaviorChangedListener(
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener) {
+        mAudioDeviceVolumeManager.removeOnDeviceVolumeBehaviorChangedListener(listener);
+    }
+
+    @Override
+    public void setDeviceAbsoluteVolumeBehavior(
+            @NonNull AudioDeviceAttributes device,
+            @NonNull VolumeInfo volume,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
+            boolean handlesVolumeAdjustment) {
+        mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, executor,
+                vclistener, handlesVolumeAdjustment);
+    }
+
+    @Override
+    public void setDeviceAbsoluteVolumeAdjustOnlyBehavior(
+            @NonNull AudioDeviceAttributes device,
+            @NonNull VolumeInfo volume,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
+            boolean handlesVolumeAdjustment) {
+        mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeAdjustOnlyBehavior(device, volume,
+                executor, vclistener, handlesVolumeAdjustment);
+    }
+}
diff --git a/services/core/java/com/android/server/hdmi/DefaultAudioManagerWrapper.java b/services/core/java/com/android/server/hdmi/DefaultAudioManagerWrapper.java
new file mode 100644
index 0000000..061e145
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/DefaultAudioManagerWrapper.java
@@ -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.server.hdmi;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioManager;
+
+import java.util.List;
+
+/**
+ * "Default" wrapper for {@link AudioManager}, as opposed to a "Fake" wrapper for testing -
+ * see {@link FakeAudioFramework.FakeAudioManagerWrapper}.
+ *
+ * Creates an instance of {@link AudioManager} and directly passes method calls to that instance.
+ *
+*/
+public class DefaultAudioManagerWrapper implements AudioManagerWrapper {
+
+    private static final String TAG = "DefaultAudioManagerWrapper";
+
+    private final AudioManager mAudioManager;
+
+    public DefaultAudioManagerWrapper(Context context) {
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+    }
+
+    @Override
+    public void adjustStreamVolume(int streamType, int direction,
+            @AudioManager.PublicVolumeFlags int flags) {
+        mAudioManager.adjustStreamVolume(streamType, direction, flags);
+    }
+
+    @Override
+    public void setStreamVolume(int streamType, int index,
+            @AudioManager.PublicVolumeFlags int flags) {
+        mAudioManager.setStreamVolume(streamType, index, flags);
+    }
+
+    @Override
+    public int getStreamVolume(int streamType) {
+        return mAudioManager.getStreamVolume(streamType);
+    }
+
+    @Override
+    public int getStreamMinVolume(int streamType) {
+        return mAudioManager.getStreamMinVolume(streamType);
+    }
+
+    @Override
+    public int getStreamMaxVolume(int streamType) {
+        return mAudioManager.getStreamMaxVolume(streamType);
+    }
+
+    @Override
+    public boolean isStreamMute(int streamType) {
+        return mAudioManager.isStreamMute(streamType);
+    }
+
+    @Override
+    public void setStreamMute(int streamType, boolean state) {
+        mAudioManager.setStreamMute(streamType, state);
+    }
+
+    @Override
+    public int setHdmiSystemAudioSupported(boolean on) {
+        return mAudioManager.setHdmiSystemAudioSupported(on);
+    }
+
+    @Override
+    public void setWiredDeviceConnectionState(AudioDeviceAttributes attributes, int state) {
+        mAudioManager.setWiredDeviceConnectionState(attributes, state);
+    }
+
+    @Override
+    public void setWiredDeviceConnectionState(int device, int state, String address, String name) {
+        mAudioManager.setWiredDeviceConnectionState(device, state, address, name);
+    }
+
+    @Override
+    @AudioManager.DeviceVolumeBehavior
+    public int getDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device) {
+        return mAudioManager.getDeviceVolumeBehavior(device);
+    }
+
+    @Override
+    public void setDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device,
+            @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior) {
+        mAudioManager.setDeviceVolumeBehavior(device, deviceVolumeBehavior);
+    }
+
+    @Override
+    @NonNull
+    public List<AudioDeviceAttributes> getDevicesForAttributes(
+            @NonNull AudioAttributes attributes) {
+        return mAudioManager.getDevicesForAttributes(attributes);
+    }
+
+}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index 35c70fb..f37ad5e 100755
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -1012,17 +1012,23 @@
         action.start();
     }
 
-    void addAvcAudioStatusAction(int targetAddress) {
+    @ServiceThreadOnly
+    void addAvbAudioStatusAction(int targetAddress) {
+        assertRunOnServiceThread();
         if (!hasAction(AbsoluteVolumeAudioStatusAction.class)) {
             addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress));
         }
     }
 
-    void removeAvcAudioStatusAction() {
+    @ServiceThreadOnly
+    void removeAvbAudioStatusAction() {
+        assertRunOnServiceThread();
         removeAction(AbsoluteVolumeAudioStatusAction.class);
     }
 
-    void updateAvcVolume(int volumeIndex) {
+    @ServiceThreadOnly
+    void updateAvbVolume(int volumeIndex) {
+        assertRunOnServiceThread();
         for (AbsoluteVolumeAudioStatusAction action :
                 getActions(AbsoluteVolumeAudioStatusAction.class)) {
             action.updateVolume(volumeIndex);
@@ -1035,7 +1041,7 @@
      * and send <Set Audio Volume Level> (to see if it gets a <Feature Abort> in response).
      */
     @ServiceThreadOnly
-    void queryAvcSupport(int targetAddress) {
+    void querySetAudioVolumeLevelSupport(int targetAddress) {
         assertRunOnServiceThread();
 
         // Send <Give Features> if using CEC 2.0 or above.
@@ -1054,7 +1060,7 @@
                             @Override
                             public void onComplete(int result) {
                                 if (result == HdmiControlManager.RESULT_SUCCESS) {
-                                    getService().checkAndUpdateAbsoluteVolumeControlState();
+                                    getService().checkAndUpdateAbsoluteVolumeBehavior();
                                 }
                             }
                         }));
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index f47c4b2..5ef06f9 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -66,7 +66,7 @@
 /**
  * Represent a logical device of type TV residing in Android system.
  */
-final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
+public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
     private static final String TAG = "HdmiCecLocalDeviceTv";
 
     // Whether ARC is available or not. "true" means that ARC is established between TV and
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
index 18a69c8..7045e65 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
@@ -259,7 +259,7 @@
             // The addition of a local device should not notify listeners
             return;
         }
-        mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
+        mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior();
         if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) {
             // Don't notify listeners of devices that haven't reported their physical address yet
             return;
@@ -384,7 +384,7 @@
     final void removeCecDevice(HdmiCecLocalDevice localDevice, int address) {
         assertRunOnServiceThread();
         HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address));
-        mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
+        mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior();
         localDevice.mCecMessageCache.flushMessagesFrom(address);
         if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) {
             // Don't notify listeners of devices that haven't reported their physical address yet
@@ -592,7 +592,7 @@
 
         updateCecDevice(newDeviceInfo);
 
-        mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
+        mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior();
     }
 
     @ServiceThreadOnly
@@ -625,7 +625,7 @@
                     .build();
             updateCecDevice(newDeviceInfo);
 
-            mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
+            mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior();
         }
     }
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 9cd5272..e87ed0a 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -231,8 +231,8 @@
             new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
                     AudioDeviceInfo.TYPE_HDMI_EARC, "");
 
-    // Audio output devices used for Absolute Volume Control
-    private static final List<AudioDeviceAttributes> AVC_AUDIO_OUTPUT_DEVICES =
+    // Audio output devices used for absolute volume behavior
+    private static final List<AudioDeviceAttributes> AVB_AUDIO_OUTPUT_DEVICES =
             Collections.unmodifiableList(Arrays.asList(AUDIO_OUTPUT_DEVICE_HDMI,
                     AUDIO_OUTPUT_DEVICE_HDMI_ARC, AUDIO_OUTPUT_DEVICE_HDMI_EARC));
 
@@ -265,7 +265,7 @@
     @HdmiControlManager.VolumeControl
     private int mHdmiCecVolumeControl;
 
-    // Caches the volume behaviors of all audio output devices in AVC_AUDIO_OUTPUT_DEVICES.
+    // Caches the volume behaviors of all audio output devices in AVB_AUDIO_OUTPUT_DEVICES.
     @GuardedBy("mLock")
     private Map<AudioDeviceAttributes, Integer> mAudioDeviceVolumeBehaviors = new HashMap<>();
 
@@ -458,6 +458,9 @@
     private boolean mEarcTxFeatureFlagEnabled = false;
 
     @ServiceThreadOnly
+    private boolean mNumericSoundbarVolumeUiOnTvFeatureFlagEnabled = false;
+
+    @ServiceThreadOnly
     private boolean mTransitionFromArcToEarcTxEnabled = false;
 
     @ServiceThreadOnly
@@ -491,10 +494,10 @@
     private PowerManagerInternalWrapper mPowerManagerInternal;
 
     @Nullable
-    private AudioManager mAudioManager;
+    private AudioManagerWrapper mAudioManager;
 
     @Nullable
-    private AudioDeviceVolumeManagerWrapperInterface mAudioDeviceVolumeManager;
+    private AudioDeviceVolumeManagerWrapper mAudioDeviceVolumeManager;
 
     @Nullable
     private Looper mIoLooper;
@@ -528,18 +531,20 @@
     /**
      * Constructor for testing.
      *
-     * It's critical to use a fake AudioDeviceVolumeManager because a normally instantiated
-     * AudioDeviceVolumeManager can access the "real" AudioService on the DUT.
+     * Takes fakes for AudioManager and AudioDeviceVolumeManager.
      *
-     * @see FakeAudioDeviceVolumeManagerWrapper
+     * This is especially important for AudioDeviceVolumeManager because a normally instantiated
+     * AudioDeviceVolumeManager can access the "real" AudioService on the DUT.
      */
     @VisibleForTesting HdmiControlService(Context context, List<Integer> deviceTypes,
-            AudioDeviceVolumeManagerWrapperInterface audioDeviceVolumeManager) {
+            AudioManagerWrapper audioManager,
+            AudioDeviceVolumeManagerWrapper audioDeviceVolumeManager) {
         super(context);
         mCecLocalDevices = deviceTypes;
         mSettingsObserver = new SettingsObserver(mHandler);
         mHdmiCecConfig = new HdmiCecConfig(context);
         mDeviceConfig = new DeviceConfigWrapper();
+        mAudioManager = audioManager;
         mAudioDeviceVolumeManager = audioDeviceVolumeManager;
     }
 
@@ -680,6 +685,8 @@
                 Constants.DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX, false);
         mTransitionFromArcToEarcTxEnabled = mDeviceConfig.getBoolean(
                 Constants.DEVICE_CONFIG_FEATURE_FLAG_TRANSITION_ARC_TO_EARC_TX, false);
+        mNumericSoundbarVolumeUiOnTvFeatureFlagEnabled = mDeviceConfig.getBoolean(
+                Constants.DEVICE_CONFIG_FEATURE_FLAG_TV_NUMERIC_SOUNDBAR_VOLUME_UI, false);
 
         synchronized (mLock) {
             mEarcEnabled = (mHdmiCecConfig.getIntValue(
@@ -899,6 +906,17 @@
                                 false);
                     }
                 });
+
+        mDeviceConfig.addOnPropertiesChangedListener(getContext().getMainExecutor(),
+                new DeviceConfig.OnPropertiesChangedListener() {
+                    @Override
+                    public void onPropertiesChanged(DeviceConfig.Properties properties) {
+                        mNumericSoundbarVolumeUiOnTvFeatureFlagEnabled = properties.getBoolean(
+                                Constants.DEVICE_CONFIG_FEATURE_FLAG_TV_NUMERIC_SOUNDBAR_VOLUME_UI,
+                                false);
+                        checkAndUpdateAbsoluteVolumeBehavior();
+                    }
+                });
     }
     /** Returns true if the device screen is off */
     boolean isScreenOff() {
@@ -934,11 +952,6 @@
     }
 
     @VisibleForTesting
-    void setAudioManager(AudioManager audioManager) {
-        mAudioManager = audioManager;
-    }
-
-    @VisibleForTesting
     void setCecController(HdmiCecController cecController) {
         mCecController = cecController;
     }
@@ -975,11 +988,13 @@
                     Context.TV_INPUT_SERVICE);
             mPowerManager = new PowerManagerWrapper(getContext());
             mPowerManagerInternal = new PowerManagerInternalWrapper();
-            mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+            if (mAudioManager == null) {
+                mAudioManager = new DefaultAudioManagerWrapper(getContext());
+            }
             mStreamMusicMaxVolume = getAudioManager().getStreamMaxVolume(AudioManager.STREAM_MUSIC);
             if (mAudioDeviceVolumeManager == null) {
                 mAudioDeviceVolumeManager =
-                        new AudioDeviceVolumeManagerWrapper(getContext());
+                        new DefaultAudioDeviceVolumeManagerWrapper(getContext());
             }
             getAudioDeviceVolumeManager().addOnDeviceVolumeBehaviorChangedListener(
                     mServiceThreadExecutor, this::onDeviceVolumeBehaviorChanged);
@@ -1773,7 +1788,7 @@
                 == HdmiControlManager.VOLUME_CONTROL_DISABLED) {
             return;
         }
-        AudioManager audioManager = getAudioManager();
+        AudioManagerWrapper audioManager = getAudioManager();
         boolean muted = audioManager.isStreamMute(AudioManager.STREAM_MUSIC);
         if (mute) {
             if (!muted) {
@@ -2751,7 +2766,7 @@
             pw.println("mPowerStatus: " + mPowerStatusController.getPowerStatus());
             pw.println("mIsCecAvailable: " + mIsCecAvailable);
             pw.println("mCecVersion: " + mCecVersion);
-            pw.println("mIsAbsoluteVolumeControlEnabled: " + isAbsoluteVolumeControlEnabled());
+            pw.println("mIsAbsoluteVolumeBehaviorEnabled: " + isAbsoluteVolumeBehaviorEnabled());
 
             // System settings
             pw.println("System_settings:");
@@ -2919,7 +2934,7 @@
             @HdmiControlManager.VolumeControl int hdmiCecVolumeControl) {
         mHdmiCecVolumeControl = hdmiCecVolumeControl;
         announceHdmiCecVolumeControlFeatureChange(hdmiCecVolumeControl);
-        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState);
+        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeBehavior);
     }
 
     // Get the source address to send out commands to devices connected to the current device
@@ -3485,7 +3500,7 @@
      * Returns null before the boot phase {@link SystemService#PHASE_SYSTEM_SERVICES_READY}.
      */
     @Nullable
-    AudioManager getAudioManager() {
+    AudioManagerWrapper getAudioManager() {
         return mAudioManager;
     }
 
@@ -3493,7 +3508,7 @@
      * Returns null before the boot phase {@link SystemService#PHASE_SYSTEM_SERVICES_READY}.
      */
     @Nullable
-    private AudioDeviceVolumeManagerWrapperInterface getAudioDeviceVolumeManager() {
+    private AudioDeviceVolumeManagerWrapper getAudioDeviceVolumeManager() {
         return mAudioDeviceVolumeManager;
     }
 
@@ -3882,7 +3897,7 @@
         synchronized (mLock) {
             mSystemAudioActivated = on;
         }
-        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState);
+        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeBehavior);
     }
 
     @ServiceThreadOnly
@@ -3993,7 +4008,7 @@
                     deviceIsActiveSource, caller);
         }
 
-        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState);
+        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeBehavior);
     }
 
     // This method should only be called when the device can be the active source
@@ -4189,17 +4204,17 @@
 
     /**
      * Listener for changes to the volume behavior of an audio output device. Caches the
-     * volume behavior of devices used for Absolute Volume Control.
+     * volume behavior of devices used for absolute volume behavior.
      */
     @VisibleForTesting
     @ServiceThreadOnly
     void onDeviceVolumeBehaviorChanged(AudioDeviceAttributes device, int volumeBehavior) {
         assertRunOnServiceThread();
-        if (AVC_AUDIO_OUTPUT_DEVICES.contains(device)) {
+        if (AVB_AUDIO_OUTPUT_DEVICES.contains(device)) {
             synchronized (mLock) {
                 mAudioDeviceVolumeBehaviors.put(device, volumeBehavior);
             }
-            checkAndUpdateAbsoluteVolumeControlState();
+            checkAndUpdateAbsoluteVolumeBehavior();
         }
     }
 
@@ -4209,7 +4224,7 @@
      */
     @AudioManager.DeviceVolumeBehavior
     private int getDeviceVolumeBehavior(AudioDeviceAttributes device) {
-        if (AVC_AUDIO_OUTPUT_DEVICES.contains(device)) {
+        if (AVB_AUDIO_OUTPUT_DEVICES.contains(device)) {
             synchronized (mLock) {
                 if (mAudioDeviceVolumeBehaviors.containsKey(device)) {
                     return mAudioDeviceVolumeBehaviors.get(device);
@@ -4220,25 +4235,29 @@
     }
 
     /**
-     * Returns whether Absolute Volume Control is enabled or not. This is determined by the
+     * Returns whether absolute volume behavior is enabled or not. This is determined by the
      * volume behavior of the relevant HDMI audio output device(s) for this device's type.
      */
-    public boolean isAbsoluteVolumeControlEnabled() {
+    public boolean isAbsoluteVolumeBehaviorEnabled() {
         if (!isTvDevice() && !isPlaybackDevice()) {
             return false;
         }
-        AudioDeviceAttributes avcAudioOutputDevice = getAvcAudioOutputDevice();
-        if (avcAudioOutputDevice == null) {
+        AudioDeviceAttributes avbAudioOutputDevice = getAvbAudioOutputDevice();
+        if (avbAudioOutputDevice == null) {
             return false;
         }
-        return getDeviceVolumeBehavior(avcAudioOutputDevice)
-                    == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE;
+
+        @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior =
+                getDeviceVolumeBehavior(avbAudioOutputDevice);
+
+        return deviceVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE
+                || deviceVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY;
     }
 
-    private AudioDeviceAttributes getAvcAudioOutputDevice() {
-        if (isTvDevice()) {
+    private AudioDeviceAttributes getAvbAudioOutputDevice() {
+        if (tv() != null) {
             return tv().getSystemAudioOutputDevice();
-        } else if (isPlaybackDevice()) {
+        } else if (playback() != null) {
             return AUDIO_OUTPUT_DEVICE_HDMI;
         } else {
             return null;
@@ -4246,30 +4265,30 @@
     }
 
     /**
-     * Checks the conditions for Absolute Volume Control (AVC), and enables or disables the feature
-     * if necessary. AVC is enabled precisely when a specific audio output device
-     * (HDMI for playback devices, and HDMI_ARC or HDMI_EARC for TVs) is using absolute volume
-     * behavior.
+     * This method is responsible for adopting or disabling absolute volume behavior and
+     * adjust-only absolute volume behavior in AudioService. These volume behaviors are adopted on
+     * specific audio output devices: HDMI for playback devices, and HDMI_ARC or HDMI_EARC for TVs.
      *
-     * AVC must be enabled on a Playback device or TV precisely when it is playing
-     * audio on an external device (the System Audio device) that supports the feature.
-     * This reduces to these conditions:
+     * This method enables absolute volume behavior on a Playback device or TV panel when it is
+     * playing audio on an external device (the System Audio device) that supports the feature.
+     * This allows the volume level of the System Audio device to be tracked and set by Android.
      *
+     * Absolute volume behavior requires the following conditions:
      * 1. If the System Audio Device is an Audio System: System Audio Mode is active
      * 2. Our HDMI audio output device is using full volume behavior
      * 3. CEC volume is enabled
-     * 4. The System Audio device supports AVC (i.e. it supports <Set Audio Volume Level>)
+     * 4. The System Audio device supports the <Set Audio Volume Level> message
      *
-     * If not all of these conditions are met, this method disables AVC if necessary.
-     *
-     * If all of these conditions are met, this method starts an action to query the System Audio
-     * device's audio status, which enables AVC upon obtaining the audio status.
+     * This method enables adjust-only absolute volume behavior on TV panels when conditions
+     * 1, 2, and 3 are met, but condition 4 is not. This allows TVs to track the volume level of
+     * the System Audio device and display numeric volume UI for it, even if the System Audio device
+     * does not support <Set Audio Volume Level>.
      */
     @ServiceThreadOnly
-    void checkAndUpdateAbsoluteVolumeControlState() {
+    void checkAndUpdateAbsoluteVolumeBehavior() {
         assertRunOnServiceThread();
 
-        // Can't enable or disable AVC before we have access to system services
+        // Can't set volume behavior before we have access to system services
         if (getAudioManager() == null) {
             return;
         }
@@ -4281,84 +4300,110 @@
             // (Doesn't apply to Playback Devices, where if SAM isn't active, we assume the
             // TV is the System Audio Device instead.)
             if (!isSystemAudioActivated()) {
-                disableAbsoluteVolumeControl();
+                switchToFullVolumeBehavior();
                 return;
             }
         } else if (isPlaybackDevice() && playback() != null) {
             localCecDevice = playback();
         } else {
-            // Either this device type doesn't support AVC, or it hasn't fully initialized yet
+            // Either this device type doesn't support AVB, or it hasn't fully initialized yet
             return;
         }
 
-        HdmiDeviceInfo systemAudioDeviceInfo = getHdmiCecNetwork().getSafeCecDeviceInfo(
+        HdmiDeviceInfo systemAudioDeviceInfo = getDeviceInfo(
                 localCecDevice.findAudioReceiverAddress());
         @AudioManager.DeviceVolumeBehavior int currentVolumeBehavior =
-                        getDeviceVolumeBehavior(getAvcAudioOutputDevice());
+                        getDeviceVolumeBehavior(getAvbAudioOutputDevice());
 
         // Condition 2: Already using full or absolute volume behavior
         boolean alreadyUsingFullOrAbsoluteVolume =
-                currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL
-                        || currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE;
+                (currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL)
+                        || (currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE)
+                        || (currentVolumeBehavior
+                        == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY);
         // Condition 3: CEC volume is enabled
         boolean cecVolumeEnabled =
                 getHdmiCecVolumeControl() == HdmiControlManager.VOLUME_CONTROL_ENABLED;
 
         if (!cecVolumeEnabled || !alreadyUsingFullOrAbsoluteVolume) {
-            disableAbsoluteVolumeControl();
+            switchToFullVolumeBehavior();
             return;
         }
 
-        // Check for safety: if the System Audio device is a candidate for AVC, we should already
+        // Check for safety: if the System Audio device is a candidate for AVB, we should already
         // have received messages from it to trigger the other conditions.
         if (systemAudioDeviceInfo == null) {
-            disableAbsoluteVolumeControl();
+            switchToFullVolumeBehavior();
             return;
         }
-        // Condition 4: The System Audio device supports AVC (i.e. <Set Audio Volume Level>).
+
+        // Condition 4: The System Audio device supports <Set Audio Volume Level>
         switch (systemAudioDeviceInfo.getDeviceFeatures().getSetAudioVolumeLevelSupport()) {
             case DeviceFeatures.FEATURE_SUPPORTED:
-                if (!isAbsoluteVolumeControlEnabled()) {
-                    // Start an action that will call {@link #enableAbsoluteVolumeControl}
+                if (currentVolumeBehavior != AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE) {
+                    // If we're currently using adjust-only absolute volume behavior, switch to
+                    // full volume behavior until we successfully adopt absolute volume behavior
+                    switchToFullVolumeBehavior();
+                    // Start an action that will call enableAbsoluteVolumeBehavior
                     // once the System Audio device sends <Report Audio Status>
-                    localCecDevice.addAvcAudioStatusAction(
+                    localCecDevice.addAvbAudioStatusAction(
                             systemAudioDeviceInfo.getLogicalAddress());
                 }
                 return;
             case DeviceFeatures.FEATURE_NOT_SUPPORTED:
-                disableAbsoluteVolumeControl();
+                // TVs may adopt adjust-only absolute volume behavior if condition 4 isn't met.
+                // This allows the device to display numeric volume UI for the System Audio device.
+                if (tv() != null && mNumericSoundbarVolumeUiOnTvFeatureFlagEnabled) {
+                    if (currentVolumeBehavior
+                            != AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY) {
+                        // If we're currently using absolute volume behavior, switch to full volume
+                        // behavior until we successfully adopt adjust-only absolute volume behavior
+                        switchToFullVolumeBehavior();
+                        // Start an action that will call enableAbsoluteVolumeBehavior
+                        // once the System Audio device sends <Report Audio Status>
+                        localCecDevice.addAvbAudioStatusAction(
+                                systemAudioDeviceInfo.getLogicalAddress());
+                    }
+                } else {
+                    switchToFullVolumeBehavior();
+                }
                 return;
             case DeviceFeatures.FEATURE_SUPPORT_UNKNOWN:
-                disableAbsoluteVolumeControl();
-                localCecDevice.queryAvcSupport(systemAudioDeviceInfo.getLogicalAddress());
-                return;
-            default:
-                return;
+                switchToFullVolumeBehavior();
+                localCecDevice.querySetAudioVolumeLevelSupport(
+                        systemAudioDeviceInfo.getLogicalAddress());
         }
     }
 
-    private void disableAbsoluteVolumeControl() {
-        if (isPlaybackDevice()) {
-            playback().removeAvcAudioStatusAction();
-        } else if (isTvDevice()) {
-            tv().removeAvcAudioStatusAction();
+    /**
+     * Switches to full volume behavior, if either absolute or adjust-only absolute volume behavior
+     * are currently used. Removes the action for handling volume updates for these behaviors.
+     */
+    private void switchToFullVolumeBehavior() {
+        if (playback() != null) {
+            playback().removeAvbAudioStatusAction();
+        } else if (tv() != null) {
+            tv().removeAvbAudioStatusAction();
         }
-        AudioDeviceAttributes device = getAvcAudioOutputDevice();
-        if (getDeviceVolumeBehavior(device) == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE) {
+        AudioDeviceAttributes device = getAvbAudioOutputDevice();
+        int volumeBehavior = getDeviceVolumeBehavior(device);
+        if (volumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE
+                || volumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY) {
             getAudioManager().setDeviceVolumeBehavior(device,
                     AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
         }
     }
 
     /**
-     * Enables Absolute Volume Control. Should only be called when all the conditions for
-     * AVC are met (see {@link #checkAndUpdateAbsoluteVolumeControlState}).
+     * Enables absolute volume behavior or adjust-only absolute volume behavior. Should only be
+     * called when the conditions for one of these behaviors is met -
+     * see {@link #checkAndUpdateAbsoluteVolumeBehavior}.
+     *
      * @param audioStatus The initial audio status to set the audio output device to
      */
-    void enableAbsoluteVolumeControl(AudioStatus audioStatus) {
+    void enableAbsoluteVolumeBehavior(AudioStatus audioStatus) {
         HdmiCecLocalDevice localDevice = isPlaybackDevice() ? playback() : tv();
-        HdmiDeviceInfo systemAudioDevice = getHdmiCecNetwork().getDeviceInfo(
-                localDevice.findAudioReceiverAddress());
+        HdmiDeviceInfo systemAudioDevice = getDeviceInfo(localDevice.findAudioReceiverAddress());
         VolumeInfo volumeInfo = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
                 .setMuted(audioStatus.getMute())
                 .setVolumeIndex(audioStatus.getVolume())
@@ -4370,10 +4415,21 @@
 
         // AudioService sets the volume of the stream and device based on the input VolumeInfo
         // when enabling absolute volume behavior, but not the mute state
-        notifyAvcMuteChange(audioStatus.getMute());
-        getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior(
-                getAvcAudioOutputDevice(), volumeInfo, mServiceThreadExecutor,
-                mAbsoluteVolumeChangedListener, true);
+        notifyAvbMuteChange(audioStatus.getMute());
+
+        // If <Set Audio Volume Level> is supported, enable absolute volume behavior.
+        // Otherwise, enable adjust-only AVB on TVs only.
+        if (systemAudioDevice.getDeviceFeatures().getSetAudioVolumeLevelSupport()
+                == DeviceFeatures.FEATURE_SUPPORTED) {
+            getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior(
+                    getAvbAudioOutputDevice(), volumeInfo, mServiceThreadExecutor,
+                    mAbsoluteVolumeChangedListener, true);
+        } else if (tv() != null) {
+            getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeAdjustOnlyBehavior(
+                    getAvbAudioOutputDevice(), volumeInfo, mServiceThreadExecutor,
+                    mAbsoluteVolumeChangedListener, true);
+        }
+
     }
 
     private AbsoluteVolumeChangedListener mAbsoluteVolumeChangedListener;
@@ -4407,6 +4463,14 @@
         public void onAudioDeviceVolumeChanged(
                 @NonNull AudioDeviceAttributes audioDevice,
                 @NonNull VolumeInfo volumeInfo) {
+
+            // Do nothing if the System Audio device does not support <Set Audio Volume Level>
+            if (mSystemAudioDevice.getDeviceFeatures().getSetAudioVolumeLevelSupport()
+                    != DeviceFeatures.FEATURE_SUPPORTED) {
+                return;
+            }
+
+            // Send <Set Audio Volume Level> to notify the System Audio device of the volume change
             int localDeviceAddress = mLocalDevice.getDeviceInfo().getLogicalAddress();
             sendCecCommand(SetAudioVolumeLevelMessage.build(
                             localDeviceAddress,
@@ -4418,8 +4482,8 @@
                         if (errorCode == SendMessageResult.SUCCESS) {
                             // Update the volume tracked in our AbsoluteVolumeAudioStatusAction
                             // so it correctly processes incoming <Report Audio Status> messages
-                            HdmiCecLocalDevice avcDevice = isTvDevice() ? tv() : playback();
-                            avcDevice.updateAvcVolume(volumeInfo.getVolumeIndex());
+                            HdmiCecLocalDevice avbDevice = isTvDevice() ? tv() : playback();
+                            avbDevice.updateAvbVolume(volumeInfo.getVolumeIndex());
                         } else {
                             sendCecCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(
                                     localDeviceAddress,
@@ -4477,13 +4541,13 @@
 
     /**
      * Notifies AudioService of a change in the volume of the System Audio device. Has no effect if
-     * AVC is disabled, or the audio output device for AVC is not playing for STREAM_MUSIC
+     * AVB is disabled, or the audio output device for AVB is not playing for STREAM_MUSIC
      */
-    void notifyAvcVolumeChange(int volume) {
-        if (!isAbsoluteVolumeControlEnabled()) return;
+    void notifyAvbVolumeChange(int volume) {
+        if (!isAbsoluteVolumeBehaviorEnabled()) return;
         List<AudioDeviceAttributes> streamMusicDevices =
                 getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES);
-        if (streamMusicDevices.contains(getAvcAudioOutputDevice())) {
+        if (streamMusicDevices.contains(getAvbAudioOutputDevice())) {
             int flags = AudioManager.FLAG_ABSOLUTE_VOLUME;
             if (isTvDevice()) {
                 flags |= AudioManager.FLAG_SHOW_UI;
@@ -4494,13 +4558,13 @@
 
     /**
      * Notifies AudioService of a change in the mute status of the System Audio device. Has no
-     * effect if AVC is disabled, or the audio output device for AVC is not playing for STREAM_MUSIC
+     * effect if AVB is disabled, or the audio output device for AVB is not playing for STREAM_MUSIC
      */
-    void notifyAvcMuteChange(boolean mute) {
-        if (!isAbsoluteVolumeControlEnabled()) return;
+    void notifyAvbMuteChange(boolean mute) {
+        if (!isAbsoluteVolumeBehaviorEnabled()) return;
         List<AudioDeviceAttributes> streamMusicDevices =
                 getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES);
-        if (streamMusicDevices.contains(getAvcAudioOutputDevice())) {
+        if (streamMusicDevices.contains(getAvbAudioOutputDevice())) {
             int direction = mute ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE;
             int flags = AudioManager.FLAG_ABSOLUTE_VOLUME;
             if (isTvDevice()) {
diff --git a/services/core/java/com/android/server/hdmi/NewDeviceAction.java b/services/core/java/com/android/server/hdmi/NewDeviceAction.java
index c4b98c2..6147c10 100644
--- a/services/core/java/com/android/server/hdmi/NewDeviceAction.java
+++ b/services/core/java/com/android/server/hdmi/NewDeviceAction.java
@@ -172,7 +172,7 @@
             return;
         }
         if (mDisplayName == null) {
-            mDisplayName = HdmiUtils.getDefaultDeviceName(mDeviceLogicalAddress);
+            mDisplayName = "";
         }
         HdmiDeviceInfo deviceInfo = HdmiDeviceInfo.cecDeviceBuilder()
                 .setLogicalAddress(mDeviceLogicalAddress)
diff --git a/services/core/java/com/android/server/hdmi/SendKeyAction.java b/services/core/java/com/android/server/hdmi/SendKeyAction.java
index 7daeaf1..2703a2c0 100644
--- a/services/core/java/com/android/server/hdmi/SendKeyAction.java
+++ b/services/core/java/com/android/server/hdmi/SendKeyAction.java
@@ -172,10 +172,10 @@
     }
 
     private void sendKeyUp() {
-        // When using Absolute Volume Control, query audio status after a volume key is released.
+        // When using absolute volume behavior, query audio status after a volume key is released.
         // This allows us to notify AudioService of the resulting volume or mute status changes.
         if (HdmiCecKeycode.isVolumeKeycode(mLastKeycode)
-                && localDevice().getService().isAbsoluteVolumeControlEnabled()) {
+                && localDevice().getService().isAbsoluteVolumeBehaviorEnabled()) {
             sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(),
                     mTargetAddress),
                     __ -> sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(
diff --git a/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java b/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java
index eb3b33d..bfa8509 100644
--- a/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java
+++ b/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java
@@ -90,7 +90,7 @@
     }
 
     void handleTimerEvent(int state) {
-        if (updateAvcSupport(FEATURE_SUPPORTED)) {
+        if (updateSetAudioVolumeLevelSupport(FEATURE_SUPPORTED)) {
             finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
         } else {
             finishWithCallback(HdmiControlManager.RESULT_EXCEPTION);
@@ -104,7 +104,7 @@
      *
      * @return Whether support was successfully updated in the network.
      */
-    private boolean updateAvcSupport(
+    private boolean updateSetAudioVolumeLevelSupport(
             @DeviceFeatures.FeatureSupportStatus int setAudioVolumeLevelSupport) {
         HdmiCecNetwork network = localDevice().mService.getHdmiCecNetwork();
         HdmiDeviceInfo currentDeviceInfo = network.getCecDeviceInfo(mTargetAddress);
diff --git a/services/core/java/com/android/server/hdmi/VolumeControlAction.java b/services/core/java/com/android/server/hdmi/VolumeControlAction.java
index d5761e170..30b188c 100644
--- a/services/core/java/com/android/server/hdmi/VolumeControlAction.java
+++ b/services/core/java/com/android/server/hdmi/VolumeControlAction.java
@@ -159,7 +159,7 @@
 
         // Update audio status if current volume position is edge of volume bar,
         // i.e max or min volume.
-        AudioManager audioManager = tv().getService().getAudioManager();
+        AudioManagerWrapper audioManager = tv().getService().getAudioManager();
         int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
         if (mIsVolumeUp) {
             int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
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/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index e8bf82f..1641d61 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -104,6 +104,7 @@
 import com.android.server.SystemService;
 import com.android.server.SystemServiceManager;
 import com.android.server.pm.parsing.PackageParser2;
+import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.utils.RequestThrottle;
 
 import libcore.io.IoUtils;
@@ -1308,6 +1309,13 @@
         }
     }
 
+    private boolean isValidForInstallConstraints(PackageStateInternal ps,
+            String installerPackageName) {
+        return TextUtils.equals(ps.getInstallSource().mInstallerPackageName, installerPackageName)
+                || TextUtils.equals(ps.getInstallSource().mUpdateOwnerPackageName,
+                installerPackageName);
+    }
+
     private CompletableFuture<InstallConstraintsResult> checkInstallConstraintsInternal(
             String installerPackageName, List<String> packageNames,
             InstallConstraints constraints, long timeoutMillis) {
@@ -1324,8 +1332,7 @@
         if (!PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)) {
             for (var packageName : packageNames) {
                 var ps = snapshot.getPackageStateInternal(packageName);
-                if (ps == null || !TextUtils.equals(
-                        ps.getInstallSource().mInstallerPackageName, installerPackageName)) {
+                if (ps == null || !isValidForInstallConstraints(ps, installerPackageName)) {
                     throw new SecurityException("Caller has no access to package " + packageName);
                 }
             }
diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
index 9127a93..ba82577 100644
--- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java
+++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
@@ -697,8 +697,6 @@
             Computer snapshot, int userId, boolean suspend) {
         final Set<String> toSuspend = packagesToSuspendInQuietMode(snapshot, userId);
         if (!suspend) {
-            // Note: this method is called from DPMS constructor to suspend apps on upgrade, but
-            // it won't enter here because 'suspend' will equal 'true'.
             final DevicePolicyManagerInternal dpm =
                     LocalServices.getService(DevicePolicyManagerInternal.class);
             if (dpm != 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/AnrController.java b/services/core/java/com/android/server/wm/AnrController.java
index 90ec964..2df601f 100644
--- a/services/core/java/com/android/server/wm/AnrController.java
+++ b/services/core/java/com/android/server/wm/AnrController.java
@@ -34,6 +34,7 @@
 import android.view.InputApplicationHandle;
 
 import com.android.internal.os.TimeoutRecord;
+import com.android.server.FgThread;
 import com.android.server.am.StackTracesDumpHelper;
 import com.android.server.criticalevents.CriticalEventLog;
 
@@ -68,7 +69,9 @@
             TimeoutRecord timeoutRecord) {
         try {
             Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "notifyAppUnresponsive()");
+            timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowStarted();
             preDumpIfLockTooSlow();
+            timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowEnded();
             final ActivityRecord activity;
             timeoutRecord.mLatencyTracker.waitingOnGlobalLockStarted();
             boolean blamePendingFocusRequest = false;
@@ -108,7 +111,7 @@
                 if (!blamePendingFocusRequest) {
                     Slog.i(TAG_WM, "ANR in " + activity.getName() + ".  Reason: "
                             + timeoutRecord.mReason);
-                    dumpAnrStateLocked(activity, null /* windowState */, timeoutRecord.mReason);
+                    dumpAnrStateAsync(activity, null /* windowState */, timeoutRecord.mReason);
                     mUnresponsiveAppByDisplay.put(activity.getDisplayId(), activity);
                 }
             }
@@ -159,7 +162,9 @@
      */
     private boolean notifyWindowUnresponsive(@NonNull IBinder inputToken,
             TimeoutRecord timeoutRecord) {
+        timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowStarted();
         preDumpIfLockTooSlow();
+        timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowEnded();
         final int pid;
         final boolean aboveSystem;
         final ActivityRecord activity;
@@ -178,7 +183,7 @@
                     ? windowState.mActivityRecord : null;
             Slog.i(TAG_WM, "ANR in " + target + ". Reason:" + timeoutRecord.mReason);
             aboveSystem = isWindowAboveSystem(windowState);
-            dumpAnrStateLocked(activity, windowState, timeoutRecord.mReason);
+            dumpAnrStateAsync(activity, windowState, timeoutRecord.mReason);
         }
         if (activity != null) {
             activity.inputDispatchingTimedOut(timeoutRecord, pid);
@@ -197,7 +202,7 @@
         timeoutRecord.mLatencyTracker.waitingOnGlobalLockStarted();
         synchronized (mService.mGlobalLock) {
             timeoutRecord.mLatencyTracker.waitingOnGlobalLockEnded();
-            dumpAnrStateLocked(null /* activity */, null /* windowState */, timeoutRecord.mReason);
+            dumpAnrStateAsync(null /* activity */, null /* windowState */, timeoutRecord.mReason);
         }
 
         // We cannot determine the z-order of the window, so place the anr dialog as high
@@ -351,15 +356,23 @@
 
     }
 
-    private void dumpAnrStateLocked(ActivityRecord activity, WindowState windowState,
-                                    String reason) {
-        try {
-            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "dumpAnrStateLocked()");
-            mService.saveANRStateLocked(activity, windowState, reason);
-            mService.mAtmService.saveANRState(reason);
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
-        }
+    /**
+     * Executes asynchronously on the fg thread not to block the stack dump for
+     * the ANRing processes.
+     */
+    private void dumpAnrStateAsync(ActivityRecord activity, WindowState windowState,
+            String reason) {
+        FgThread.getExecutor().execute(() -> {
+            try {
+                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "dumpAnrStateLocked()");
+                synchronized (mService.mGlobalLock) {
+                    mService.saveANRStateLocked(activity, windowState, reason);
+                    mService.mAtmService.saveANRState(reason);
+                }
+            } finally {
+                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+            }
+        });
     }
 
     private boolean isWindowAboveSystem(@NonNull WindowState windowState) {
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/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index ae93a94..2b72215 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -223,7 +223,7 @@
         try {
             activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true);
             ProtoLog.v(WM_DEBUG_STATES,
-                    "Refershing activity for camera compatibility treatment, "
+                    "Refreshing activity for camera compatibility treatment, "
                             + "activityRecord=%s", activity);
             final ClientTransaction transaction = ClientTransaction.obtain(
                     activity.app.getThread(), activity.token);
@@ -311,11 +311,14 @@
         }
     }
 
-    // Refreshing only when configuration changes after rotation.
+    // Refreshing only when configuration changes after rotation or camera split screen aspect ratio
+    // treatment is enabled
     private boolean shouldRefreshActivity(ActivityRecord activity, Configuration newConfig,
             Configuration lastReportedConfig) {
-        return newConfig.windowConfiguration.getDisplayRotation()
-                        != lastReportedConfig.windowConfiguration.getDisplayRotation()
+        final boolean displayRotationChanged = (newConfig.windowConfiguration.getDisplayRotation()
+                != lastReportedConfig.windowConfiguration.getDisplayRotation());
+        return (displayRotationChanged
+                || activity.mLetterboxUiController.isCameraCompatSplitScreenAspectRatioAllowed())
                 && isTreatmentEnabledForActivity(activity)
                 && activity.mLetterboxUiController.shouldRefreshActivityForCameraCompat();
     }
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index f492bab..a93cb8ad 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -214,7 +214,7 @@
     // otherwise the apps get blacked out when they are resumed and do not have focus yet.
     private boolean mIsCompatFakeFocusEnabled;
 
-    // Whether should use split screen aspect ratio for the activity when camera compat treatment
+    // Whether we should use split screen aspect ratio for the activity when camera compat treatment
     // is enabled and activity is connected to the camera in fullscreen.
     private final boolean mIsCameraCompatSplitScreenAspectRatioEnabled;
 
@@ -1118,7 +1118,7 @@
     }
 
     /**
-     * Whether should use split screen aspect ratio for the activity when camera compat treatment
+     * Whether we should use split screen aspect ratio for the activity when camera compat treatment
      * is enabled and activity is connected to the camera in fullscreen.
      */
     boolean isCameraCompatSplitScreenAspectRatioEnabled() {
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 0288e4b..5b39790 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -958,7 +958,7 @@
      * Whether we use split screen aspect ratio for the activity when camera compat treatment
      * is active because the corresponding config is enabled and activity supports resizing.
      */
-    private boolean isCameraCompatSplitScreenAspectRatioAllowed() {
+    boolean isCameraCompatSplitScreenAspectRatioAllowed() {
         return mLetterboxConfiguration.isCameraCompatSplitScreenAspectRatioEnabled()
                 && !mActivityRecord.shouldCreateCompatDisplayInsets();
     }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 25b7df4..7776132 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9014,7 +9014,7 @@
 
         final SurfaceControl mirror = SurfaceControl.mirrorSurface(displaySc);
         outSurfaceControl.copyFrom(mirror, "WMS.mirrorDisplay");
-
+        mirror.release();
         return true;
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
index 6d51bd7..d960439 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
@@ -16,6 +16,8 @@
 
 package com.android.server.devicepolicy;
 
+import static com.android.server.devicepolicy.DevicePolicyManagerService.DEFAULT_KEEP_PROFILES_RUNNING_FLAG;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -70,6 +72,7 @@
     private static final String TAG_PASSWORD_TOKEN_HANDLE = "password-token";
     private static final String TAG_PROTECTED_PACKAGES = "protected-packages";
     private static final String TAG_BYPASS_ROLE_QUALIFICATIONS = "bypass-role-qualifications";
+    private static final String TAG_KEEP_PROFILES_RUNNING = "keep-profiles-running";
     private static final String ATTR_VALUE = "value";
     private static final String ATTR_ALIAS = "alias";
     private static final String ATTR_ID = "id";
@@ -193,6 +196,12 @@
     // starts.
     String mNewUserDisclaimer = NEW_USER_DISCLAIMER_NOT_NEEDED;
 
+    /**
+     * Effective state of the feature flag. It is updated to the current configuration value
+     * during boot and doesn't change value after than unless overridden by test code.
+     */
+    boolean mEffectiveKeepProfilesRunning = DEFAULT_KEEP_PROFILES_RUNNING_FLAG;
+
     DevicePolicyData(@UserIdInt int userId) {
         mUserId = userId;
     }
@@ -392,6 +401,12 @@
                 out.endTag(null, TAG_BYPASS_ROLE_QUALIFICATIONS);
             }
 
+            if (policyData.mEffectiveKeepProfilesRunning != DEFAULT_KEEP_PROFILES_RUNNING_FLAG) {
+                out.startTag(null, TAG_KEEP_PROFILES_RUNNING);
+                out.attributeBoolean(null, ATTR_VALUE, policyData.mEffectiveKeepProfilesRunning);
+                out.endTag(null, TAG_KEEP_PROFILES_RUNNING);
+            }
+
             out.endTag(null, "policies");
 
             out.endDocument();
@@ -574,7 +589,10 @@
                             parser.getAttributeBoolean(null, ATTR_VALUE, false);
                 } else if (TAG_BYPASS_ROLE_QUALIFICATIONS.equals(tag)) {
                     policy.mBypassDevicePolicyManagementRoleQualifications = true;
-                    policy.mCurrentRoleHolder =  parser.getAttributeValue(null, ATTR_VALUE);
+                    policy.mCurrentRoleHolder = parser.getAttributeValue(null, ATTR_VALUE);
+                } else if (TAG_KEEP_PROFILES_RUNNING.equals(tag)) {
+                    policy.mEffectiveKeepProfilesRunning = parser.getAttributeBoolean(
+                            null, ATTR_VALUE, DEFAULT_KEEP_PROFILES_RUNNING_FLAG);
                 // Deprecated tags below
                 } else if (TAG_PROTECTED_PACKAGES.equals(tag)) {
                     if (policy.mUserControlDisabledPackages == null) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
index 1392d02..1322225 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
@@ -56,6 +56,7 @@
 import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.utils.Slogf;
 
 import libcore.io.IoUtils;
 
@@ -1511,7 +1512,7 @@
                 readInner(parser);
 
             } catch (XmlPullParserException | IOException | ClassNotFoundException e) {
-                Log.e(TAG, "Error parsing resources file", e);
+                Slogf.wtf(TAG, "Error parsing resources file", e);
             } finally {
                 IoUtils.closeQuietly(input);
             }
@@ -1533,7 +1534,7 @@
                         readEnforcingAdminsInner(parser);
                         break;
                     default:
-                        Log.e(TAG, "Unknown tag " + tag);
+                        Slogf.wtf(TAG, "Unknown tag " + tag);
                 }
             }
         }
@@ -1554,7 +1555,7 @@
                         policyState = PolicyState.readFromXml(parser);
                         break;
                     default:
-                        Log.e(TAG, "Unknown tag for local policy entry" + tag);
+                        Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag);
                 }
             }
 
@@ -1564,7 +1565,9 @@
                 }
                 mLocalPolicies.get(userId).put(policyKey, policyState);
             } else {
-                Log.e(TAG, "Error parsing local policy");
+                Slogf.wtf(TAG, "Error parsing local policy, policyKey is "
+                        + (policyKey == null ? "null" : policyKey) + ", and policyState is "
+                        + (policyState == null ? "null" : policyState) + ".");
             }
         }
 
@@ -1583,20 +1586,26 @@
                         policyState = PolicyState.readFromXml(parser);
                         break;
                     default:
-                        Log.e(TAG, "Unknown tag for local policy entry" + tag);
+                        Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag);
                 }
             }
 
             if (policyKey != null && policyState != null) {
                 mGlobalPolicies.put(policyKey, policyState);
             } else {
-                Log.e(TAG, "Error parsing global policy");
+                Slogf.wtf(TAG, "Error parsing global policy, policyKey is "
+                        + (policyKey == null ? "null" : policyKey) + ", and policyState is "
+                        + (policyState == null ? "null" : policyState) + ".");
             }
         }
 
         private void readEnforcingAdminsInner(TypedXmlPullParser parser)
                 throws XmlPullParserException {
             EnforcingAdmin admin = EnforcingAdmin.readFromXml(parser);
+            if (admin == null) {
+                Slogf.wtf(TAG, "Error parsing enforcingAdmins, EnforcingAdmin is null.");
+                return;
+            }
             if (!mEnforcingAdmins.contains(admin.getUserId())) {
                 mEnforcingAdmins.put(admin.getUserId(), new HashSet<>());
             }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index ef45cee..9d1c77d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -446,7 +446,6 @@
 import android.util.DebugUtils;
 import android.util.IndentingPrintWriter;
 import android.util.IntArray;
-import android.util.Log;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -489,6 +488,7 @@
 import com.android.server.PersistentDataBlockManagerInternal;
 import com.android.server.SystemServerInitThreadPool;
 import com.android.server.SystemService;
+import com.android.server.SystemServiceManager;
 import com.android.server.devicepolicy.ActiveAdmin.TrustAgentInfo;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.net.NetworkPolicyManagerInternal;
@@ -677,7 +677,7 @@
     // to decide whether an existing policy in the {@link #DEVICE_POLICIES_XML} needs to
     // be upgraded. See {@link PolicyVersionUpgrader} on instructions how to add an upgrade
     // step.
-    static final int DPMS_VERSION = 4;
+    static final int DPMS_VERSION = 5;
 
     static {
         SECURE_SETTINGS_ALLOWLIST = new ArraySet<>();
@@ -874,7 +874,7 @@
 
     // TODO(b/265683382) remove the flag after rollout.
     private static final String KEEP_PROFILES_RUNNING_FLAG = "enable_keep_profiles_running";
-    private static final boolean DEFAULT_KEEP_PROFILES_RUNNING_FLAG = true;
+    public static final boolean DEFAULT_KEEP_PROFILES_RUNNING_FLAG = true;
 
     private static final String ENABLE_WORK_PROFILE_TELEPHONY_FLAG =
             "enable_work_profile_telephony";
@@ -889,12 +889,6 @@
     private static final boolean DEFAULT_APPLICATION_EXEMPTIONS_FLAG = true;
 
     /**
-     * This feature flag is checked once after boot and this value us used until the next reboot to
-     * avoid needing to handle the flag changing on the fly.
-     */
-    private boolean mKeepProfilesRunning = isKeepProfilesRunningFlagEnabled();
-
-    /**
      * For apps targeting U+
      * Enable multiple admins to coexist on the same device.
      */
@@ -2096,12 +2090,6 @@
             performPolicyVersionUpgrade();
         }
 
-        // TODO(b/265683382) move it into an upgrade step when removing the flag, so that it is
-        // executed only once on upgrading devices, not every boot.
-        if (mKeepProfilesRunning) {
-            suspendAppsForQuietProfiles();
-        }
-
         mUserData = new SparseArray<>();
         mOwners = makeOwners(injector, pathProvider);
 
@@ -2202,12 +2190,12 @@
         return packageNameAndSignature;
     }
 
-    private void suspendAppsForQuietProfiles() {
+    private void suspendAppsForQuietProfiles(boolean toSuspend) {
         PackageManagerInternal pmi = mInjector.getPackageManagerInternal();
         List<UserInfo> users = mUserManager.getUsers();
         for (UserInfo user : users) {
             if (user.isManagedProfile() && user.isQuietModeEnabled()) {
-                pmi.setPackagesSuspendedForQuietMode(user.id, true);
+                pmi.setPackagesSuspendedForQuietMode(user.id, toSuspend);
             }
         }
     }
@@ -3482,6 +3470,10 @@
             revertTransferOwnershipIfNecessaryLocked();
         }
         updateUsbDataSignal();
+
+        // In case flag value has changed, we apply it during boot to avoid doing it concurrently
+        // with user toggling quiet mode.
+        setKeepProfileRunningEnabledUnchecked(isKeepProfilesRunningFlagEnabled());
     }
 
     // TODO(b/230841522) Make it static.
@@ -5561,7 +5553,9 @@
             if (isPermissionCheckFlagEnabled()) {
                 CallerIdentity caller = getCallerIdentity(who, callerPackageName);
                 ap = enforcePermissionAndGetEnforcingAdmin(
-                        who, MANAGE_DEVICE_POLICY_WIPE_DATA,
+                        who,
+                        /*permission=*/ MANAGE_DEVICE_POLICY_WIPE_DATA,
+                        /* adminPolicy=*/ DeviceAdminInfo.USES_POLICY_WIPE_DATA,
                         caller.getPackageName(), affectedUserId).getActiveAdmin();
             } else {
                 // This API can only be called by an active device admin,
@@ -5843,9 +5837,11 @@
             ActiveAdmin ap;
             if (isPermissionCheckFlagEnabled()) {
                 CallerIdentity caller = getCallerIdentity(who, callerPackageName);
-                // TODO: Allow use of USES_POLICY_FORCE_LOCK
                 ap = enforcePermissionAndGetEnforcingAdmin(
-                        who, MANAGE_DEVICE_POLICY_LOCK, caller.getPackageName(),
+                        who,
+                        /*permission=*/ MANAGE_DEVICE_POLICY_LOCK,
+                        /*AdminPolicy=*/DeviceAdminInfo.USES_POLICY_FORCE_LOCK,
+                        caller.getPackageName(),
                         affectedUserId).getActiveAdmin();
             } else {
                 ap = getActiveAdminForCallerLocked(
@@ -10071,6 +10067,13 @@
         if (!mHasTelephonyFeature) {
             return;
         }
+        if (!LocalServices.getService(SystemServiceManager.class).isBootCompleted()) {
+            Slogf.i(LOG_TAG, "Skip clearing managed profile Apn before boot completed");
+            // Cannot talk to APN content provider before system boots
+            // Ideally we should delay the cleanup post boot_completed, not just
+            // skipping  it altogether.
+            return;
+        }
         final List<ApnSetting> apns = getOverrideApnsUnchecked();
         for (ApnSetting apn : apns) {
             if (apn.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) {
@@ -10326,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);
@@ -11317,7 +11321,8 @@
                             (size == 1 ? "" : "s"));
                 }
                 pw.println();
-                pw.println("Keep profiles running: " + mKeepProfilesRunning);
+                pw.println("Keep profiles running: "
+                        + getUserData(UserHandle.USER_SYSTEM).mEffectiveKeepProfilesRunning);
                 pw.println();
 
                 mPolicyCache.dump(pw);
@@ -11802,9 +11807,10 @@
             if (isPermissionCheckFlagEnabled()) {
                 CallerIdentity caller = getCallerIdentity(admin, callerPackageName);
                 int affectedUserId = parent ? getProfileParentId(userHandle) : userHandle;
-                // TODO: Support USES_POLICY_DISABLE_KEYGUARD_FEATURES
                 ap = enforcePermissionAndGetEnforcingAdmin(
-                        admin, MANAGE_DEVICE_POLICY_KEYGUARD,
+                        admin,
+                        /*permission=*/MANAGE_DEVICE_POLICY_KEYGUARD,
+                        /*adminPolicy=*/DeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES,
                         caller.getPackageName(), affectedUserId).getActiveAdmin();
             } else {
                 ap = getActiveAdminForCallerLocked(admin,
@@ -13350,23 +13356,23 @@
             caller = getCallerIdentity(who);
         }
         int userId = caller.getUserId();
+        int affectedUserId = parent ? getProfileParentId(userId) : userId;
 
         checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_USER_RESTRICTION);
 
         if (isPolicyEngineForFinanceFlagEnabled()) {
             if (!isDeviceOwner(caller) && !isProfileOwner(caller)) {
+                EnforcingAdmin admin = enforcePermissionForUserRestriction(
+                        who,
+                        key,
+                        caller.getPackageName(),
+                        affectedUserId);
                 if (!mInjector.isChangeEnabled(ENABLE_COEXISTENCE_CHANGE, callerPackage, userId)) {
                     throw new IllegalStateException("Calling package is not targeting Android U.");
                 }
                 if (!UserRestrictionsUtils.isValidRestriction(key)) {
                     throw new IllegalArgumentException("Invalid restriction key: " + key);
                 }
-                int affectedUserId = parent ? getProfileParentId(userId) : userId;
-                EnforcingAdmin admin = enforcePermissionForUserRestriction(
-                        who,
-                        key,
-                        caller.getPackageName(),
-                        affectedUserId);
                 PolicyDefinition<Boolean> policyDefinition =
                         PolicyDefinition.getPolicyDefinitionForUserRestriction(key);
                 if (enabledFromThisOwner) {
@@ -16267,7 +16273,7 @@
 
         @Override
         public boolean isKeepProfilesRunningEnabled() {
-            return mKeepProfilesRunning;
+            return getUserDataUnchecked(UserHandle.USER_SYSTEM).mEffectiveKeepProfilesRunning;
         }
 
         private @Mode int findInteractAcrossProfilesResetMode(String packageName) {
@@ -18017,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);
             }
 
@@ -18034,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) {
@@ -22998,6 +23019,7 @@
             MANAGE_DEVICE_POLICY_FACTORY_RESET,
             MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES,
             MANAGE_DEVICE_POLICY_KEYGUARD,
+            MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS,
             MANAGE_DEVICE_POLICY_LOCK_TASK,
             MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY,
             MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS,
@@ -23005,7 +23027,7 @@
             MANAGE_DEVICE_POLICY_SUPPORT_MESSAGE,
             MANAGE_DEVICE_POLICY_TIME,
             MANAGE_DEVICE_POLICY_USERS,
-            MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS
+            MANAGE_DEVICE_POLICY_WIPE_DATA
     );
 
     /**
@@ -23525,14 +23547,15 @@
      *
      * @param callerPackageName The package name  of the calling application.
      * @param adminPolicy The admin policy that should grant holders permission.
-     * @param permission The name of the permission being checked.
+     * @param permissions The names of the permissions being checked.
      * @param targetUserId The userId of the user which the caller needs permission to act on.
      * @throws SecurityException if the caller has not been granted the given permission,
      * the associated cross-user permission if the caller's user is different to the target user.
      */
     private void enforcePermissions(String[] permissions, int adminPolicy,
             String callerPackageName, int targetUserId) throws SecurityException {
-        if (hasAdminPolicy(adminPolicy, callerPackageName)) {
+        if (hasAdminPolicy(adminPolicy, callerPackageName)
+                && mInjector.userHandleGetCallingUserId() == targetUserId) {
             return;
         }
         enforcePermissions(permissions, callerPackageName, targetUserId);
@@ -23561,8 +23584,9 @@
 
     private boolean hasAdminPolicy(int adminPolicy, String callerPackageName) {
         CallerIdentity caller = getCallerIdentity(callerPackageName);
-        ActiveAdmin deviceAdmin = getActiveAdminForCaller(null, caller);
-        return deviceAdmin != null && deviceAdmin.info.usesPolicy(adminPolicy);
+        ActiveAdmin deviceAdmin = getActiveAdminWithPolicyForUidLocked(
+                null, adminPolicy, caller.getUid());
+        return deviceAdmin != null;
     }
 
     /**
@@ -23755,6 +23779,18 @@
         return false;
     }
 
+    private void setKeepProfileRunningEnabledUnchecked(boolean keepProfileRunning) {
+        synchronized (getLockObject()) {
+            DevicePolicyData policyData = getUserDataUnchecked(UserHandle.USER_SYSTEM);
+            if (policyData.mEffectiveKeepProfilesRunning == keepProfileRunning) {
+                return;
+            }
+            policyData.mEffectiveKeepProfilesRunning = keepProfileRunning;
+            saveSettingsLocked(UserHandle.USER_SYSTEM);
+        }
+        suspendAppsForQuietProfiles(keepProfileRunning);
+    }
+
     private boolean isWorkProfileTelephonyEnabled() {
         return isWorkProfileTelephonyDevicePolicyManagerFlagEnabled()
                 && isWorkProfileTelephonySubscriptionManagerFlagEnabled();
@@ -23779,7 +23815,7 @@
     public void setOverrideKeepProfilesRunning(boolean enabled) {
         Preconditions.checkCallAuthorization(
                 hasCallingOrSelfPermission(MANAGE_PROFILE_AND_DEVICE_OWNERS));
-        mKeepProfilesRunning = enabled;
+        setKeepProfileRunningEnabledUnchecked(enabled);
         Slog.i(LOG_TAG, "Keep profiles running overridden to: " + enabled);
     }
 
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/EnforcingAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
index 3ed2d34..5243d14 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java
@@ -30,6 +30,7 @@
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.role.RoleManagerLocal;
 import com.android.server.LocalManagerRegistry;
+import com.android.server.utils.Slogf;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -51,6 +52,9 @@
  *
  */
 final class EnforcingAdmin {
+
+    static final String TAG = "EnforcingAdmin";
+
     static final String ROLE_AUTHORITY_PREFIX = "role:";
     static final String DPC_AUTHORITY = "enterprise";
     static final String DEVICE_ADMIN_AUTHORITY = "device_admin";
@@ -286,6 +290,7 @@
         }
     }
 
+    @Nullable
     static EnforcingAdmin readFromXml(TypedXmlPullParser parser)
             throws XmlPullParserException {
         String packageName = parser.getAttributeValue(/* namespace= */ null, ATTR_PACKAGE_NAME);
@@ -294,13 +299,25 @@
         int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID);
 
         if (isRoleAuthority) {
+            if (packageName == null) {
+                Slogf.wtf(TAG, "Error parsing EnforcingAdmin with RoleAuthority, packageName is "
+                        + "null.");
+                return null;
+            }
             // TODO(b/281697976): load active admin
             return new EnforcingAdmin(packageName, userId, null);
         } else {
+            if (packageName == null || authoritiesStr == null) {
+                Slogf.wtf(TAG, "Error parsing EnforcingAdmin, packageName is "
+                        + (packageName == null ? "null" : packageName) + ", and authorities is "
+                        + (authoritiesStr == null ? "null" : authoritiesStr) + ".");
+                return null;
+            }
             String className = parser.getAttributeValue(/* namespace= */ null, ATTR_CLASS_NAME);
             ComponentName componentName = className == null
                     ? null :  new ComponentName(packageName, className);
             Set<String> authorities = Set.of(authoritiesStr.split(ATTR_AUTHORITIES_SEPARATOR));
+            // TODO(b/281697976): load active admin
             return new EnforcingAdmin(packageName, componentName, authorities, userId, null);
         }
     }
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/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
index 63b250d..37d4f95 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
@@ -457,6 +457,7 @@
                 case TAG_POLICY_ENGINE_MIGRATION:
                     mMigratedToPolicyEngine = parser.getAttributeBoolean(
                             null, ATTR_MIGRATED_TO_POLICY_ENGINE, false);
+                    break;
                 default:
                     Slog.e(TAG, "Unexpected tag: " + tag);
                     return false;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index 7e5bb0b..7e48407 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -42,6 +42,7 @@
 import com.android.internal.util.function.QuadFunction;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.utils.Slogf;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -53,6 +54,9 @@
 import java.util.Set;
 
 final class PolicyDefinition<V> {
+
+    static final String TAG = "PolicyDefinition";
+
     private static final int POLICY_FLAG_NONE = 0;
 
     // Only use this flag if a policy can not be applied locally.
@@ -596,22 +600,40 @@
         mPolicyKey.saveToXml(serializer);
     }
 
+    @Nullable
     static <V> PolicyDefinition<V> readFromXml(TypedXmlPullParser parser)
             throws XmlPullParserException, IOException {
         // TODO: can we avoid casting?
         PolicyKey policyKey = readPolicyKeyFromXml(parser);
+        if (policyKey == null) {
+            Slogf.wtf(TAG, "Error parsing PolicyDefinition, PolicyKey is null.");
+            return null;
+        }
         PolicyDefinition<V> genericPolicyDefinition =
                 (PolicyDefinition<V>) POLICY_DEFINITIONS.get(policyKey.getIdentifier());
+        if (genericPolicyDefinition == null) {
+            Slogf.wtf(TAG, "Unknown generic policy key: " + policyKey);
+            return null;
+        }
         return genericPolicyDefinition.createPolicyDefinition(policyKey);
     }
 
+    @Nullable
     static <V> PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser)
             throws XmlPullParserException, IOException {
         // TODO: can we avoid casting?
         PolicyKey policyKey = PolicyKey.readGenericPolicyKeyFromXml(parser);
+        if (policyKey == null) {
+            Slogf.wtf(TAG, "Error parsing PolicyKey, GenericPolicyKey is null");
+            return null;
+        }
         PolicyDefinition<PolicyValue<V>> genericPolicyDefinition =
                 (PolicyDefinition<PolicyValue<V>>) POLICY_DEFINITIONS.get(
                         policyKey.getIdentifier());
+        if (genericPolicyDefinition == null) {
+            Slogf.wtf(TAG, "Error parsing PolicyKey, Unknown generic policy key: " + policyKey);
+            return null;
+        }
         return genericPolicyDefinition.mPolicyKey.readFromXml(parser);
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
index dd4c6af..599c4a7 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
@@ -19,11 +19,11 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.admin.PolicyValue;
-import android.util.Log;
 
 import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.utils.Slogf;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -224,6 +224,7 @@
         }
     }
 
+    @Nullable
     static <V> PolicyState<V> readFromXml(TypedXmlPullParser parser)
             throws IOException, XmlPullParserException {
 
@@ -245,33 +246,55 @@
                         switch (adminPolicyTag) {
                             case TAG_ENFORCING_ADMIN_ENTRY:
                                 admin = EnforcingAdmin.readFromXml(parser);
+                                if (admin == null) {
+                                    Slogf.wtf(TAG, "Error Parsing TAG_ENFORCING_ADMIN_ENTRY, "
+                                            + "EnforcingAdmin is null");
+                                }
                                 break;
                             case TAG_POLICY_VALUE_ENTRY:
                                 value = policyDefinition.readPolicyValueFromXml(parser);
+                                if (value == null) {
+                                    Slogf.wtf(TAG, "Error Parsing TAG_POLICY_VALUE_ENTRY, "
+                                            + "PolicyValue is null");
+                                }
                                 break;
                         }
                     }
                     if (admin != null) {
                         policiesSetByAdmins.put(admin, value);
                     } else {
-                        Log.e(TAG, "Error Parsing TAG_ADMIN_POLICY_ENTRY");
+                        Slogf.wtf(TAG, "Error Parsing TAG_ADMIN_POLICY_ENTRY, EnforcingAdmin "
+                                + "is null");
                     }
                     break;
                 case TAG_POLICY_DEFINITION_ENTRY:
                     policyDefinition = PolicyDefinition.readFromXml(parser);
+                    if (policyDefinition == null) {
+                        Slogf.wtf(TAG, "Error Parsing TAG_POLICY_DEFINITION_ENTRY, "
+                                + "PolicyDefinition is null");
+                    }
                     break;
 
                 case TAG_RESOLVED_VALUE_ENTRY:
+                    if (policyDefinition == null) {
+                        Slogf.wtf(TAG, "Error Parsing TAG_RESOLVED_VALUE_ENTRY, "
+                                + "policyDefinition is null");
+                        break;
+                    }
                     currentResolvedPolicy = policyDefinition.readPolicyValueFromXml(parser);
+                    if (currentResolvedPolicy == null) {
+                        Slogf.wtf(TAG, "Error Parsing TAG_RESOLVED_VALUE_ENTRY, "
+                                + "currentResolvedPolicy is null");
+                    }
                     break;
                 default:
-                    Log.e(TAG, "Unknown tag: " + tag);
+                    Slogf.wtf(TAG, "Unknown tag: " + tag);
             }
         }
         if (policyDefinition != null) {
             return new PolicyState<V>(policyDefinition, policiesSetByAdmins, currentResolvedPolicy);
         } else {
-            Log.e("PolicyState", "Error parsing policyState");
+            Slogf.wtf(TAG, "Error parsing policyState, policyDefinition is null");
             return null;
         }
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyVersionUpgrader.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyVersionUpgrader.java
index 1fe4b57..06f11be 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyVersionUpgrader.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyVersionUpgrader.java
@@ -110,6 +110,12 @@
             currentVersion = 4;
         }
 
+        if (currentVersion == 4) {
+            Slog.i(LOG_TAG, String.format("Upgrading from version %d", currentVersion));
+            initializeEffectiveKeepProfilesRunning(allUsersData);
+            currentVersion = 5;
+        }
+
         writePoliciesAndVersion(allUsers, allUsersData, ownersData, currentVersion);
     }
 
@@ -214,6 +220,16 @@
                 ownerAdmin.suspendedPackages.size(), ownerPackage, ownerUserId));
     }
 
+    private void initializeEffectiveKeepProfilesRunning(
+            SparseArray<DevicePolicyData> allUsersData) {
+        DevicePolicyData systemUserData = allUsersData.get(UserHandle.USER_SYSTEM);
+        if (systemUserData == null) {
+            return;
+        }
+        systemUserData.mEffectiveKeepProfilesRunning = false;
+        Slog.i(LOG_TAG, "Keep profile running effective state set to false");
+    }
+
     private OwnersData loadOwners(int[] allUsers) {
         OwnersData ownersData = new OwnersData(mPathProvider);
         ownersData.load(allUsers);
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/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java
index 1d6846c..eb2ee35 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java
@@ -76,7 +76,7 @@
 public class PolicyVersionUpgraderTest extends DpmTestBase {
     // NOTE: Only change this value if the corresponding CL also adds a test to test the upgrade
     // to the new version.
-    private static final int LATEST_TESTED_VERSION = 4;
+    private static final int LATEST_TESTED_VERSION = 5;
     public static final String PERMISSIONS_TAG = "admin-can-grant-sensors-permissions";
     public static final String DEVICE_OWNER_XML = "device_owner_2.xml";
     private ComponentName mFakeAdmin;
@@ -313,6 +313,24 @@
     }
 
     @Test
+    public void testEffectiveKeepProfilesRunningSet() throws Exception {
+        writeVersionToXml(4);
+
+        final int userId = UserHandle.USER_SYSTEM;
+        mProvider.mUsers = new int[]{userId};
+        preparePoliciesFile(userId, "device_policies.xml");
+
+        mUpgrader.upgradePolicy(5);
+
+        assertThat(readVersionFromXml()).isAtLeast(5);
+
+        Document policies = readPolicies(userId);
+        Element keepProfilesRunning = (Element) policies.getDocumentElement()
+                .getElementsByTagName("keep-profiles-running").item(0);
+        assertThat(keepProfilesRunning.getAttribute("value")).isEqualTo("false");
+    }
+
+    @Test
     public void isLatestVersionTested() {
         assertThat(DevicePolicyManagerService.DPMS_VERSION).isEqualTo(LATEST_TESTED_VERSION);
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
index a7fe64a..be5e262 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
@@ -26,7 +26,6 @@
 
 import android.content.Context;
 import android.content.ContextWrapper;
-import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -61,19 +60,9 @@
     public void setUp() throws Exception {
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
         mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
-                new FakeAudioDeviceVolumeManagerWrapper()) {
-            @Override
-            AudioManager getAudioManager() {
-                return new AudioManager() {
-                    @Override
-                    public void setWiredDeviceConnectionState(
-                            int type, int state, String address, String name) {
-                        // Do nothing.
-                    }
-                };
-            }
-
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) {
             @Override
             boolean isPowerStandby() {
                 return false;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
index 3df0449..5be3c8e 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
@@ -25,7 +25,6 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
-import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -37,8 +36,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -58,27 +55,21 @@
     private TestLooper mTestLooper = new TestLooper();
     private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
 
-    @Mock
-    private AudioManager mAudioManager;
-
     @Before
     public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         HdmiControlService hdmiControlService =
                 new HdmiControlService(mContextSpy, Collections.emptyList(),
-                        new FakeAudioDeviceVolumeManagerWrapper()) {
+                        audioFramework.getAudioManager(),
+                        audioFramework.getAudioDeviceVolumeManager()) {
                     @Override
                     boolean isPowerStandby() {
                         return false;
                     }
 
-                    @Override
-                    AudioManager getAudioManager() {
-                        return mAudioManager;
-                    }
 
                     @Override
                     boolean isAddressAllocated() {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
index 61ab99b..7845c30 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
@@ -27,7 +27,6 @@
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
-import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -39,7 +38,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
@@ -61,8 +59,6 @@
 
     private TestLooper mTestLooper = new TestLooper();
     private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
-    @Mock
-    private AudioManager mAudioManager;
 
     @Before
     public void setUp() throws Exception {
@@ -70,14 +66,12 @@
 
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         HdmiControlService hdmiControlService =
                 new HdmiControlService(mContextSpy, Collections.emptyList(),
-                        new FakeAudioDeviceVolumeManagerWrapper()) {
-                    @Override
-                    AudioManager getAudioManager() {
-                        return mAudioManager;
-                    }
-
+                        audioFramework.getAudioManager(),
+                        audioFramework.getAudioDeviceVolumeManager()) {
                     @Override
                     boolean isPowerStandby() {
                         return false;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
similarity index 71%
rename from services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
rename to services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
index 9f295b8..bc09d4b 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
@@ -24,16 +24,11 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 
-import static org.mockito.AdditionalMatchers.not;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -59,8 +54,6 @@
 
 import org.junit.Before;
 import org.junit.Test;
-import org.mockito.InOrder;
-import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
@@ -68,20 +61,20 @@
 import java.util.Collections;
 
 /**
- * Tests that Absolute Volume Control (AVC) is enabled and disabled correctly, and that
+ * Tests that absolute volume behavior (AVB) is enabled and disabled correctly, and that
  * the device responds correctly to incoming <Report Audio Status> messages and API calls
- * from AudioService when AVC is active.
+ * from AudioService when AVB is active.
  *
  * This is an abstract base class. Concrete subclasses specify the type of the local device, and the
  * type of the System Audio device. This allows each test to be run for multiple setups.
  *
  * We test the following pairs of (local device, System Audio device):
- * (Playback, TV): {@link PlaybackDeviceToTvAvcTest}
- * (Playback, Audio System): {@link PlaybackDeviceToAudioSystemAvcTest}
- * (TV, Audio System): {@link TvToAudioSystemAvcTest}
+ * (Playback, TV): {@link PlaybackDeviceToTvAvbTest}
+ * (Playback, Audio System): {@link PlaybackDeviceToAudioSystemAvbTest}
+ * (TV, Audio System): {@link TvToAudioSystemAvbTest}
  */
-public abstract class BaseAbsoluteVolumeControlTest {
-    private HdmiControlService mHdmiControlService;
+public abstract class BaseAbsoluteVolumeBehaviorTest {
+    protected HdmiControlService mHdmiControlService;
     private HdmiCecController mHdmiCecController;
     private HdmiCecLocalDevice mHdmiCecLocalDevice;
     private FakeHdmiCecConfig mHdmiCecConfig;
@@ -90,19 +83,20 @@
     private Context mContextSpy;
     private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
 
-    @Mock protected AudioManager mAudioManager;
-    protected FakeAudioDeviceVolumeManagerWrapper mAudioDeviceVolumeManager;
+    protected FakeAudioFramework mAudioFramework;
+    protected AudioManagerWrapper mAudioManager;
+    protected AudioDeviceVolumeManagerWrapper mAudioDeviceVolumeManager;
 
     protected TestLooper mTestLooper = new TestLooper();
     protected FakeNativeWrapper mNativeWrapper;
 
-    // Audio Status given by the System Audio device in its initial <Report Audio Status> that
-    // triggers AVC being enabled
+    // Default Audio Status given by the System Audio device in its initial <Report Audio Status>
+    // that triggers AVB being enabled
     private static final AudioStatus INITIAL_SYSTEM_AUDIO_DEVICE_STATUS =
             new AudioStatus(50, false);
 
-    // VolumeInfo passed to AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior to enable AVC
-    private static final VolumeInfo ENABLE_AVC_VOLUME_INFO =
+    // VolumeInfo passed to AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior to enable AVB
+    private static final VolumeInfo ENABLE_AVB_VOLUME_INFO =
             new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
                     .setMuted(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute())
                     .setVolumeIndex(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume())
@@ -110,6 +104,8 @@
                     .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
                     .build();
 
+    private static final int EMPTY_FLAGS = 0;
+
     protected abstract HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService);
 
     protected abstract int getPhysicalAddress();
@@ -126,17 +122,17 @@
         mContextSpy = spy(new ContextWrapper(
                 InstrumentationRegistry.getInstrumentation().getTargetContext()));
 
-        mAudioDeviceVolumeManager = spy(new FakeAudioDeviceVolumeManagerWrapper());
+        mAudioFramework = new FakeAudioFramework();
+        mAudioManager = spy(mAudioFramework.getAudioManager());
+        mAudioDeviceVolumeManager = spy(mAudioFramework.getAudioDeviceVolumeManager());
+
+        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, EMPTY_FLAGS);
+        mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, true);
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
                         Collections.singletonList(getDeviceType()),
-                        mAudioDeviceVolumeManager) {
-                    @Override
-                    AudioManager getAudioManager() {
-                        return mAudioManager;
-                    }
-
+                        mAudioManager, mAudioDeviceVolumeManager) {
                     @Override
                     protected void writeStringSystemProperty(String key, String value) {
                         // do nothing
@@ -186,22 +182,12 @@
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_BOOT_UP);
         mTestLooper.dispatchAll();
 
-        // Simulate AudioManager's behavior and response when setDeviceVolumeBehavior is called
-        doAnswer(invocation -> {
-            setDeviceVolumeBehavior(invocation.getArgument(0), invocation.getArgument(1));
-            return null;
-        }).when(mAudioManager).setDeviceVolumeBehavior(any(), anyInt());
-
-        // Set starting volume behavior
-        doReturn(AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE)
-                .when(mAudioManager).getDeviceVolumeBehavior(eq(getAudioOutputDevice()));
-
         // Audio service always plays STREAM_MUSIC on the device we need
-        doReturn(Collections.singletonList(getAudioOutputDevice())).when(mAudioManager)
-                .getDevicesForAttributes(HdmiControlService.STREAM_MUSIC_ATTRIBUTES);
+        mAudioFramework.setDevicesForAttributes(HdmiControlService.STREAM_MUSIC_ATTRIBUTES,
+                Collections.singletonList(getAudioOutputDevice()));
 
         // Max volume of STREAM_MUSIC
-        doReturn(25).when(mAudioManager).getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+        mAudioFramework.setStreamMaxVolume(AudioManager.STREAM_MUSIC, 25);
 
         // Receive messages from devices to make sure they're registered in HdmiCecNetwork
         mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
@@ -221,16 +207,6 @@
     }
 
     /**
-     * Simulates the volume behavior of {@code device} being set to {@code behavior}.
-     */
-    protected void setDeviceVolumeBehavior(AudioDeviceAttributes device,
-            @AudioManager.DeviceVolumeBehavior int behavior) {
-        doReturn(behavior).when(mAudioManager).getDeviceVolumeBehavior(eq(device));
-        mHdmiControlService.onDeviceVolumeBehaviorChanged(device, behavior);
-        mTestLooper.dispatchAll();
-    }
-
-    /**
      * Changes the setting for CEC volume.
      */
     protected void setCecVolumeControlSetting(@HdmiControlManager.VolumeControl int setting) {
@@ -278,16 +254,6 @@
     }
 
     /**
-     * Has the device receive a <Report Audio Status> reporting the status in
-     * {@link #INITIAL_SYSTEM_AUDIO_DEVICE_STATUS}
-     */
-    protected void receiveInitialReportAudioStatus() {
-        receiveReportAudioStatus(
-                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume(),
-                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute());
-    }
-
-    /**
      * Has the device receive a <Report Audio Status> message from the System Audio Device.
      */
     protected void receiveReportAudioStatus(int volume, boolean mute) {
@@ -300,40 +266,34 @@
     }
 
     /**
-     * Triggers all the conditions required to enable Absolute Volume Control.
+     * Triggers all the conditions required to enable absolute volume behavior.
      */
-    protected void enableAbsoluteVolumeControl() {
-        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+    protected void enableAbsoluteVolumeBehavior() {
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
         setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
         receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
         enableSystemAudioModeIfNeeded();
-        receiveInitialReportAudioStatus();
+        receiveReportAudioStatus(
+                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume(),
+                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute());
 
-        verifyAbsoluteVolumeEnabled();
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
     }
 
-    /**
-     * Verifies that AVC was enabled - that is the audio output device's volume behavior was last
-     * set to absolute volume behavior.
-     */
-    protected void verifyAbsoluteVolumeEnabled() {
-        InOrder inOrder = inOrder(mAudioManager, mAudioDeviceVolumeManager);
-        inOrder.verify(mAudioDeviceVolumeManager, atLeastOnce()).setDeviceAbsoluteVolumeBehavior(
-                eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean());
-        inOrder.verify(mAudioManager, never()).setDeviceVolumeBehavior(
-                eq(getAudioOutputDevice()), not(eq(AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE)));
-    }
+    protected void enableAdjustOnlyAbsoluteVolumeBehavior() {
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED);
+        receiveReportAudioStatus(
+                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume(),
+                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute());
 
-    /**
-     * Verifies that AVC was disabled - that is, the audio output device's volume behavior was
-     * last set to something other than absolute volume behavior.
-     */
-    protected void verifyAbsoluteVolumeDisabled() {
-        InOrder inOrder = inOrder(mAudioManager, mAudioDeviceVolumeManager);
-        inOrder.verify(mAudioManager, atLeastOnce()).setDeviceVolumeBehavior(
-                eq(getAudioOutputDevice()), not(eq(AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE)));
-        inOrder.verify(mAudioDeviceVolumeManager, never()).setDeviceAbsoluteVolumeBehavior(
-                eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean());
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY);
     }
 
     protected void verifyGiveAudioStatusNeverSent() {
@@ -350,7 +310,8 @@
 
     @Test
     public void allConditionsExceptSavlSupportMet_sendsSetAudioVolumeLevelAndGiveFeatures() {
-        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
         setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
         enableSystemAudioModeIfNeeded();
 
@@ -365,7 +326,8 @@
 
     @Test
     public void allConditionsMet_savlSupportLast_reportFeatures_giveAudioStatusSent() {
-        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
         setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
         enableSystemAudioModeIfNeeded();
         verifyGiveAudioStatusNeverSent();
@@ -376,7 +338,8 @@
 
     @Test
     public void allConditionsMet_savlSupportLast_noFeatureAbort_giveAudioStatusSent() {
-        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
         setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
         enableSystemAudioModeIfNeeded();
         verifyGiveAudioStatusNeverSent();
@@ -388,7 +351,8 @@
 
     @Test
     public void allConditionsMet_cecVolumeEnabledLast_giveAudioStatusSent() {
-        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
         enableSystemAudioModeIfNeeded();
         receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
         verifyGiveAudioStatusNeverSent();
@@ -404,7 +368,9 @@
         receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
         verifyGiveAudioStatusNeverSent();
 
-        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        mTestLooper.dispatchAll();
         verifyGiveAudioStatusSent();
     }
 
@@ -413,7 +379,8 @@
         // Only run when the System Audio device is an Audio System.
         assume().that(getSystemAudioDeviceType()).isEqualTo(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
 
-        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
         setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
         receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
         verifyGiveAudioStatusNeverSent();
@@ -423,62 +390,97 @@
     }
 
     @Test
-    public void giveAudioStatusSent_systemAudioDeviceSendsReportAudioStatus_avcEnabled() {
+    public void giveAudioStatusSent_systemAudioDeviceSendsReportAudioStatus_avbEnabled() {
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
         setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
         enableSystemAudioModeIfNeeded();
         receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
-        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
 
-        // Verify that AVC was never enabled
-        verify(mAudioDeviceVolumeManager, never()).setDeviceAbsoluteVolumeBehavior(
-                eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean());
-        receiveInitialReportAudioStatus();
+        // AVB should not be enabled before receiving <Report Audio Status>
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
 
-        verifyAbsoluteVolumeEnabled();
+        receiveReportAudioStatus(60, false);
+
+        // Check that absolute volume behavior was the last one adopted
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
+
+        // Check that the volume and mute status received were included when setting AVB
+        verify(mAudioDeviceVolumeManager).setDeviceAbsoluteVolumeBehavior(
+                eq(getAudioOutputDevice()),
+                eq(new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                        .setVolumeIndex(60)
+                        .setMuted(false)
+                        .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
+                        .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
+                        .build()),
+                any(), any(), anyBoolean());
     }
 
     @Test
-    public void avcEnabled_cecVolumeDisabled_absoluteVolumeDisabled() {
-        enableAbsoluteVolumeControl();
+    public void giveAudioStatusSent_reportAudioStatusVolumeOutOfBounds_avbNotEnabled() {
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        receiveReportAudioStatus(127, false);
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+    }
+
+    @Test
+    public void avbEnabled_cecVolumeDisabled_avbDisabled() {
+        enableAbsoluteVolumeBehavior();
 
         setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_DISABLED);
-        verifyAbsoluteVolumeDisabled();
+
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
     }
 
     @Test
-    public void avcEnabled_setAudioVolumeLevelNotSupported_absoluteVolumeDisabled() {
-        enableAbsoluteVolumeControl();
+    public void avbEnabled_setAudioVolumeLevelNotSupported_avbDisabled() {
+        enableAbsoluteVolumeBehavior();
 
         receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED);
-        verifyAbsoluteVolumeDisabled();
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
     }
 
     @Test
-    public void avcEnabled_setAudioVolumeLevelFeatureAborted_absoluteVolumeDisabled() {
-        enableAbsoluteVolumeControl();
+    public void avbEnabled_setAudioVolumeLevelFeatureAborted_avbDisabled() {
+        enableAbsoluteVolumeBehavior();
 
         mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildFeatureAbortCommand(
                 getSystemAudioDeviceLogicalAddress(), getLogicalAddress(),
                 Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL, Constants.ABORT_UNRECOGNIZED_OPCODE));
         mTestLooper.dispatchAll();
-        verifyAbsoluteVolumeDisabled();
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
     }
 
     @Test
-    public void avcEnabled_systemAudioModeDisabled_absoluteVolumeDisabled() {
+    public void avbEnabled_systemAudioModeDisabled_avbDisabled() {
         // Only run when the System Audio device is an Audio System.
         assume().that(getSystemAudioDeviceType()).isEqualTo(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
 
-        enableAbsoluteVolumeControl();
+        enableAbsoluteVolumeBehavior();
 
         receiveSetSystemAudioMode(false);
-        verifyAbsoluteVolumeDisabled();
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
     }
 
     @Test
-    public void avcEnabled_receiveReportAudioStatus_notifiesVolumeOrMuteChanges() {
+    public void avbEnabled_receiveReportAudioStatus_notifiesVolumeOrMuteChanges() {
         // Initial <Report Audio Status> has volume=50 and mute=false
-        enableAbsoluteVolumeControl();
+        enableAbsoluteVolumeBehavior();
 
         // New volume and mute status: sets both
         receiveReportAudioStatus(20, true);
@@ -512,12 +514,20 @@
                 eq(AudioManager.ADJUST_UNMUTE), anyInt());
         clearInvocations(mAudioManager);
 
+        // Volume not within range [0, 100]: sets neither volume nor mute
+        receiveReportAudioStatus(127, true);
+        verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(),
+                anyInt());
+        verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(),
+                anyInt());
+        clearInvocations(mAudioManager);
+
         // If AudioService causes us to send <Set Audio Volume Level>, the System Audio device's
         // volume changes. Afterward, a duplicate of an earlier <Report Audio Status> should
         // still cause us to call setStreamVolume()
         mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeChanged(
                 getAudioOutputDevice(),
-                new VolumeInfo.Builder(ENABLE_AVC_VOLUME_INFO)
+                new VolumeInfo.Builder(ENABLE_AVB_VOLUME_INFO)
                         .setVolumeIndex(20)
                         .build()
         );
@@ -530,13 +540,13 @@
     }
 
     @Test
-    public void avcEnabled_audioDeviceVolumeAdjusted_sendsUserControlPressedAndGiveAudioStatus() {
-        enableAbsoluteVolumeControl();
+    public void avbEnabled_audioDeviceVolumeAdjusted_sendsUserControlPressedAndGiveAudioStatus() {
+        enableAbsoluteVolumeBehavior();
         mNativeWrapper.clearResultMessages();
 
         mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeAdjusted(
                 getAudioOutputDevice(),
-                ENABLE_AVC_VOLUME_INFO,
+                ENABLE_AVB_VOLUME_INFO,
                 AudioManager.ADJUST_RAISE,
                 AudioDeviceVolumeManager.ADJUST_MODE_NORMAL
         );
@@ -554,14 +564,16 @@
     }
 
     @Test
-    public void avcEnabled_audioDeviceVolumeChanged_sendsSetAudioVolumeLevel() {
-        enableAbsoluteVolumeControl();
+    public void avbEnabled_audioDeviceVolumeChanged_sendsSetAudioVolumeLevel() {
+        enableAbsoluteVolumeBehavior();
         mNativeWrapper.clearResultMessages();
 
         mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeChanged(
                 getAudioOutputDevice(),
-                new VolumeInfo.Builder(ENABLE_AVC_VOLUME_INFO)
+                new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
                         .setVolumeIndex(20)
+                        .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
+                        .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
                         .build()
         );
         mTestLooper.dispatchAll();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BasePlaybackDeviceAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BasePlaybackDeviceAvbTest.java
new file mode 100644
index 0000000..4c12e436
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BasePlaybackDeviceAvbTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.hdmi;
+
+import android.hardware.hdmi.DeviceFeatures;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioManager;
+
+import org.junit.Test;
+
+/**
+ * Base class for tests for absolute volume behavior on Playback devices. Contains tests that are
+ * relevant to Playback devices but not to TVs.
+ *
+ * Subclasses contain tests for the following pairs of (local device, System Audio device):
+ * (Playback, TV): {@link PlaybackDeviceToTvAvbTest}
+ * (Playback, Audio System): {@link PlaybackDeviceToAudioSystemAvbTest}
+ */
+public abstract class BasePlaybackDeviceAvbTest extends BaseAbsoluteVolumeBehaviorTest {
+
+    @Override
+    protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) {
+        return new HdmiCecLocalDevicePlayback(hdmiControlService);
+    }
+
+    @Override
+    protected int getPhysicalAddress() {
+        return 0x1100;
+    }
+
+    @Override
+    protected int getDeviceType() {
+        return HdmiDeviceInfo.DEVICE_PLAYBACK;
+    }
+
+    @Override
+    protected AudioDeviceAttributes getAudioOutputDevice() {
+        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI;
+    }
+
+    /**
+     * Unlike TVs, Playback devices don't start the process for adopting adjust-only AVB
+     * if the System Audio device doesn't support <Set Audio Volume Level>
+     */
+    @Test
+    public void savlNotSupported_allOtherConditionsMet_giveAudioStatusNotSent() {
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED);
+        verifyGiveAudioStatusNeverSent();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java
index 28ba4bb..9b65762 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java
@@ -54,9 +54,13 @@
     @Before
     public void SetUp() {
         mDeviceInfoForTests = HdmiDeviceInfo.hardwarePort(1001, 1234);
+
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         HdmiControlService hdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                        Collections.emptyList(), audioFramework.getAudioManager(),
+                        audioFramework.getAudioDeviceVolumeManager()) {
 
                     @Override
                     void sendCecCommand(
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
index c4c5c2a..af4eab3 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
@@ -31,7 +31,6 @@
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
-import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -76,19 +75,11 @@
 
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlService = new HdmiControlService(mContextSpy,
                 Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
-                new FakeAudioDeviceVolumeManagerWrapper()) {
-            @Override
-            AudioManager getAudioManager() {
-                return new AudioManager() {
-                    @Override
-                    public void setWiredDeviceConnectionState(
-                            int type, int state, String address, String name) {
-                        // Do nothing.
-                    }
-                };
-            }
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) {
 
             @Override
             boolean isPowerStandby() {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
index b571f43..5070b08 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
@@ -97,10 +97,13 @@
         Context context = InstrumentationRegistry.getTargetContext();
         mMyLooper = mTestLooper.getLooper();
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
                         Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
-                        new FakeAudioDeviceVolumeManagerWrapper()) {
+                        audioFramework.getAudioManager(),
+                        audioFramework.getAudioDeviceVolumeManager()) {
                     @Override
                     boolean isCecControlEnabled() {
                         return true;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
index 9c1b670..49023c6 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
@@ -106,10 +106,13 @@
         Context context = InstrumentationRegistry.getTargetContext();
         mMyLooper = mTestLooper.getLooper();
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
                         Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
-                        new FakeAudioDeviceVolumeManagerWrapper()) {
+                        audioFramework.getAudioManager(),
+                        audioFramework.getAudioDeviceVolumeManager()) {
                     @Override
                     boolean isCecControlEnabled() {
                         return true;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java
deleted file mode 100644
index d33ef9b..0000000
--- a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2022 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.hdmi;
-
-import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener;
-import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener;
-
-import android.annotation.CallbackExecutor;
-import android.annotation.NonNull;
-import android.media.AudioDeviceAttributes;
-import android.media.AudioDeviceVolumeManager;
-import android.media.AudioManager;
-import android.media.VolumeInfo;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.Executor;
-
-/**
- * Wrapper for {@link AudioDeviceVolumeManager} that stubs its methods. Useful for testing.
- */
-public class FakeAudioDeviceVolumeManagerWrapper implements
-        AudioDeviceVolumeManagerWrapperInterface {
-
-    private final Set<OnDeviceVolumeBehaviorChangedListener> mVolumeBehaviorListeners;
-
-    public FakeAudioDeviceVolumeManagerWrapper() {
-        mVolumeBehaviorListeners = new HashSet<>();
-    }
-
-    @Override
-    public void addOnDeviceVolumeBehaviorChangedListener(
-            @NonNull @CallbackExecutor Executor executor,
-            @NonNull OnDeviceVolumeBehaviorChangedListener listener)
-            throws SecurityException {
-        mVolumeBehaviorListeners.add(listener);
-    }
-
-    @Override
-    public void removeOnDeviceVolumeBehaviorChangedListener(
-            @NonNull OnDeviceVolumeBehaviorChangedListener listener) {
-        mVolumeBehaviorListeners.remove(listener);
-    }
-
-    @Override
-    public void setDeviceAbsoluteVolumeBehavior(
-            @NonNull AudioDeviceAttributes device,
-            @NonNull VolumeInfo volume,
-            @NonNull @CallbackExecutor Executor executor,
-            @NonNull OnAudioDeviceVolumeChangedListener vclistener,
-            boolean handlesVolumeAdjustment) {
-        // Notify all volume behavior listeners that the device adopted absolute volume behavior
-        for (OnDeviceVolumeBehaviorChangedListener listener : mVolumeBehaviorListeners) {
-            listener.onDeviceVolumeBehaviorChanged(device,
-                    AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
-        }
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java
new file mode 100644
index 0000000..7294ba6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.hdmi;
+
+import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener;
+import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.media.VolumeInfo;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * Contains a fake AudioManager and fake AudioDeviceVolumeManager.
+ * Stores the shared state for these managers, simulating a fake AudioService.
+ */
+public class FakeAudioFramework {
+
+    private final FakeAudioManagerWrapper mAudioManager = new FakeAudioManagerWrapper();
+    private final FakeAudioDeviceVolumeManagerWrapper mAudioDeviceVolumeManager =
+            new FakeAudioDeviceVolumeManagerWrapper();
+
+    private static final int DEFAULT_DEVICE_VOLUME_BEHAVIOR =
+            AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE;
+    private final Map<AudioDeviceAttributes, Integer> mDeviceVolumeBehaviors = new HashMap<>();
+
+    private final Set<OnDeviceVolumeBehaviorChangedListener> mVolumeBehaviorListeners =
+            new HashSet<>();
+
+    private final Map<AudioAttributes, List<AudioDeviceAttributes>> mDevicesForAttributes =
+            new HashMap<>();
+
+    private static final int DEFAULT_VOLUME = 0;
+    private final Map<Integer, Integer> mStreamVolumes = new HashMap<>();
+
+    private static final int DEFAULT_MAX_VOLUME = 100;
+    private final Map<Integer, Integer> mStreamMaxVolumes = new HashMap<>();
+
+    private static final boolean DEFAULT_MUTE_STATUS = false;
+    private final Map<Integer, Boolean> mStreamMuteStatuses = new HashMap<>();
+
+    public FakeAudioFramework() {
+    }
+
+    /**
+     * Returns a fake AudioManager whose methods affect this object's internal state.
+     */
+    public FakeAudioManagerWrapper getAudioManager() {
+        return mAudioManager;
+    }
+
+    public class FakeAudioManagerWrapper implements AudioManagerWrapper {
+        @Override
+        public void adjustStreamVolume(int streamType, int direction,
+                @AudioManager.PublicVolumeFlags int flags) {
+            switch (direction) {
+                case AudioManager.ADJUST_MUTE:
+                    mStreamMuteStatuses.put(streamType, true);
+                    break;
+                case AudioManager.ADJUST_UNMUTE:
+                    mStreamMuteStatuses.put(streamType, false);
+                    break;
+                default:
+                    // Other adjustments not implemented
+            }
+        }
+
+        @Override
+        public void setStreamVolume(int streamType, int index,
+                @AudioManager.PublicVolumeFlags int flags) {
+            mStreamVolumes.put(streamType, index);
+        }
+
+        @Override
+        public int getStreamVolume(int streamType) {
+            return mStreamVolumes.getOrDefault(streamType, DEFAULT_VOLUME);
+        }
+
+        @Override
+        public int getStreamMinVolume(int streamType) {
+            return 0;
+        }
+
+        @Override
+        public int getStreamMaxVolume(int streamType) {
+            return mStreamMaxVolumes.getOrDefault(streamType, DEFAULT_MAX_VOLUME);
+        }
+
+        @Override
+        public boolean isStreamMute(int streamType) {
+            return mStreamMuteStatuses.getOrDefault(streamType, DEFAULT_MUTE_STATUS);
+        }
+
+        @Override
+        public void setStreamMute(int streamType, boolean state) {
+            mStreamMuteStatuses.put(streamType, state);
+        }
+
+        @Override
+        public int setHdmiSystemAudioSupported(boolean on) {
+            return AudioSystem.DEVICE_NONE;
+        }
+
+        @Override
+        public void setWiredDeviceConnectionState(AudioDeviceAttributes attributes, int state) {
+            // Do nothing
+        }
+
+        @Override
+        public void setWiredDeviceConnectionState(int device, int state, String address,
+                String name) {
+            // Do nothing
+        }
+
+
+        @Override
+        @AudioManager.DeviceVolumeBehavior
+        public int getDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device) {
+            return mDeviceVolumeBehaviors.getOrDefault(device, DEFAULT_DEVICE_VOLUME_BEHAVIOR);
+        }
+
+        public void setDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device,
+                @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior) {
+            setVolumeBehaviorHelper(device, deviceVolumeBehavior);
+        }
+
+        @Override
+        @NonNull
+        public List<AudioDeviceAttributes> getDevicesForAttributes(
+                @NonNull AudioAttributes attributes) {
+            return mDevicesForAttributes.getOrDefault(attributes, Collections.emptyList());
+        }
+    }
+
+    /**
+     * Returns a fake AudioDeviceVolumeManager whose methods affect this object's internal state.
+     */
+    public FakeAudioDeviceVolumeManagerWrapper getAudioDeviceVolumeManager() {
+        return mAudioDeviceVolumeManager;
+    }
+
+    public class FakeAudioDeviceVolumeManagerWrapper implements AudioDeviceVolumeManagerWrapper {
+        @Override
+        public void addOnDeviceVolumeBehaviorChangedListener(
+                @NonNull @CallbackExecutor Executor executor,
+                @NonNull OnDeviceVolumeBehaviorChangedListener listener)
+                throws SecurityException {
+            mVolumeBehaviorListeners.add(listener);
+        }
+
+        @Override
+        public void removeOnDeviceVolumeBehaviorChangedListener(
+                @NonNull OnDeviceVolumeBehaviorChangedListener listener) {
+            mVolumeBehaviorListeners.remove(listener);
+        }
+
+        @Override
+        public void setDeviceAbsoluteVolumeBehavior(
+                @NonNull AudioDeviceAttributes device,
+                @NonNull VolumeInfo volume,
+                @NonNull @CallbackExecutor Executor executor,
+                @NonNull OnAudioDeviceVolumeChangedListener vclistener,
+                boolean handlesVolumeAdjustment) {
+            setVolumeBehaviorHelper(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
+        }
+
+        @Override
+        public void setDeviceAbsoluteVolumeAdjustOnlyBehavior(
+                @NonNull AudioDeviceAttributes device,
+                @NonNull VolumeInfo volume,
+                @NonNull @CallbackExecutor Executor executor,
+                @NonNull OnAudioDeviceVolumeChangedListener vclistener,
+                boolean handlesVolumeAdjustment) {
+            setVolumeBehaviorHelper(device,
+                    AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY);
+        }
+    }
+
+    /**
+     * Allows tests to manipulate the return value of
+     * {@link FakeAudioManagerWrapper#getDevicesForAttributes}
+     */
+    public void setDevicesForAttributes(AudioAttributes attributes,
+            List<AudioDeviceAttributes> devices) {
+        mDevicesForAttributes.put(attributes, devices);
+    }
+
+    /**
+     * Allows tests to manipulate the return value of
+     * {@link FakeAudioManagerWrapper#getStreamMaxVolume}
+     */
+    public void setStreamMaxVolume(int streamType, int maxVolume) {
+        mStreamMaxVolumes.put(streamType, maxVolume);
+    }
+
+    /**
+     * Helper method for changing an audio device's volume behavior. Notifies listeners.
+     */
+    private void setVolumeBehaviorHelper(AudioDeviceAttributes device,
+            @AudioManager.DeviceVolumeBehavior int newVolumeBehavior) {
+
+        int currentVolumeBehavior = mDeviceVolumeBehaviors.getOrDefault(
+                device, DEFAULT_DEVICE_VOLUME_BEHAVIOR);
+
+        mDeviceVolumeBehaviors.put(device, newVolumeBehavior);
+
+        if (newVolumeBehavior != currentVolumeBehavior) {
+            // Notify volume behavior listeners
+            for (OnDeviceVolumeBehaviorChangedListener listener : mVolumeBehaviorListeners) {
+                listener.onDeviceVolumeBehaviorChanged(device, newVolumeBehavior);
+            }
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
index e3d9558..5e54d3b 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
@@ -86,9 +86,11 @@
         mContextSpy = spy(new ContextWrapper(
                 InstrumentationRegistry.getInstrumentation().getTargetContext()));
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy,
                 Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
-                new FakeAudioDeviceVolumeManagerWrapper()));
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
         doReturn(mHdmiCecAtomWriterSpy).when(mHdmiControlServiceSpy).getAtomWriter();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
index a7232fe..0870bac 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
@@ -100,9 +100,11 @@
     public void SetUp() {
         mMyLooper = mTestLooper.getLooper();
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlServiceSpy = spy(new HdmiControlService(
                 InstrumentationRegistry.getTargetContext(), Collections.emptyList(),
-                new FakeAudioDeviceVolumeManagerWrapper()));
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()));
         doReturn(mMyLooper).when(mHdmiControlServiceSpy).getIoLooper();
         doReturn(mMyLooper).when(mHdmiControlServiceSpy).getServiceLooper();
         doAnswer(__ -> mCecVersion).when(mHdmiControlServiceSpy).getCecVersion();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index f5c0f2a..a6e05dd 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -54,23 +54,23 @@
 @RunWith(JUnit4.class)
 /** Tests for {@link HdmiCecLocalDeviceAudioSystem} class. */
 public class HdmiCecLocalDeviceAudioSystemTest {
-
     private static final HdmiCecMessage MESSAGE_REQUEST_SAD_LCPM =
             HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
                     ADDR_TV, ADDR_AUDIO_SYSTEM, new int[] {Constants.AUDIO_CODEC_LPCM});
 
+    private static final int EMPTY_FLAGS = 0;
+
     private HdmiControlService mHdmiControlService;
     private HdmiCecController mHdmiCecController;
     private HdmiCecLocalDeviceAudioSystem mHdmiCecLocalDeviceAudioSystem;
     private HdmiCecLocalDevicePlayback mHdmiCecLocalDevicePlayback;
     private FakeNativeWrapper mNativeWrapper;
     private FakePowerManagerWrapper mPowerManager;
+    private FakeAudioFramework mAudioFramework;
+    private AudioManagerWrapper mAudioManager;
     private Looper mMyLooper;
     private TestLooper mTestLooper = new TestLooper();
     private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
-    private int mMusicVolume;
-    private int mMusicMaxVolume;
-    private boolean mMusicMute;
     private static final int SELF_PHYSICAL_ADDRESS = 0x2000;
     private static final int HDMI_1_PHYSICAL_ADDRESS = 0x2100;
     private static final int HDMI_2_PHYSICAL_ADDRESS = 0x2200;
@@ -88,66 +88,12 @@
         mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK);
         mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
 
+        mAudioFramework = new FakeAudioFramework();
+        mAudioManager = mAudioFramework.getAudioManager();
         mHdmiControlService =
             new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                    mLocalDeviceTypes,
-                    new FakeAudioDeviceVolumeManagerWrapper()) {
-                @Override
-                AudioManager getAudioManager() {
-                    return new AudioManager() {
-                        @Override
-                        public int getStreamVolume(int streamType) {
-                            switch (streamType) {
-                                case STREAM_MUSIC:
-                                    return mMusicVolume;
-                                default:
-                                    return 0;
-                            }
-                        }
-
-                        @Override
-                        public boolean isStreamMute(int streamType) {
-                            switch (streamType) {
-                                case STREAM_MUSIC:
-                                    return mMusicMute;
-                                default:
-                                    return false;
-                            }
-                        }
-
-                        @Override
-                        public int getStreamMaxVolume(int streamType) {
-                            switch (streamType) {
-                                case STREAM_MUSIC:
-                                    return mMusicMaxVolume;
-                                default:
-                                    return 100;
-                            }
-                        }
-
-                        @Override
-                        public void adjustStreamVolume(
-                                int streamType, int direction, int flags) {
-                            switch (streamType) {
-                                case STREAM_MUSIC:
-                                    if (direction == AudioManager.ADJUST_UNMUTE) {
-                                        mMusicMute = false;
-                                    } else if (direction == AudioManager.ADJUST_MUTE) {
-                                        mMusicMute = true;
-                                    }
-                                    break;
-                                default:
-                            }
-                        }
-
-                        @Override
-                        public void setWiredDeviceConnectionState(
-                                int type, int state, String address, String name) {
-                            // Do nothing.
-                        }
-                    };
-                }
-
+                    mLocalDeviceTypes, mAudioManager,
+                    mAudioFramework.getAudioDeviceVolumeManager()) {
                 @Override
                 void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) {
                     mDeviceInfo = device;
@@ -236,10 +182,12 @@
 
     @Test
     public void handleGiveAudioStatus_volume_10_mute_true() throws Exception {
-        mMusicVolume = 10;
-        mMusicMute = true;
-        mMusicMaxVolume = 20;
-        int scaledVolume = VolumeControlAction.scaleToCecVolume(10, mMusicMaxVolume);
+        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 10, EMPTY_FLAGS);
+        mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_MUTE,
+                EMPTY_FLAGS);
+        mAudioFramework.setStreamMaxVolume(AudioManager.STREAM_MUSIC, 20);
+        int scaledVolume = VolumeControlAction.scaleToCecVolume(10,
+                mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC));
         HdmiCecMessage expectedMessage =
                 HdmiCecMessageBuilder.buildReportAudioStatus(
                         ADDR_AUDIO_SYSTEM, ADDR_TV, scaledVolume, true);
@@ -303,7 +251,7 @@
     @Test
     @Ignore("b/120845532")
     public void handleSetSystemAudioMode_setOn_orignalOff() throws Exception {
-        mMusicMute = true;
+        mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, true);
         HdmiCecMessage messageSet =
                 HdmiCecMessageBuilder.buildSetSystemAudioMode(ADDR_TV, ADDR_AUDIO_SYSTEM, true);
         HdmiCecMessage messageGive =
@@ -326,13 +274,13 @@
                 .isEqualTo(Constants.HANDLED);
         mTestLooper.dispatchAll();
         assertThat(mNativeWrapper.getOnlyResultMessage()).isEqualTo(expectedMessage);
-        assertThat(mMusicMute).isFalse();
+        assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isFalse();
     }
 
     @Test
     @Ignore("b/120845532")
     public void handleSystemAudioModeRequest_turnOffByTv() throws Exception {
-        assertThat(mMusicMute).isFalse();
+        assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isFalse();
         // Check if feature correctly turned off
         HdmiCecMessage messageGive =
                 HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus(ADDR_TV, ADDR_AUDIO_SYSTEM);
@@ -354,7 +302,7 @@
             .isEqualTo(Constants.HANDLED);
         mTestLooper.dispatchAll();
         assertThat(mNativeWrapper.getOnlyResultMessage()).isEqualTo(expectedMessage);
-        assertThat(mMusicMute).isTrue();
+        assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isTrue();
     }
 
     @Test
@@ -368,7 +316,7 @@
                 HdmiCecMessageBuilder.buildSetSystemAudioMode(
                         ADDR_AUDIO_SYSTEM, ADDR_BROADCAST, false);
         assertThat(mNativeWrapper.getResultMessages()).contains(expectedMessage);
-        assertThat(mMusicMute).isTrue();
+        assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isTrue();
     }
 
     @Test
@@ -463,13 +411,13 @@
     public void terminateSystemAudioMode_systemAudioModeOff() throws Exception {
         mHdmiCecLocalDeviceAudioSystem.checkSupportAndSetSystemAudioMode(false);
         assertThat(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated()).isFalse();
-        mMusicMute = false;
+        mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, false);
         HdmiCecMessage message =
                 HdmiCecMessageBuilder.buildSetSystemAudioMode(
                         ADDR_AUDIO_SYSTEM, ADDR_BROADCAST, false);
         mHdmiCecLocalDeviceAudioSystem.terminateSystemAudioMode();
         assertThat(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated()).isFalse();
-        assertThat(mMusicMute).isFalse();
+        assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isFalse();
         assertThat(mNativeWrapper.getResultMessages()).isEmpty();
     }
 
@@ -477,13 +425,13 @@
     public void terminateSystemAudioMode_systemAudioModeOn() throws Exception {
         mHdmiCecLocalDeviceAudioSystem.checkSupportAndSetSystemAudioMode(true);
         assertThat(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated()).isTrue();
-        mMusicMute = false;
+        mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, false);
         HdmiCecMessage expectedMessage =
                 HdmiCecMessageBuilder.buildSetSystemAudioMode(
                         ADDR_AUDIO_SYSTEM, ADDR_BROADCAST, false);
         mHdmiCecLocalDeviceAudioSystem.terminateSystemAudioMode();
         assertThat(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated()).isFalse();
-        assertThat(mMusicMute).isTrue();
+        assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isTrue();
         mTestLooper.dispatchAll();
         assertThat(mNativeWrapper.getResultMessages()).contains(expectedMessage);
     }
@@ -705,8 +653,6 @@
 
     @Test
     public void giveAudioStatus_volumeEnabled() {
-        mMusicVolume = 50;
-        mMusicMaxVolume = 100;
         mHdmiControlService.setHdmiCecVolumeControlEnabledInternal(
                 HdmiControlManager.VOLUME_CONTROL_ENABLED);
         mHdmiCecLocalDeviceAudioSystem.setSystemAudioControlFeatureEnabled(true);
@@ -733,8 +679,6 @@
 
     @Test
     public void giveAudioStatus_volumeDisabled() {
-        mMusicVolume = 50;
-        mMusicMaxVolume = 100;
         mHdmiControlService.setHdmiCecVolumeControlEnabledInternal(
                 HdmiControlManager.VOLUME_CONTROL_DISABLED);
         mHdmiCecLocalDeviceAudioSystem.setSystemAudioControlFeatureEnabled(true);
@@ -761,8 +705,6 @@
 
     @Test
     public void reportAudioStatus_volumeEnabled() {
-        mMusicVolume = 50;
-        mMusicMaxVolume = 100;
         mHdmiControlService.setHdmiCecVolumeControlEnabledInternal(
                 HdmiControlManager.VOLUME_CONTROL_ENABLED);
         mHdmiCecLocalDeviceAudioSystem.setSystemAudioControlFeatureEnabled(true);
@@ -786,8 +728,6 @@
 
     @Test
     public void reportAudioStatus_volumeDisabled() {
-        mMusicVolume = 50;
-        mMusicMaxVolume = 100;
         mHdmiControlService.setHdmiCecVolumeControlEnabledInternal(
                 HdmiControlManager.VOLUME_CONTROL_DISABLED);
         mHdmiCecLocalDeviceAudioSystem.setSystemAudioControlFeatureEnabled(true);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index beba9c6..40c762c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -31,7 +31,6 @@
 import android.hardware.hdmi.HdmiPortInfo;
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
-import android.media.AudioManager;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.test.TestLooper;
@@ -46,7 +45,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
@@ -88,8 +86,6 @@
     private boolean mActiveMediaSessionsPaused;
     private FakePowerManagerInternalWrapper mPowerManagerInternal =
             new FakePowerManagerInternalWrapper();
-    @Mock
-    protected AudioManager mAudioManager;
 
     @Before
     public void setUp() {
@@ -98,10 +94,12 @@
         Context context = InstrumentationRegistry.getTargetContext();
         mMyLooper = mTestLooper.getLooper();
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
                         Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
-                        new FakeAudioDeviceVolumeManagerWrapper()) {
+                        audioFramework.getAudioManager(),
+                        audioFramework.getAudioDeviceVolumeManager()) {
 
                     @Override
                     void wakeUp() {
@@ -110,11 +108,6 @@
                     }
 
                     @Override
-                    AudioManager getAudioManager() {
-                        return mAudioManager;
-                    }
-
-                    @Override
                     void pauseActiveMediaSessions() {
                         mActiveMediaSessionsPaused = true;
                     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
index 9c5c0d4..9882670 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
@@ -33,6 +33,7 @@
 
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -53,8 +54,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -128,18 +127,17 @@
     private boolean isControlEnabled;
     private int mPowerStatus;
 
-    @Mock
-    private AudioManager mAudioManager;
+    private AudioManagerWrapper mAudioManager;
 
     @Before
     public void SetUp() {
-        MockitoAnnotations.initMocks(this);
-
         Context context = InstrumentationRegistry.getTargetContext();
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+        mAudioManager = spy(audioFramework.getAudioManager());
         mHdmiControlService =
                 new HdmiControlService(context, Collections.emptyList(),
-                        new FakeAudioDeviceVolumeManagerWrapper()) {
+                        mAudioManager, audioFramework.getAudioDeviceVolumeManager()) {
                     @Override
                     boolean isCecControlEnabled() {
                         return isControlEnabled;
@@ -171,11 +169,6 @@
                     void wakeUp() {
                         mWakeupMessageReceived = true;
                     }
-
-                    @Override
-                    AudioManager getAudioManager() {
-                        return mAudioManager;
-                    }
                 };
         mHdmiControlService.setIoLooper(mTestLooper.getLooper());
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index d2b1bdd..d52b7ea 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -33,9 +33,9 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.hardware.hdmi.HdmiControlManager;
@@ -55,8 +55,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -122,20 +120,21 @@
         }
     }
 
-    @Mock
-    private AudioManager mAudioManager;
+    private FakeAudioFramework mAudioFramework;
+    private AudioManagerWrapper mAudioManager;
 
     @Before
     public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
         Context context = InstrumentationRegistry.getTargetContext();
         mMyLooper = mTestLooper.getLooper();
 
+        mAudioFramework = new FakeAudioFramework();
+        mAudioManager = spy(mAudioFramework.getAudioManager());
+
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
                         Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
-                        new FakeAudioDeviceVolumeManagerWrapper()) {
+                        mAudioManager, mAudioFramework.getAudioDeviceVolumeManager()) {
                     @Override
                     void wakeUp() {
                         mWokenUp = true;
@@ -167,11 +166,6 @@
                     }
 
                     @Override
-                    AudioManager getAudioManager() {
-                        return mAudioManager;
-                    }
-
-                    @Override
                     void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) {
                         mDeviceEventListeners.add(new DeviceEventListener(device, status));
                     }
@@ -967,7 +961,7 @@
 
     @Test
     public void receiveSetAudioVolumeLevel_samNotActivated_noFeatureAbort_volumeChanges() {
-        when(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)).thenReturn(25);
+        mAudioFramework.setStreamMaxVolume(AudioManager.STREAM_MUSIC, 25);
 
         // Max volume of STREAM_MUSIC is retrieved on boot
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
index 81cfdbe..bdf3a5f 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
@@ -58,9 +58,11 @@
 
     @Before
     public void setUp() throws Exception {
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         HdmiControlService mHdmiControlService = new HdmiControlService(
                 InstrumentationRegistry.getTargetContext(), Collections.emptyList(),
-                new FakeAudioDeviceVolumeManagerWrapper());
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager());
 
         mHdmiControlService.setIoLooper(mTestLooper.getLooper());
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
index d341153..1ad9ce0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
@@ -67,8 +67,11 @@
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getTargetContext();
+
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlService = new HdmiControlService(mContext, Collections.emptyList(),
-                new FakeAudioDeviceVolumeManagerWrapper()) {
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) {
             @Override
             void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) {
                 mDeviceEventListenerStatuses.add(status);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
index 55e8b20..c002067 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
@@ -64,9 +64,11 @@
         Context contextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         Looper myLooper = mTestLooper.getLooper();
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlService = new HdmiControlService(contextSpy,
                 Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
-                new FakeAudioDeviceVolumeManagerWrapper()) {
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) {
             @Override
             boolean isCecControlEnabled() {
                 return true;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index fd6eb92..0e6b412 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -49,7 +49,6 @@
 import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener;
 import android.hardware.hdmi.IHdmiControlStatusChangeListener;
 import android.hardware.hdmi.IHdmiVendorCommandListener;
-import android.media.AudioManager;
 import android.os.Binder;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -64,9 +63,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Mock;
 import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -96,19 +93,17 @@
     private HdmiPortInfo[] mHdmiPortInfo;
     private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
 
-    @Mock protected AudioManager mAudioManager;
-
     @Before
     public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
         HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
         mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK);
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, mLocalDeviceTypes,
-                new FakeAudioDeviceVolumeManagerWrapper()));
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
 
@@ -171,7 +166,6 @@
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlServiceSpy.setPowerManager(mPowerManager);
         mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
-        mHdmiControlServiceSpy.setAudioManager(mAudioManager);
         mHdmiControlServiceSpy.setEarcSupported(true);
 
         mTestLooper.dispatchAll();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
index c3aec84..185f90f 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -35,7 +36,6 @@
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.media.AudioDescriptor;
 import android.media.AudioDeviceAttributes;
-import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -49,7 +49,6 @@
 import org.junit.runners.JUnit4;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
-import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
@@ -78,8 +77,7 @@
     private Looper mMyLooper;
     private TestLooper mTestLooper = new TestLooper();
 
-    @Mock
-    private AudioManager mAudioManager;
+    private AudioManagerWrapper mAudioManager;
 
     @Captor
     ArgumentCaptor<AudioDeviceAttributes> mAudioAttributesCaptor;
@@ -91,10 +89,13 @@
         Context context = InstrumentationRegistry.getTargetContext();
         mMyLooper = mTestLooper.getLooper();
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+        mAudioManager = spy(audioFramework.getAudioManager());
+
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
                         Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
-                        new FakeAudioDeviceVolumeManagerWrapper()) {
+                        mAudioManager, audioFramework.getAudioDeviceVolumeManager()) {
                     @Override
                     boolean isCecControlEnabled() {
                         return true;
@@ -114,11 +115,6 @@
                     boolean isPowerStandby() {
                         return false;
                     }
-
-                    @Override
-                    AudioManager getAudioManager() {
-                        return mAudioManager;
-                    }
                 };
 
         mHdmiControlService.setIoLooper(mMyLooper);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
index b0e8ca7..1172a87 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
@@ -31,7 +31,6 @@
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
-import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -88,20 +87,11 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         mHdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlService = new HdmiControlService(mContextSpy,
                 Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
-                new FakeAudioDeviceVolumeManagerWrapper()) {
-            @Override
-            AudioManager getAudioManager() {
-                return new AudioManager() {
-                    @Override
-                    public void setWiredDeviceConnectionState(
-                            int type, int state, String address, String name) {
-                        // Do nothing.
-                    }
-                };
-            }
-
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) {
             @Override
             boolean isPowerStandby() {
                 return false;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvbTest.java
similarity index 72%
rename from services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvcTest.java
rename to services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvbTest.java
index 6418602..43ab804 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvcTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvbTest.java
@@ -16,10 +16,12 @@
 
 package com.android.server.hdmi;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import android.hardware.hdmi.DeviceFeatures;
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
-import android.media.AudioDeviceAttributes;
+import android.media.AudioManager;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.SmallTest;
@@ -33,33 +35,13 @@
 import java.util.Arrays;
 
 /**
- * Tests for Absolute Volume Control where the local device is a Playback device and the
+ * Tests for absolute volume behavior where the local device is a Playback device and the
  * System Audio device is an Audio System.
  */
 @SmallTest
 @Presubmit
 @RunWith(JUnit4.class)
-public class PlaybackDeviceToAudioSystemAvcTest extends BaseAbsoluteVolumeControlTest {
-
-    @Override
-    protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) {
-        return new HdmiCecLocalDevicePlayback(hdmiControlService);
-    }
-
-    @Override
-    protected int getPhysicalAddress() {
-        return 0x1100;
-    }
-
-    @Override
-    protected int getDeviceType() {
-        return HdmiDeviceInfo.DEVICE_PLAYBACK;
-    }
-
-    @Override
-    protected AudioDeviceAttributes getAudioOutputDevice() {
-        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI;
-    }
+public class PlaybackDeviceToAudioSystemAvbTest extends BasePlaybackDeviceAvbTest {
 
     @Override
     protected int getSystemAudioDeviceLogicalAddress() {
@@ -72,17 +54,18 @@
     }
 
     /**
-     * AVC is disabled if the Audio System disables System Audio mode, and the TV has unknown
+     * AVB is disabled if the Audio System disables System Audio mode, and the TV has unknown
      * support for <Set Audio Volume Level>. It is enabled once the TV confirms support for
      * <Set Audio Volume Level> and sends <Report Audio Status>.
      */
     @Test
     public void switchToTv_absoluteVolumeControlDisabledUntilAllConditionsMet() {
-        enableAbsoluteVolumeControl();
+        enableAbsoluteVolumeBehavior();
 
-        // Audio System disables System Audio Mode. AVC should be disabled.
+        // Audio System disables System Audio Mode. AVB should be disabled.
         receiveSetSystemAudioMode(false);
-        verifyAbsoluteVolumeDisabled();
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
 
         // TV reports support for <Set Audio Volume Level>
         mNativeWrapper.onCecMessage(ReportFeaturesMessage.build(
@@ -102,6 +85,7 @@
                 false));
         mTestLooper.dispatchAll();
 
-        verifyAbsoluteVolumeEnabled();
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvbTest.java
similarity index 73%
rename from services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvcTest.java
rename to services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvbTest.java
index 504c3bc..9b343e3 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvcTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvbTest.java
@@ -16,12 +16,14 @@
 
 package com.android.server.hdmi;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.clearInvocations;
 
 import android.hardware.hdmi.DeviceFeatures;
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
-import android.media.AudioDeviceAttributes;
+import android.media.AudioManager;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.SmallTest;
@@ -34,33 +36,13 @@
 import java.util.Collections;
 
 /**
- * Tests for Absolute Volume Control where the local device is a Playback device and the
+ * Tests for absolute volume behavior where the local device is a Playback device and the
  * System Audio device is a TV.
  */
 @SmallTest
 @Presubmit
 @RunWith(JUnit4.class)
-public class PlaybackDeviceToTvAvcTest extends BaseAbsoluteVolumeControlTest {
-
-    @Override
-    protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) {
-        return new HdmiCecLocalDevicePlayback(hdmiControlService);
-    }
-
-    @Override
-    protected int getPhysicalAddress() {
-        return 0x1100;
-    }
-
-    @Override
-    protected int getDeviceType() {
-        return HdmiDeviceInfo.DEVICE_PLAYBACK;
-    }
-
-    @Override
-    protected AudioDeviceAttributes getAudioOutputDevice() {
-        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI;
-    }
+public class PlaybackDeviceToTvAvbTest extends BasePlaybackDeviceAvbTest {
 
     @Override
     protected int getSystemAudioDeviceLogicalAddress() {
@@ -73,17 +55,18 @@
     }
 
     /**
-     * AVC is disabled when an Audio System with unknown support for <Set Audio Volume Level>
+     * AVB is disabled when an Audio System with unknown support for <Set Audio Volume Level>
      * becomes the System Audio device. It is enabled once the Audio System reports that it
      * supports <Set Audio Volume Level> and sends <Report Audio Status>.
      */
     @Test
     public void switchToAudioSystem_absoluteVolumeControlDisabledUntilAllConditionsMet() {
-        enableAbsoluteVolumeControl();
+        enableAbsoluteVolumeBehavior();
 
-        // Audio System enables System Audio Mode. AVC should be disabled.
+        // Audio System enables System Audio Mode. AVB should be disabled.
         receiveSetSystemAudioMode(true);
-        verifyAbsoluteVolumeDisabled();
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
 
         clearInvocations(mAudioManager, mAudioDeviceVolumeManager);
 
@@ -105,6 +88,7 @@
                 false));
         mTestLooper.dispatchAll();
 
-        verifyAbsoluteVolumeEnabled();
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
index 89743cd..9f0a44c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
@@ -30,7 +30,6 @@
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
-import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -65,20 +64,11 @@
     public void setUp() throws Exception {
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlService = new HdmiControlService(mContextSpy,
                 Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
-                new FakeAudioDeviceVolumeManagerWrapper()) {
-            @Override
-            AudioManager getAudioManager() {
-                return new AudioManager() {
-                    @Override
-                    public void setWiredDeviceConnectionState(
-                            int type, int state, String address, String name) {
-                        // Do nothing.
-                    }
-                };
-            }
-
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) {
             @Override
             boolean isPowerStandby() {
                 return false;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index 1c19341..043db1e 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -95,9 +95,12 @@
         Context context = InstrumentationRegistry.getTargetContext();
         mMyLooper = mTestLooper.getLooper();
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlService =
                 new HdmiControlService(context, Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
-                        new FakeAudioDeviceVolumeManagerWrapper()) {
+                        audioFramework.getAudioManager(),
+                        audioFramework.getAudioDeviceVolumeManager()) {
                     @Override
                     boolean isCecControlEnabled() {
                         return true;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
index 5b1bdf6..1bc99b6 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
@@ -146,10 +146,13 @@
 
         HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(context);
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
                         Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
-                        new FakeAudioDeviceVolumeManagerWrapper()) {
+                        audioFramework.getAudioManager(),
+                        audioFramework.getAudioDeviceVolumeManager()) {
                     @Override
                     boolean isCecControlEnabled() {
                         return true;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
index cac7815..a73f4aa 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
@@ -80,9 +80,11 @@
         mContextSpy = spy(new ContextWrapper(
                 InstrumentationRegistry.getInstrumentation().getTargetContext()));
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy,
                 Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
-                new FakeAudioDeviceVolumeManagerWrapper()));
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
 
@@ -152,11 +154,11 @@
         mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
         mTestLooper.dispatchAll();
 
-        @DeviceFeatures.FeatureSupportStatus int avcSupport =
+        @DeviceFeatures.FeatureSupportStatus int savlSupport =
                 mHdmiControlServiceSpy.getHdmiCecNetwork().getCecDeviceInfo(Constants.ADDR_TV)
                         .getDeviceFeatures().getSetAudioVolumeLevelSupport();
 
-        assertThat(avcSupport).isEqualTo(FEATURE_SUPPORTED);
+        assertThat(savlSupport).isEqualTo(FEATURE_SUPPORTED);
         assertThat(mTestCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
     }
 
@@ -172,11 +174,11 @@
                 Constants.ABORT_UNRECOGNIZED_OPCODE));
         mTestLooper.dispatchAll();
 
-        @DeviceFeatures.FeatureSupportStatus int avcSupport =
+        @DeviceFeatures.FeatureSupportStatus int savlSupport =
                 mHdmiControlServiceSpy.getHdmiCecNetwork().getCecDeviceInfo(Constants.ADDR_TV)
                         .getDeviceFeatures().getSetAudioVolumeLevelSupport();
 
-        assertThat(avcSupport).isEqualTo(FEATURE_NOT_SUPPORTED);
+        assertThat(savlSupport).isEqualTo(FEATURE_NOT_SUPPORTED);
         assertThat(mTestCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
     }
 
@@ -189,11 +191,11 @@
         mPlaybackDevice.addAndStartAction(mAction);
         mTestLooper.dispatchAll();
 
-        @DeviceFeatures.FeatureSupportStatus int avcSupport =
+        @DeviceFeatures.FeatureSupportStatus int savlSupport =
                 mHdmiControlServiceSpy.getHdmiCecNetwork().getCecDeviceInfo(Constants.ADDR_TV)
                         .getDeviceFeatures().getSetAudioVolumeLevelSupport();
 
-        assertThat(avcSupport).isEqualTo(FEATURE_SUPPORT_UNKNOWN);
+        assertThat(savlSupport).isEqualTo(FEATURE_SUPPORT_UNKNOWN);
         assertThat(mTestCallback.getResult()).isEqualTo(
                 HdmiControlManager.RESULT_COMMUNICATION_FAILED);
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
index c40cd0e..c3beff7 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
@@ -29,7 +29,6 @@
 import android.content.ContextWrapper;
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
-import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -68,20 +67,11 @@
 
         Looper myLooper = mTestLooper.getLooper();
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         mHdmiControlService = new HdmiControlService(mContextSpy,
                 Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
-                new FakeAudioDeviceVolumeManagerWrapper()) {
-            @Override
-            AudioManager getAudioManager() {
-                return new AudioManager() {
-                    @Override
-                    public void setWiredDeviceConnectionState(
-                            int type, int state, String address, String name) {
-                        // Do nothing.
-                    }
-                };
-            }
-
+                audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) {
             @Override
             boolean isPowerStandby() {
                 return false;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
index b13ef4f..f801f88 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
@@ -24,7 +24,6 @@
 import android.content.Context;
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
-import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -68,8 +67,11 @@
 
         Context context = InstrumentationRegistry.getTargetContext();
 
+        FakeAudioFramework audioFramework = new FakeAudioFramework();
+
         HdmiControlService hdmiControlService = new HdmiControlService(context,
-                Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                Collections.emptyList(), audioFramework.getAudioManager(),
+                audioFramework.getAudioDeviceVolumeManager()) {
                     @Override
                     void sendCecCommand(
                             HdmiCecMessage command, @Nullable SendMessageCallback callback) {
@@ -100,36 +102,6 @@
                     }
 
                     @Override
-                    AudioManager getAudioManager() {
-                        return new AudioManager() {
-
-                            @Override
-                            public int setHdmiSystemAudioSupported(boolean on) {
-                                return 0;
-                            }
-
-                            @Override
-                            public int getStreamVolume(int streamType) {
-                                return 0;
-                            }
-
-                            @Override
-                            public boolean isStreamMute(int streamType) {
-                                return false;
-                            }
-
-                            @Override
-                            public int getStreamMaxVolume(int streamType) {
-                                return 100;
-                            }
-
-                            @Override
-                            public void adjustStreamVolume(
-                                    int streamType, int direction, int flags) {}
-                        };
-                    }
-
-                    @Override
                     boolean isPowerStandby() {
                         return false;
                     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java
new file mode 100644
index 0000000..079ef2e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2022 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.hdmi;
+
+import static com.android.server.hdmi.HdmiCecKeycode.CEC_KEYCODE_VOLUME_UP;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.hdmi.DeviceFeatures;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceVolumeManager;
+import android.media.AudioManager;
+import android.media.VolumeInfo;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for absolute volume behavior where the local device is a TV and the System Audio device
+ * is an Audio System. Assumes that the TV uses ARC (rather than eARC).
+ */
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class TvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehaviorTest {
+
+    @Override
+    protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) {
+        return new HdmiCecLocalDeviceTv(hdmiControlService);
+    }
+
+    @Override
+    protected int getPhysicalAddress() {
+        return 0x0000;
+    }
+
+    @Override
+    protected int getDeviceType() {
+        return HdmiDeviceInfo.DEVICE_TV;
+    }
+
+    @Override
+    protected AudioDeviceAttributes getAudioOutputDevice() {
+        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI_ARC;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceLogicalAddress() {
+        return Constants.ADDR_AUDIO_SYSTEM;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceType() {
+        return HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM;
+    }
+
+    /**
+     * TVs start the process for adopting adjust-only AVB if the System Audio device doesn't
+     * support <Set Audio Volume Level>
+     */
+    @Test
+    public void savlNotSupported_allOtherConditionsMet_giveAudioStatusSent() {
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        verifyGiveAudioStatusNeverSent();
+
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED);
+        verifyGiveAudioStatusSent();
+    }
+
+    @Test
+    public void savlNotSupported_systemAudioDeviceSendsReportAudioStatus_adjustOnlyAvbEnabled() {
+        mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(),
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED);
+
+        // Adjust-only AVB should not be enabled before receiving <Report Audio Status>
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+
+        receiveReportAudioStatus(20, false);
+
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY);
+
+        verify(mAudioDeviceVolumeManager).setDeviceAbsoluteVolumeAdjustOnlyBehavior(
+                eq(getAudioOutputDevice()),
+                eq(new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                        .setVolumeIndex(20)
+                        .setMuted(false)
+                        .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
+                        .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
+                        .build()),
+                any(), any(), anyBoolean());
+    }
+
+
+    @Test
+    public void avbEnabled_savlNotSupported_receiveReportAudioStatus_switchToAdjustOnlyAvb() {
+        enableAbsoluteVolumeBehavior();
+
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED);
+
+        receiveReportAudioStatus(40, true);
+
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY);
+
+        verify(mAudioDeviceVolumeManager).setDeviceAbsoluteVolumeAdjustOnlyBehavior(
+                eq(getAudioOutputDevice()),
+                eq(new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                        .setVolumeIndex(40)
+                        .setMuted(true)
+                        .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
+                        .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
+                        .build()),
+                any(), any(), anyBoolean());
+    }
+
+    @Test
+    public void avbEnabled_savlFeatureAborted_receiveReportAudioStatus_switchToAdjustOnlyAvb() {
+        enableAbsoluteVolumeBehavior();
+
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                getSystemAudioDeviceLogicalAddress(), getLogicalAddress(),
+                Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL, Constants.ABORT_UNRECOGNIZED_OPCODE));
+        mTestLooper.dispatchAll();
+
+        receiveReportAudioStatus(40, true);
+
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY);
+
+        verify(mAudioDeviceVolumeManager).setDeviceAbsoluteVolumeAdjustOnlyBehavior(
+                eq(getAudioOutputDevice()),
+                eq(new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                        .setVolumeIndex(40)
+                        .setMuted(true)
+                        .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
+                        .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
+                        .build()),
+                any(), any(), anyBoolean());
+    }
+
+    @Test
+    public void adjustOnlyAvbEnabled_receiveReportAudioStatus_notifiesVolumeOrMuteChanges() {
+        enableAdjustOnlyAbsoluteVolumeBehavior();
+
+        // New volume and mute status: sets both
+        receiveReportAudioStatus(20, true);
+        verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(5),
+                anyInt());
+        verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_MUTE), anyInt());
+        clearInvocations(mAudioManager);
+
+        // New volume only: sets volume only
+        receiveReportAudioStatus(32, true);
+        verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+                anyInt());
+        verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_MUTE), anyInt());
+        clearInvocations(mAudioManager);
+
+        // New mute status only: sets mute only
+        receiveReportAudioStatus(32, false);
+        verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+                anyInt());
+        verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_UNMUTE), anyInt());
+        clearInvocations(mAudioManager);
+
+        // Repeat of earlier message: sets neither volume nor mute
+        receiveReportAudioStatus(32, false);
+        verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+                anyInt());
+        verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_UNMUTE), anyInt());
+
+        // Volume not within range [0, 100]: sets neither volume nor mute
+        receiveReportAudioStatus(127, true);
+        verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(),
+                anyInt());
+        verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(),
+                anyInt());
+    }
+
+    @Test
+    public void adjustOnlyAvbEnabled_audioDeviceVolumeAdjusted_sendsUcpAndGiveAudioStatus() {
+        enableAdjustOnlyAbsoluteVolumeBehavior();
+        mNativeWrapper.clearResultMessages();
+
+        mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeAdjusted(
+                getAudioOutputDevice(),
+                new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                        .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
+                        .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
+                        .build(),
+                AudioManager.ADJUST_RAISE,
+                AudioDeviceVolumeManager.ADJUST_MODE_NORMAL
+        );
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildUserControlPressed(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress(), CEC_KEYCODE_VOLUME_UP));
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildUserControlReleased(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress()));
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildGiveAudioStatus(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress()));
+    }
+
+    @Test
+    public void adjustOnlyAvbEnabled_audioDeviceVolumeChanged_doesNotSendSetAudioVolumeLevel() {
+        enableAdjustOnlyAbsoluteVolumeBehavior();
+
+        mNativeWrapper.clearResultMessages();
+
+        mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeChanged(
+                getAudioOutputDevice(),
+                new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                        .setVolumeIndex(20)
+                        .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
+                        .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
+                        .build()
+        );
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).isEmpty();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java
deleted file mode 100644
index 41c0e0d..0000000
--- a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2022 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.hdmi;
-
-import android.hardware.hdmi.HdmiDeviceInfo;
-import android.media.AudioDeviceAttributes;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/**
- * Tests for Absolute Volume Control where the local device is a TV and the System Audio device
- * is an Audio System. Assumes that the TV uses ARC (rather than eARC).
- */
-@SmallTest
-@Presubmit
-@RunWith(JUnit4.class)
-public class TvToAudioSystemAvcTest extends BaseAbsoluteVolumeControlTest {
-
-    @Override
-    protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) {
-        return new HdmiCecLocalDeviceTv(hdmiControlService);
-    }
-
-    @Override
-    protected int getPhysicalAddress() {
-        return 0x0000;
-    }
-
-    @Override
-    protected int getDeviceType() {
-        return HdmiDeviceInfo.DEVICE_TV;
-    }
-
-    @Override
-    protected AudioDeviceAttributes getAudioOutputDevice() {
-        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI_ARC;
-    }
-
-    @Override
-    protected int getSystemAudioDeviceLogicalAddress() {
-        return Constants.ADDR_AUDIO_SYSTEM;
-    }
-
-    @Override
-    protected int getSystemAudioDeviceType() {
-        return HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM;
-    }
-}
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/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
index a311726..3ca35ef 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -479,6 +479,8 @@
     public void testOnActivityConfigurationChanging_displayRotationNotChanging_noRefresh()
             throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        doReturn(false).when(mActivity.mLetterboxUiController)
+                .isCameraCompatSplitScreenAspectRatioAllowed();
 
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
         callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ false);
@@ -487,6 +489,19 @@
     }
 
     @Test
+    public void testOnActivityConfigurationChanging_splitScreenAspectRatioAllowed_refresh()
+            throws Exception {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        doReturn(true).when(mActivity.mLetterboxUiController)
+                .isCameraCompatSplitScreenAspectRatioAllowed();
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ false);
+
+        assertActivityRefreshRequested(/* refreshRequested */ true);
+    }
+
+    @Test
     public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
         when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
                 .thenReturn(false);
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
index f1e1a5a..dc5f6e9 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
@@ -1252,6 +1252,7 @@
                     mEventLogger.enqueue(new SessionEvent(Type.RESUME_FAILED,
                                 modelData.getModelId(), String.valueOf(status))
                             .printLog(ALOGW, TAG));
+                    modelData.setRequested(false);
                     callback.onResumeFailed(status);
                 } catch (RemoteException e) {
                     mEventLogger.enqueue(new SessionEvent(Type.RESUME_FAILED,
@@ -1300,6 +1301,7 @@
                     mEventLogger.enqueue(new SessionEvent(Type.PAUSE_FAILED,
                                 modelData.getModelId(), String.valueOf(status))
                             .printLog(ALOGW, TAG));
+                    modelData.setRequested(false);
                     callback.onPauseFailed(status);
                 } catch (RemoteException e) {
                     mEventLogger.enqueue(new SessionEvent(Type.PAUSE_FAILED,
@@ -1453,6 +1455,7 @@
         }
 
         synchronized void setNotLoaded() {
+            mRecognitionToken = null;
             mModelState = MODEL_NOTLOADED;
         }
 
@@ -1462,6 +1465,7 @@
 
         synchronized void clearState() {
             mModelState = MODEL_NOTLOADED;
+            mRecognitionToken = null;
             mRecognitionConfig = null;
             mRequested = false;
             mCallback = null;
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 **/