Merge "Cherrypick from git_tm-dev: Fixing broken {docroot} links"
diff --git a/Android.bp b/Android.bp
index 2740ccc..d031284 100644
--- a/Android.bp
+++ b/Android.bp
@@ -404,7 +404,7 @@
         "modules-utils-uieventlogger-interface",
         "framework-permission-aidl-java",
         "spatializer-aidl-java",
-        "audiopolicy-types-aidl-java",
+        "audiopolicy-aidl-java",
         "sounddose-aidl-java",
     ],
 }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5f4c19b..14d43f5 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6635,6 +6635,7 @@
     method public boolean isAudioServerRunning();
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean isBluetoothVariableLatencyEnabled();
     method public boolean isHdmiSystemAudioSupported();
+    method @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public boolean isHotwordStreamSupported(boolean);
     method @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public boolean isPstnCallAudioInterceptable();
     method @RequiresPermission(android.Manifest.permission.ACCESS_ULTRASOUND) public boolean isUltrasoundSupported();
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void muteAwaitConnection(@NonNull int[], @NonNull android.media.AudioDeviceAttributes, long, @NonNull java.util.concurrent.TimeUnit) throws java.lang.IllegalStateException;
@@ -6763,12 +6764,16 @@
   public class AudioRecord implements android.media.AudioRecordingMonitor android.media.AudioRouting android.media.MicrophoneDirection {
     ctor @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public AudioRecord(android.media.AudioAttributes, android.media.AudioFormat, int, int) throws java.lang.IllegalArgumentException;
     method public static long getMaxSharedAudioHistoryMillis();
+    method public boolean isHotwordLookbackStream();
+    method public boolean isHotwordStream();
     method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.MediaSyncEvent shareAudioHistory(@NonNull String, @IntRange(from=0) long);
   }
 
   public static class AudioRecord.Builder {
     method public android.media.AudioRecord.Builder setAudioAttributes(@NonNull android.media.AudioAttributes) throws java.lang.IllegalArgumentException;
     method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.AudioRecord.Builder setMaxSharedAudioHistoryMillis(long) throws java.lang.IllegalArgumentException;
+    method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.AudioRecord.Builder setRequestHotwordLookbackStream(boolean);
+    method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.AudioRecord.Builder setRequestHotwordStream(boolean);
     method public android.media.AudioRecord.Builder setSessionId(int) throws java.lang.IllegalArgumentException;
     method @NonNull public android.media.AudioRecord.Builder setSharedAudioEvent(@NonNull android.media.MediaSyncEvent) throws java.lang.IllegalArgumentException;
   }
diff --git a/core/jni/android_media_AudioRecord.cpp b/core/jni/android_media_AudioRecord.cpp
index 30333d2..1910331 100644
--- a/core/jni/android_media_AudioRecord.cpp
+++ b/core/jni/android_media_AudioRecord.cpp
@@ -149,7 +149,8 @@
                                             jint channelIndexMask, jint audioFormat,
                                             jint buffSizeInBytes, jintArray jSession,
                                             jobject jAttributionSource, jlong nativeRecordInJavaObj,
-                                            jint sharedAudioHistoryMs) {
+                                            jint sharedAudioHistoryMs,
+                                            jint halFlags) {
     //ALOGV(">> Entering android_media_AudioRecord_setup");
     //ALOGV("sampleRate=%d, audioFormat=%d, channel mask=%x, buffSizeInBytes=%d "
     //     "nativeRecordInJavaObj=0x%llX",
@@ -239,10 +240,7 @@
         }
         ALOGV("AudioRecord_setup for source=%d tags=%s flags=%08x", paa->source, paa->tags, paa->flags);
 
-        audio_input_flags_t flags = AUDIO_INPUT_FLAG_NONE;
-        if (paa->flags & AUDIO_FLAG_HW_HOTWORD) {
-            flags = AUDIO_INPUT_FLAG_HW_HOTWORD;
-        }
+        const auto flags = static_cast<audio_input_flags_t>(halFlags);
         // create the callback information:
         // this data will be passed with every AudioRecord callback
         // we use a weak reference so the AudioRecord object can be garbage collected.
@@ -831,7 +829,7 @@
         {"native_start", "(II)I", (void *)android_media_AudioRecord_start},
         {"native_stop", "()V", (void *)android_media_AudioRecord_stop},
         {"native_setup",
-         "(Ljava/lang/Object;Ljava/lang/Object;[IIIII[ILandroid/os/Parcel;JI)I",
+         "(Ljava/lang/Object;Ljava/lang/Object;[IIIII[ILandroid/os/Parcel;JII)I",
          (void *)android_media_AudioRecord_setup},
         {"native_finalize", "()V", (void *)android_media_AudioRecord_finalize},
         {"native_release", "()V", (void *)android_media_AudioRecord_release},
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 19610a93..a84c42af 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -7528,6 +7528,27 @@
 
     /**
      * @hide
+     * Indicates whether the platform supports capturing content from the hotword recognition
+     * pipeline. To capture content of this type, create an AudioRecord with
+     * {@link AudioRecord.Builder.setRequestHotwordStream(boolean, boolean)}.
+     * @param lookbackAudio Query if the hotword stream additionally supports providing buffered
+     * audio prior to stream open.
+     * @return True if the platform supports capturing hotword content, and if lookbackAudio
+     * is true, if it additionally supports capturing buffered hotword content prior to stream
+     * open. False otherwise.
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
+    public boolean isHotwordStreamSupported(boolean lookbackAudio) {
+        try {
+            return getService().isHotwordStreamSupported(lookbackAudio);
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
+     * @hide
      * Introspection API to retrieve audio product strategies.
      * When implementing {Car|Oem}AudioManager, use this method  to retrieve the collection of
      * audio product strategies, which is indexed by a weakly typed index in order to be extended
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 6e3829a..b6ab262 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -37,6 +37,7 @@
 import android.content.AttributionSource.ScopedParcelState;
 import android.content.Context;
 import android.media.MediaRecorder.Source;
+import android.media.audio.common.AudioInputFlags;
 import android.media.audiopolicy.AudioMix;
 import android.media.audiopolicy.AudioMixingRule;
 import android.media.audiopolicy.AudioPolicy;
@@ -276,6 +277,11 @@
      */
     private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
     /**
+     * Audio HAL Input Flags as bitfield.
+     */
+    private int mHalInputFlags = 0;
+
+    /**
      * AudioAttributes
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -360,7 +366,8 @@
     public AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
             int sessionId) throws IllegalArgumentException {
         this(attributes, format, bufferSizeInBytes, sessionId,
-                ActivityThread.currentApplication(), 0 /*maxSharedAudioHistoryMs*/);
+                ActivityThread.currentApplication(),
+                0 /*maxSharedAudioHistoryMs*/, 0 /* halInputFlags */);
     }
 
     /**
@@ -382,14 +389,15 @@
      *   time. See also {@link AudioManager#generateAudioSessionId()} to obtain a session ID before
      *   construction.
      * @param context An optional context on whose behalf the recoding is performed.
-     *
+     * @param maxSharedAudioHistoryMs
+     * @param halInputFlags Bitfield indexed by {@link AudioInputFlags} which is passed to the HAL.
      * @throws IllegalArgumentException
      */
     private AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
             int sessionId, @Nullable Context context,
-            int maxSharedAudioHistoryMs) throws IllegalArgumentException {
+            int maxSharedAudioHistoryMs, int halInputFlags) throws IllegalArgumentException {
         mRecordingState = RECORDSTATE_STOPPED;
-
+        mHalInputFlags = halInputFlags;
         if (attributes == null) {
             throw new IllegalArgumentException("Illegal null AudioAttributes");
         }
@@ -469,7 +477,7 @@
             int initResult = native_setup(new WeakReference<AudioRecord>(this), mAudioAttributes,
                     sampleRate, mChannelMask, mChannelIndexMask, mAudioFormat,
                     mNativeBufferSizeInBytes, session, attributionSourceState.getParcel(),
-                    0 /*nativeRecordInJavaObj*/, maxSharedAudioHistoryMs);
+                    0 /*nativeRecordInJavaObj*/, maxSharedAudioHistoryMs, mHalInputFlags);
             if (initResult != SUCCESS) {
                 loge("Error code " + initResult + " when initializing native AudioRecord object.");
                 return; // with mState == STATE_UNINITIALIZED
@@ -535,7 +543,8 @@
                         session,
                         attributionSourceState.getParcel(),
                         nativeRecordInJavaObj,
-                        0);
+                        0 /*maxSharedAudioHistoryMs*/,
+                        0 /*halInputFlags*/);
             }
             if (initResult != SUCCESS) {
                 loge("Error code "+initResult+" when initializing native AudioRecord object.");
@@ -597,6 +606,8 @@
         private int mPrivacySensitive = PRIVACY_SENSITIVE_DEFAULT;
         private int mMaxSharedAudioHistoryMs = 0;
         private int mCallRedirectionMode = AudioManager.CALL_REDIRECT_NONE;
+        private boolean mIsHotwordStream = false;
+        private boolean mIsHotwordLookback = false;
 
         private static final int PRIVACY_SENSITIVE_DEFAULT = -1;
         private static final int PRIVACY_SENSITIVE_DISABLED = 0;
@@ -906,17 +917,73 @@
         }
 
         /**
+         * @hide
+         * Set to indicate that the requested AudioRecord object should produce the same type
+         * of audio content that the hotword recognition model consumes. SoundTrigger hotword
+         * recognition will not be disrupted. The source in the set AudioAttributes and the set
+         * audio source will be overridden if this API is used.
+         * <br> Use {@link AudioManager#isHotwordStreamSupported(boolean)} to query support.
+         * @param hotwordContent true if AudioRecord should produce content captured from the
+         *        hotword pipeline. false if AudioRecord should produce content captured outside
+         *        the hotword pipeline.
+         * @return the same Builder instance.
+         **/
+        @SystemApi
+        @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
+        public @NonNull Builder setRequestHotwordStream(boolean hotwordContent) {
+            mIsHotwordStream = hotwordContent;
+            return this;
+        }
+
+        /**
+         * @hide
+         * Set to indicate that the requested AudioRecord object should produce the same type
+         * of audio content that the hotword recognition model consumes and that the stream will
+         * be able to provide buffered audio content from an unspecified duration prior to stream
+         * open. The source in the set AudioAttributes and the set audio source will be overridden
+         * if this API is used.
+         * <br> Use {@link AudioManager#isHotwordStreamSupported(boolean)} to query support.
+         * <br> If this is set, {@link AudioRecord.Builder#setRequestHotwordStream(boolean)}
+         * must not be set, or {@link AudioRecord.Builder#build()} will throw.
+         * @param hotwordLookbackContent true if AudioRecord should produce content captured from
+         *        the hotword pipeline with capture content from prior to open. false if AudioRecord
+         *        should not capture such content.
+         * to stream open is requested.
+         * @return the same Builder instance.
+         **/
+        @SystemApi
+        @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
+        public @NonNull Builder setRequestHotwordLookbackStream(boolean hotwordLookbackContent) {
+            mIsHotwordLookback = hotwordLookbackContent;
+            return this;
+        }
+
+
+        /**
          * @return a new {@link AudioRecord} instance successfully initialized with all
          *     the parameters set on this <code>Builder</code>.
          * @throws UnsupportedOperationException if the parameters set on the <code>Builder</code>
-         *     were incompatible, or if they are not supported by the device,
-         *     or if the device was not available.
+         *     were incompatible, if the parameters are not supported by the device, if the caller
+         *     does not hold the appropriate permissions, or if the device was not available.
          */
         @RequiresPermission(android.Manifest.permission.RECORD_AUDIO)
         public AudioRecord build() throws UnsupportedOperationException {
             if (mAudioPlaybackCaptureConfiguration != null) {
                 return buildAudioPlaybackCaptureRecord();
             }
+            int halInputFlags = 0;
+            if (mIsHotwordStream) {
+                if (mIsHotwordLookback) {
+                    throw new UnsupportedOperationException(
+                            "setRequestHotwordLookbackStream and " +
+                            "setRequestHotwordStream used concurrently");
+                } else {
+                    halInputFlags = (1 << AudioInputFlags.HOTWORD_TAP);
+                }
+            } else if (mIsHotwordLookback) {
+                    halInputFlags = (1 << AudioInputFlags.HOTWORD_TAP) |
+                        (1 << AudioInputFlags.HW_LOOKBACK);
+            }
 
             if (mFormat == null) {
                 mFormat = new AudioFormat.Builder()
@@ -942,6 +1009,12 @@
                         .build();
             }
 
+            if (mIsHotwordStream || mIsHotwordLookback) {
+                mAttributes = new AudioAttributes.Builder(mAttributes)
+                        .setInternalCapturePreset(MediaRecorder.AudioSource.VOICE_RECOGNITION)
+                        .build();
+            }
+
             // If mPrivacySensitive is default, the privacy flag is already set
             // according to audio source in audio attributes.
             if (mPrivacySensitive != PRIVACY_SENSITIVE_DEFAULT) {
@@ -980,7 +1053,7 @@
                 }
                 final AudioRecord record = new AudioRecord(
                         mAttributes, mFormat, mBufferSizeInBytes, mSessionId, mContext,
-                                    mMaxSharedAudioHistoryMs);
+                                    mMaxSharedAudioHistoryMs, halInputFlags);
                 if (record.getState() == STATE_UNINITIALIZED) {
                     // release is not necessary
                     throw new UnsupportedOperationException("Cannot create AudioRecord");
@@ -1041,8 +1114,8 @@
         case AudioFormat.CHANNEL_IN_DEFAULT: // AudioFormat.CHANNEL_CONFIGURATION_DEFAULT
         case AudioFormat.CHANNEL_IN_MONO:
         case AudioFormat.CHANNEL_CONFIGURATION_MONO:
-            mask = AudioFormat.CHANNEL_IN_MONO;
-            break;
+        mask = AudioFormat.CHANNEL_IN_MONO;
+        break;
         case AudioFormat.CHANNEL_IN_STEREO:
         case AudioFormat.CHANNEL_CONFIGURATION_STEREO:
             mask = AudioFormat.CHANNEL_IN_STEREO;
@@ -1384,6 +1457,35 @@
         return (mAudioAttributes.getAllFlags() & AudioAttributes.FLAG_CAPTURE_PRIVATE) != 0;
     }
 
+    /**
+     * @hide
+     * Returns whether the AudioRecord object produces the same type of audio content that
+     * the hotword recognition model consumes.
+     * <br> If {@link isHotwordLookbackStream(boolean)} is true, this will return false
+     * <br> See {@link Builder#setRequestHotwordStream(boolean)}
+     * @return true if AudioRecord produces hotword content, false otherwise
+     **/
+    @SystemApi
+    public boolean isHotwordStream() {
+        return ((mHalInputFlags & (1 << AudioInputFlags.HOTWORD_TAP)) != 0 &&
+                 (mHalInputFlags & (1 << AudioInputFlags.HW_LOOKBACK)) == 0);
+    }
+
+    /**
+     * @hide
+     * Returns whether the AudioRecord object produces the same type of audio content that
+     * the hotword recognition model consumes, and includes capture content from prior to
+     * stream open.
+     * <br> See {@link Builder#setRequestHotwordLookbackStream(boolean)}
+     * @return true if AudioRecord produces hotword capture content from
+     * prior to stream open, false otherwise
+     **/
+    @SystemApi
+    public boolean isHotwordLookbackStream() {
+        return ((mHalInputFlags & (1 << AudioInputFlags.HW_LOOKBACK)) != 0);
+    }
+
+
     //---------------------------------------------------------
     // Transport control methods
     //--------------------
@@ -2346,13 +2448,13 @@
             Object /*AudioAttributes*/ attributes,
             int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
             int buffSizeInBytes, int[] sessionId, String opPackageName,
-            long nativeRecordInJavaObj) {
+            long nativeRecordInJavaObj, int halInputFlags) {
         AttributionSource attributionSource = AttributionSource.myAttributionSource()
                 .withPackageName(opPackageName);
         try (ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState()) {
             return native_setup(audiorecordThis, attributes, sampleRate, channelMask,
                     channelIndexMask, audioFormat, buffSizeInBytes, sessionId,
-                    attributionSourceState.getParcel(), nativeRecordInJavaObj, 0);
+                    attributionSourceState.getParcel(), nativeRecordInJavaObj, 0, halInputFlags);
         }
     }
 
@@ -2360,7 +2462,7 @@
             Object /*AudioAttributes*/ attributes,
             int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
             int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
-            long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
+            long nativeRecordInJavaObj, int maxSharedAudioHistoryMs, int halInputFlags);
 
     // TODO remove: implementation calls directly into implementation of native_release()
     private native void native_finalize();
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 4b36237..0de367d 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -165,6 +165,9 @@
     @EnforcePermission("ACCESS_ULTRASOUND")
     boolean isUltrasoundSupported();
 
+    @EnforcePermission("CAPTURE_AUDIO_HOTWORD")
+    boolean isHotwordStreamSupported(boolean lookbackAudio);
+
     void setMicrophoneMute(boolean on, String callingPackage, int userId, in String attributionTag);
 
     oneway void setMicrophoneMuteFromSwitch(boolean on);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index 57a00c9..b6b657e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -204,15 +204,6 @@
             Trace.endSection();
         }
 
-        @Override
-        public void onUserListItemClicked(@NonNull UserRecord record,
-                @Nullable UserSwitchDialogController.DialogShower dialogShower) {
-            if (dialogShower != null) {
-                mDialogShower.dismiss();
-            }
-            super.onUserListItemClicked(record, dialogShower);
-        }
-
         public void linkToViewGroup(ViewGroup viewGroup) {
             PseudoGridView.ViewGroupAdapterBridge.link(viewGroup, this);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 68d30d3..2b4f51c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -60,7 +60,7 @@
      * animation to and from the parent dialog.
      */
     @JvmOverloads
-    open fun onUserListItemClicked(
+    fun onUserListItemClicked(
         record: UserRecord,
         dialogShower: DialogShower? = null,
     ) {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index 45cb11e..c8f98c3 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -455,6 +455,7 @@
             }
             UserActionModel.ADD_SUPERVISED_USER -> {
                 uiEventLogger.log(MultiUserActionsEvent.CREATE_RESTRICTED_USER_FROM_USER_SWITCHER)
+                dismissDialog()
                 activityStarter.startActivity(
                     Intent()
                         .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
index d451230..79721b3 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -66,12 +66,6 @@
     private fun startHandlingDialogShowRequests() {
         applicationScope.get().launch {
             interactor.get().dialogShowRequests.filterNotNull().collect { request ->
-                currentDialog?.let {
-                    if (it.isShowing) {
-                        it.cancel()
-                    }
-                }
-
                 val (dialog, dialogCuj) =
                     when (request) {
                         is ShowDialogRequestModel.ShowAddUserDialog ->
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
index 08a90b7..18e40f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
@@ -30,7 +30,6 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.qs.QSUserSwitcherEvent
-import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.statusbar.policy.UserSwitcherController
 import com.android.systemui.user.data.source.UserRecord
 import org.junit.Assert.assertEquals
@@ -42,7 +41,6 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
@@ -152,15 +150,6 @@
         assertNull(adapter.users.find { it.isManageUsers })
     }
 
-    @Test
-    fun clickDismissDialog() {
-        val shower: UserSwitchDialogController.DialogShower =
-            mock(UserSwitchDialogController.DialogShower::class.java)
-        adapter.injectDialogShower(shower)
-        adapter.onUserListItemClicked(createUserRecord(current = true, guest = false), shower)
-        verify(shower).dismiss()
-    }
-
     private fun createUserRecord(current: Boolean, guest: Boolean) =
         UserRecord(
             UserInfo(0 /* id */, "name", 0 /* flags */),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index 9534575..1a2ee18 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -61,12 +62,12 @@
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineScope
-import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+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
@@ -74,11 +75,13 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
+import org.mockito.Mockito.atLeastOnce
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class UserInteractorTest : SysuiTestCase() {
@@ -95,7 +98,7 @@
 
     private lateinit var underTest: UserInteractor
 
-    private lateinit var testCoroutineScope: TestCoroutineScope
+    private lateinit var testScope: TestScope
     private lateinit var userRepository: FakeUserRepository
     private lateinit var keyguardRepository: FakeKeyguardRepository
     private lateinit var telephonyRepository: FakeTelephonyRepository
@@ -119,11 +122,12 @@
         userRepository = FakeUserRepository()
         keyguardRepository = FakeKeyguardRepository()
         telephonyRepository = FakeTelephonyRepository()
-        testCoroutineScope = TestCoroutineScope()
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
         val refreshUsersScheduler =
             RefreshUsersScheduler(
-                applicationScope = testCoroutineScope,
-                mainDispatcher = IMMEDIATE,
+                applicationScope = testScope.backgroundScope,
+                mainDispatcher = testDispatcher,
                 repository = userRepository,
             )
         underTest =
@@ -136,21 +140,21 @@
                         repository = keyguardRepository,
                     ),
                 manager = manager,
-                applicationScope = testCoroutineScope,
+                applicationScope = testScope.backgroundScope,
                 telephonyInteractor =
                     TelephonyInteractor(
                         repository = telephonyRepository,
                     ),
                 broadcastDispatcher = fakeBroadcastDispatcher,
-                backgroundDispatcher = IMMEDIATE,
+                backgroundDispatcher = testDispatcher,
                 activityManager = activityManager,
                 refreshUsersScheduler = refreshUsersScheduler,
                 guestUserInteractor =
                     GuestUserInteractor(
                         applicationContext = context,
-                        applicationScope = testCoroutineScope,
-                        mainDispatcher = IMMEDIATE,
-                        backgroundDispatcher = IMMEDIATE,
+                        applicationScope = testScope.backgroundScope,
+                        mainDispatcher = testDispatcher,
+                        backgroundDispatcher = testDispatcher,
                         manager = manager,
                         repository = userRepository,
                         deviceProvisionedController = deviceProvisionedController,
@@ -167,7 +171,7 @@
 
     @Test
     fun `onRecordSelected - user`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -184,7 +188,7 @@
 
     @Test
     fun `onRecordSelected - switch to guest user`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -200,7 +204,7 @@
 
     @Test
     fun `onRecordSelected - switch to restricted user`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             var userInfos = createUserInfos(count = 2, includeGuest = false).toMutableList()
             userInfos.add(
                 UserInfo(
@@ -225,7 +229,7 @@
 
     @Test
     fun `onRecordSelected - enter guest mode`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -234,6 +238,7 @@
             whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
 
             underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower)
+            runCurrent()
 
             verify(uiEventLogger, times(1))
                 .log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER)
@@ -244,7 +249,7 @@
 
     @Test
     fun `onRecordSelected - action`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -260,81 +265,72 @@
 
     @Test
     fun `users - switcher enabled`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var value: List<UserModel>? = null
-            val job = underTest.users.onEach { value = it }.launchIn(this)
-            assertUsers(models = value, count = 3, includeGuest = true)
+            val value = collectLastValue(underTest.users)
 
-            job.cancel()
+            assertUsers(models = value(), count = 3, includeGuest = true)
         }
 
     @Test
     fun `users - switches to second user`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var value: List<UserModel>? = null
-            val job = underTest.users.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.users)
             userRepository.setSelectedUserInfo(userInfos[1])
 
-            assertUsers(models = value, count = 2, selectedIndex = 1)
-            job.cancel()
+            assertUsers(models = value(), count = 2, selectedIndex = 1)
         }
 
     @Test
     fun `users - switcher not enabled`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
 
-            var value: List<UserModel>? = null
-            val job = underTest.users.onEach { value = it }.launchIn(this)
-            assertUsers(models = value, count = 1)
-
-            job.cancel()
+            val value = collectLastValue(underTest.users)
+            assertUsers(models = value(), count = 1)
         }
 
     @Test
     fun selectedUser() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var value: UserModel? = null
-            val job = underTest.selectedUser.onEach { value = it }.launchIn(this)
-            assertUser(value, id = 0, isSelected = true)
+            val value = collectLastValue(underTest.selectedUser)
+            assertUser(value(), id = 0, isSelected = true)
 
             userRepository.setSelectedUserInfo(userInfos[1])
-            assertUser(value, id = 1, isSelected = true)
-
-            job.cancel()
+            assertUser(value(), id = 1, isSelected = true)
         }
 
     @Test
     fun `actions - device unlocked`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
 
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value)
+            runCurrent()
+
+            assertThat(value())
                 .isEqualTo(
                     listOf(
                         UserActionModel.ENTER_GUEST_MODE,
@@ -343,13 +339,11 @@
                         UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
                     )
                 )
-
-            job.cancel()
         }
 
     @Test
     fun `actions - device unlocked - full screen`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 2, includeGuest = false)
 
@@ -357,10 +351,9 @@
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value)
+            assertThat(value())
                 .isEqualTo(
                     listOf(
                         UserActionModel.ADD_USER,
@@ -369,46 +362,38 @@
                         UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
                     )
                 )
-
-            job.cancel()
         }
 
     @Test
     fun `actions - device unlocked user not primary - empty list`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value).isEqualTo(emptyList<UserActionModel>())
-
-            job.cancel()
+            assertThat(value()).isEqualTo(emptyList<UserActionModel>())
         }
 
     @Test
     fun `actions - device unlocked user is guest - empty list`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = true)
             assertThat(userInfos[1].isGuest).isTrue()
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value).isEqualTo(emptyList<UserActionModel>())
-
-            job.cancel()
+            assertThat(value()).isEqualTo(emptyList<UserActionModel>())
         }
 
     @Test
     fun `actions - device locked add from lockscreen set - full list`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -419,10 +404,9 @@
                 )
             )
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value)
+            assertThat(value())
                 .isEqualTo(
                     listOf(
                         UserActionModel.ENTER_GUEST_MODE,
@@ -431,13 +415,11 @@
                         UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
                     )
                 )
-
-            job.cancel()
         }
 
     @Test
     fun `actions - device locked add from lockscreen set - full list - full screen`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
@@ -449,10 +431,9 @@
                 )
             )
             keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value)
+            assertThat(value())
                 .isEqualTo(
                     listOf(
                         UserActionModel.ADD_USER,
@@ -461,42 +442,35 @@
                         UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
                     )
                 )
-
-            job.cancel()
         }
 
     @Test
     fun `actions - device locked - only  manage user is shown`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             keyguardRepository.setKeyguardShowing(true)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
+            val value = collectLastValue(underTest.actions)
 
-            assertThat(value).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT))
-
-            job.cancel()
+            assertThat(value()).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT))
         }
 
     @Test
     fun `executeAction - add user - dialog shown`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             keyguardRepository.setKeyguardShowing(false)
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogRequest = collectLastValue(underTest.dialogShowRequests)
             val dialogShower: UserSwitchDialogController.DialogShower = mock()
 
             underTest.executeAction(UserActionModel.ADD_USER, dialogShower)
-
             verify(uiEventLogger, times(1))
                 .log(MultiUserActionsEvent.CREATE_USER_FROM_USER_SWITCHER)
-            assertThat(dialogRequest)
+            assertThat(dialogRequest())
                 .isEqualTo(
                     ShowDialogRequestModel.ShowAddUserDialog(
                         userHandle = userInfos[0].userHandle,
@@ -507,14 +481,12 @@
                 )
 
             underTest.onDialogShown()
-            assertThat(dialogRequest).isNull()
-
-            job.cancel()
+            assertThat(dialogRequest()).isNull()
         }
 
     @Test
-    fun `executeAction - add supervised user - starts activity`() =
-        runBlocking(IMMEDIATE) {
+    fun `executeAction - add supervised user - dismisses dialog and starts activity`() =
+        testScope.runTest {
             underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
 
             verify(uiEventLogger, times(1))
@@ -528,7 +500,7 @@
 
     @Test
     fun `executeAction - navigate to manage users`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
 
             val intentCaptor = kotlinArgumentCaptor<Intent>()
@@ -538,7 +510,7 @@
 
     @Test
     fun `executeAction - guest mode`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -546,25 +518,24 @@
             val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
             whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
             val dialogRequests = mutableListOf<ShowDialogRequestModel?>()
-            val showDialogsJob =
-                underTest.dialogShowRequests
-                    .onEach {
-                        dialogRequests.add(it)
-                        if (it != null) {
-                            underTest.onDialogShown()
-                        }
+            backgroundScope.launch {
+                underTest.dialogShowRequests.collect {
+                    dialogRequests.add(it)
+                    if (it != null) {
+                        underTest.onDialogShown()
                     }
-                    .launchIn(this)
-            val dismissDialogsJob =
-                underTest.dialogDismissRequests
-                    .onEach {
-                        if (it != null) {
-                            underTest.onDialogDismissed()
-                        }
+                }
+            }
+            backgroundScope.launch {
+                underTest.dialogDismissRequests.collect {
+                    if (it != null) {
+                        underTest.onDialogDismissed()
                     }
-                    .launchIn(this)
+                }
+            }
 
             underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+            runCurrent()
 
             verify(uiEventLogger, times(1))
                 .log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER)
@@ -573,85 +544,79 @@
                     ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
                 )
             verify(activityManager).switchUser(guestUserInfo.id)
-
-            showDialogsJob.cancel()
-            dismissDialogsJob.cancel()
         }
 
     @Test
     fun `selectUser - already selected guest re-selected - exit guest dialog`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = true)
             val guestUserInfo = userInfos[1]
             assertThat(guestUserInfo.isGuest).isTrue()
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(guestUserInfo)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogRequest = collectLastValue(underTest.dialogShowRequests)
 
             underTest.selectUser(
                 newlySelectedUserId = guestUserInfo.id,
                 dialogShower = dialogShower,
             )
 
-            assertThat(dialogRequest)
+            assertThat(dialogRequest())
                 .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
             verify(dialogShower, never()).dismiss()
-            job.cancel()
         }
 
     @Test
     fun `selectUser - currently guest non-guest selected - exit guest dialog`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = true)
             val guestUserInfo = userInfos[1]
             assertThat(guestUserInfo.isGuest).isTrue()
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(guestUserInfo)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogRequest = collectLastValue(underTest.dialogShowRequests)
 
             underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower)
 
-            assertThat(dialogRequest)
+            assertThat(dialogRequest())
                 .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
             verify(dialogShower, never()).dismiss()
-            job.cancel()
         }
 
     @Test
     fun `selectUser - not currently guest - switches users`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogRequest = collectLastValue(underTest.dialogShowRequests)
 
             underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower)
 
-            assertThat(dialogRequest).isNull()
+            assertThat(dialogRequest()).isNull()
             verify(activityManager).switchUser(userInfos[1].id)
             verify(dialogShower).dismiss()
-            job.cancel()
         }
 
     @Test
     fun `Telephony call state changes - refreshes users`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
+            runCurrent()
+
             val refreshUsersCallCount = userRepository.refreshUsersCallCount
 
             telephonyRepository.setCallState(1)
+            runCurrent()
 
             assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
         }
 
     @Test
     fun `User switched broadcast`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -660,9 +625,11 @@
             val callback2: UserInteractor.UserCallback = mock()
             underTest.addCallback(callback1)
             underTest.addCallback(callback2)
+            runCurrent()
             val refreshUsersCallCount = userRepository.refreshUsersCallCount
 
             userRepository.setSelectedUserInfo(userInfos[1])
+            runCurrent()
             fakeBroadcastDispatcher.registeredReceivers.forEach {
                 it.onReceive(
                     context,
@@ -670,16 +637,17 @@
                         .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
                 )
             }
+            runCurrent()
 
-            verify(callback1).onUserStateChanged()
-            verify(callback2).onUserStateChanged()
+            verify(callback1, atLeastOnce()).onUserStateChanged()
+            verify(callback2, atLeastOnce()).onUserStateChanged()
             assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id)
             assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
         }
 
     @Test
     fun `User info changed broadcast`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -692,12 +660,14 @@
                 )
             }
 
+            runCurrent()
+
             assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
         }
 
     @Test
     fun `System user unlocked broadcast - refresh users`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -710,13 +680,14 @@
                         .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
                 )
             }
+            runCurrent()
 
             assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
         }
 
     @Test
     fun `Non-system user unlocked broadcast - do not refresh users`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -734,14 +705,14 @@
 
     @Test
     fun userRecords() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = false)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
             keyguardRepository.setKeyguardShowing(false)
 
-            testCoroutineScope.advanceUntilIdle()
+            runCurrent()
 
             assertRecords(
                 records = underTest.userRecords.value,
@@ -760,7 +731,7 @@
 
     @Test
     fun userRecordsFullScreen() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 3, includeGuest = false)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
@@ -768,7 +739,7 @@
             userRepository.setSelectedUserInfo(userInfos[0])
             keyguardRepository.setKeyguardShowing(false)
 
-            testCoroutineScope.advanceUntilIdle()
+            runCurrent()
 
             assertRecords(
                 records = underTest.userRecords.value,
@@ -787,7 +758,7 @@
 
     @Test
     fun selectedUserRecord() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             userRepository.setUserInfos(userInfos)
@@ -805,64 +776,54 @@
 
     @Test
     fun `users - secondary user - guest user can be switched to`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var res: List<UserModel>? = null
-            val job = underTest.users.onEach { res = it }.launchIn(this)
-            assertThat(res?.size == 3).isTrue()
-            assertThat(res?.find { it.isGuest }).isNotNull()
-            job.cancel()
+            val res = collectLastValue(underTest.users)
+            assertThat(res()?.size == 3).isTrue()
+            assertThat(res()?.find { it.isGuest }).isNotNull()
         }
 
     @Test
     fun `users - secondary user - no guest action`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var res: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { res = it }.launchIn(this)
-            assertThat(res?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull()
-            job.cancel()
+            val res = collectLastValue(underTest.actions)
+            assertThat(res()?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull()
         }
 
     @Test
     fun `users - secondary user - no guest user record`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 3, includeGuest = true)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var res: List<UserRecord>? = null
-            val job = underTest.userRecords.onEach { res = it }.launchIn(this)
-            assertThat(res?.find { it.isGuest }).isNull()
-            job.cancel()
+            assertThat(underTest.userRecords.value.find { it.isGuest }).isNull()
         }
 
     @Test
     fun `show user switcher - full screen disabled - shows dialog switcher`() =
-        runBlocking(IMMEDIATE) {
-            var dialogRequest: ShowDialogRequestModel? = null
+        testScope.runTest {
             val expandable = mock<Expandable>()
             underTest.showUserSwitcher(context, expandable)
 
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogRequest = collectLastValue(underTest.dialogShowRequests)
 
             // Dialog is shown.
-            assertThat(dialogRequest)
+            assertThat(dialogRequest())
                 .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable))
 
             underTest.onDialogShown()
-            assertThat(dialogRequest).isNull()
-
-            job.cancel()
+            assertThat(dialogRequest()).isNull()
         }
 
     @Test
@@ -893,8 +854,8 @@
 
     @Test
     fun `users - secondary user - managed profile is not included`() =
-        runBlocking(IMMEDIATE) {
-            var userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList()
+        testScope.runTest {
+            val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList()
             userInfos.add(
                 UserInfo(
                     50,
@@ -907,23 +868,19 @@
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
 
-            var res: List<UserModel>? = null
-            val job = underTest.users.onEach { res = it }.launchIn(this)
-            assertThat(res?.size == 3).isTrue()
-            job.cancel()
+            val res = collectLastValue(underTest.users)
+            assertThat(res()?.size == 3).isTrue()
         }
 
     @Test
     fun `current user is not primary and user switcher is disabled`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[1])
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
-            var selectedUser: UserModel? = null
-            val job = underTest.selectedUser.onEach { selectedUser = it }.launchIn(this)
-            assertThat(selectedUser).isNotNull()
-            job.cancel()
+            val selectedUser = collectLastValue(underTest.selectedUser)
+            assertThat(selectedUser()).isNotNull()
         }
 
     private fun assertUsers(
@@ -1061,7 +1018,6 @@
     }
 
     companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
         private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
         private val GUEST_ICON: Drawable = mock()
         private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation"
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 5c00452..4d53b48 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -173,7 +173,6 @@
         "android.hardware.power.stats-V1-java",
         "android.hardware.power-V4-java",
         "android.hidl.manager-V1.2-java",
-        "capture_state_listener-aidl-java",
         "icu4j_calendar_astronomer",
         "netd-client",
         "overlayable_policy_aidl-java",
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 0fec082..1d4d425 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -655,8 +655,7 @@
         // TODO: migrate alarm-prioritization flag to BroadcastConstants
         return (isForeground()
                 || interactive
-                || alarm)
-                && receivers.size() == 1;
+                || alarm);
     }
 
     @NonNull String getHostingRecordTriggerType() {
diff --git a/services/core/java/com/android/server/attention/AttentionManagerService.java b/services/core/java/com/android/server/attention/AttentionManagerService.java
index d4ef638..658e38b 100644
--- a/services/core/java/com/android/server/attention/AttentionManagerService.java
+++ b/services/core/java/com/android/server/attention/AttentionManagerService.java
@@ -668,8 +668,8 @@
             mIProximityUpdateCallback = new IProximityUpdateCallback.Stub() {
                 @Override
                 public void onProximityUpdate(double distance) {
+                    mCallbackInternal.onProximityUpdate(distance);
                     synchronized (mLock) {
-                        mCallbackInternal.onProximityUpdate(distance);
                         freeIfInactiveLocked();
                     }
                 }
diff --git a/services/core/java/com/android/server/audio/AudioPolicyFacade.java b/services/core/java/com/android/server/audio/AudioPolicyFacade.java
new file mode 100644
index 0000000..02e80d6
--- /dev/null
+++ b/services/core/java/com/android/server/audio/AudioPolicyFacade.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 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.audio;
+
+
+/**
+ * Facade to IAudioPolicyService which fulfills AudioService dependencies.
+ * See @link{IAudioPolicyService.aidl}
+ */
+public interface AudioPolicyFacade {
+
+    public boolean isHotwordStreamSupported(boolean lookbackAudio);
+}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 24c7d2c..0f7a725 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -146,6 +146,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.PermissionEnforcer;
 import android.os.PersistableBundle;
 import android.os.PowerManager;
 import android.os.Process;
@@ -249,6 +250,7 @@
     private final AudioSystemAdapter mAudioSystem;
     private final SystemServerAdapter mSystemServer;
     private final SettingsAdapter mSettings;
+    private final AudioPolicyFacade mAudioPolicy;
 
     /** Debug audio mode */
     protected static final boolean DEBUG_MODE = false;
@@ -923,7 +925,13 @@
 
         public Lifecycle(Context context) {
             super(context);
-            mService = new AudioService(context);
+            mService = new AudioService(context,
+                              AudioSystemAdapter.getDefaultAdapter(),
+                              SystemServerAdapter.getDefaultAdapter(context),
+                              SettingsAdapter.getDefaultAdapter(),
+                              new DefaultAudioPolicyFacade(),
+                              null);
+
         }
 
         @Override
@@ -976,14 +984,6 @@
     // Construction
     ///////////////////////////////////////////////////////////////////////////
 
-    /** @hide */
-    public AudioService(Context context) {
-        this(context,
-                AudioSystemAdapter.getDefaultAdapter(),
-                SystemServerAdapter.getDefaultAdapter(context),
-                SettingsAdapter.getDefaultAdapter(),
-                null);
-    }
 
     /**
      * @param context
@@ -994,9 +994,11 @@
      *               {@link AudioSystemThread} is created as the messaging thread instead.
      */
     public AudioService(Context context, AudioSystemAdapter audioSystem,
-            SystemServerAdapter systemServer, SettingsAdapter settings, @Nullable Looper looper) {
-        this (context, audioSystem, systemServer, settings, looper,
-                context.getSystemService(AppOpsManager.class));
+            SystemServerAdapter systemServer, SettingsAdapter settings,
+            AudioPolicyFacade audioPolicy, @Nullable Looper looper) {
+        this (context, audioSystem, systemServer, settings, audioPolicy, looper,
+                context.getSystemService(AppOpsManager.class),
+                PermissionEnforcer.fromContext(context));
     }
 
     /**
@@ -1009,8 +1011,10 @@
      */
     @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     public AudioService(Context context, AudioSystemAdapter audioSystem,
-            SystemServerAdapter systemServer, SettingsAdapter settings, @Nullable Looper looper,
-            AppOpsManager appOps) {
+            SystemServerAdapter systemServer, SettingsAdapter settings,
+            AudioPolicyFacade audioPolicy, @Nullable Looper looper, AppOpsManager appOps,
+            @NonNull PermissionEnforcer enforcer) {
+        super(enforcer);
         sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()"));
         mContext = context;
         mContentResolver = context.getContentResolver();
@@ -1019,7 +1023,7 @@
         mAudioSystem = audioSystem;
         mSystemServer = systemServer;
         mSettings = settings;
-
+        mAudioPolicy = audioPolicy;
         mPlatformType = AudioSystem.getPlatformType(context);
 
         mIsSingleVolume = AudioSystem.isSingleVolume(context);
@@ -3955,6 +3959,20 @@
         return AudioSystem.isUltrasoundSupported();
     }
 
+    /** @see AudioManager#isHotwordStreamSupported() */
+    @android.annotation.EnforcePermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
+    public boolean isHotwordStreamSupported(boolean lookbackAudio) {
+        super.isHotwordStreamSupported_enforcePermission();
+        try {
+            return mAudioPolicy.isHotwordStreamSupported(lookbackAudio);
+        } catch (IllegalStateException e) {
+            // Suppress connection failure to APM, since the method is purely informative
+            Log.e(TAG, "Suppressing exception calling into AudioPolicy", e);
+            return false;
+        }
+    }
+
+
     private boolean canChangeAccessibilityVolume() {
         synchronized (mAccessibilityServiceUidsLock) {
             if (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
diff --git a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
new file mode 100644
index 0000000..37b8126
--- /dev/null
+++ b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 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.audio;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.IAudioPolicyService;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.SafeCloseable;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * Default implementation of a facade to IAudioPolicyManager which fulfills AudioService
+ * dependencies. This forwards calls as-is to IAudioPolicyManager.
+ * Public methods throw IllegalStateException if AudioPolicy is not initialized/available
+ */
+public class DefaultAudioPolicyFacade implements AudioPolicyFacade, IBinder.DeathRecipient {
+
+    private static final String TAG = "DefaultAudioPolicyFacade";
+    private static final String AUDIO_POLICY_SERVICE_NAME = "media.audio_policy";
+
+    private final Object mServiceLock = new Object();
+    @GuardedBy("mServiceLock")
+    private IAudioPolicyService mAudioPolicy;
+
+    public DefaultAudioPolicyFacade() {
+        try {
+            getAudioPolicyOrInit();
+        } catch (IllegalStateException e) {
+            // Log and suppress this exception, we may be able to connect later
+            Log.e(TAG, "Failed to initialize APM connection", e);
+        }
+    }
+
+    @Override
+    public boolean isHotwordStreamSupported(boolean lookbackAudio) {
+        IAudioPolicyService ap = getAudioPolicyOrInit();
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            return ap.isHotwordStreamSupported(lookbackAudio);
+        } catch (RemoteException e) {
+            resetServiceConnection(ap.asBinder());
+            throw new IllegalStateException(e);
+        }
+    }
+
+    @Override
+    public void binderDied() {
+        Log.wtf(TAG, "Unexpected binderDied without IBinder object");
+    }
+
+    @Override
+    public void binderDied(@NonNull IBinder who) {
+        resetServiceConnection(who);
+    }
+
+    private void resetServiceConnection(@Nullable IBinder deadAudioPolicy) {
+        synchronized (mServiceLock) {
+            if (mAudioPolicy != null && mAudioPolicy.asBinder().equals(deadAudioPolicy)) {
+                mAudioPolicy.asBinder().unlinkToDeath(this, 0);
+                mAudioPolicy = null;
+            }
+        }
+    }
+
+    private @Nullable IAudioPolicyService getAudioPolicy() {
+        synchronized (mServiceLock) {
+            return mAudioPolicy;
+        }
+    }
+
+    /*
+     * Does not block.
+     * @throws IllegalStateException for any failed connection
+     */
+    private @NonNull IAudioPolicyService getAudioPolicyOrInit() {
+        synchronized (mServiceLock) {
+            if (mAudioPolicy != null) {
+                return mAudioPolicy;
+            }
+            // Do not block while attempting to connect to APM. Defer to caller.
+            IAudioPolicyService ap = IAudioPolicyService.Stub.asInterface(
+                    ServiceManager.checkService(AUDIO_POLICY_SERVICE_NAME));
+            if (ap == null) {
+                throw new IllegalStateException(TAG + ": Unable to connect to AudioPolicy");
+            }
+            try {
+                ap.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                throw new IllegalStateException(
+                        TAG + ": Unable to link deathListener to AudioPolicy", e);
+            }
+            mAudioPolicy = ap;
+            return mAudioPolicy;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/security/OWNERS b/services/core/java/com/android/server/security/OWNERS
index 5c2d5ba..5bcc98b6 100644
--- a/services/core/java/com/android/server/security/OWNERS
+++ b/services/core/java/com/android/server/security/OWNERS
@@ -1,4 +1,4 @@
 # Bug component: 36824
 
 per-file *AttestationVerification* = file:/core/java/android/security/attestationverification/OWNERS
-per-file FileIntegrityService.java = victorhsieh@google.com
+per-file FileIntegrity*.java = victorhsieh@google.com
diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
index ad2e7e4..38093de 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
@@ -64,6 +64,8 @@
     private IAudioDeviceVolumeDispatcher.Stub mMockDispatcher =
             mock(IAudioDeviceVolumeDispatcher.Stub.class);
 
+    private AudioPolicyFacade mMockAudioPolicy = mock(AudioPolicyFacade.class);
+
     @Before
     public void setUp() throws Exception {
         mTestLooper = new TestLooper();
@@ -74,7 +76,7 @@
         mSystemServer = new NoOpSystemServerAdapter();
         mSettingsAdapter = new NoOpSettingsAdapter();
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
-                mSettingsAdapter, mTestLooper.getLooper()) {
+                mSettingsAdapter, mMockAudioPolicy, mTestLooper.getLooper()) {
             @Override
             public int getDeviceForStream(int stream) {
                 return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
index 7f54b63..4e9ac7c 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -17,6 +17,7 @@
 package com.android.server.audio;
 
 import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
@@ -45,6 +46,7 @@
     private SystemServerAdapter mSystemServer;
     private SettingsAdapter mSettingsAdapter;
     private TestLooper mTestLooper;
+    private AudioPolicyFacade mAudioPolicyMock = mock(AudioPolicyFacade.class);
 
     private AudioService mAudioService;
 
@@ -59,7 +61,7 @@
         mSystemServer = new NoOpSystemServerAdapter();
         mSettingsAdapter = new NoOpSettingsAdapter();
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
-                mSettingsAdapter, mTestLooper.getLooper()) {
+                mSettingsAdapter, mAudioPolicyMock, mTestLooper.getLooper()) {
             @Override
             public int getDeviceForStream(int stream) {
                 return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
index adcbe6b..88d57ac 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.audio;
 
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
@@ -28,6 +29,7 @@
 import android.content.Context;
 import android.media.AudioSystem;
 import android.os.Looper;
+import android.os.PermissionEnforcer;
 import android.os.UserHandle;
 import android.util.Log;
 
@@ -37,8 +39,11 @@
 
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 import org.mockito.Mock;
 import org.mockito.Spy;
 
@@ -49,11 +54,18 @@
 
     private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100;
 
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
     private Context mContext;
     private AudioSystemAdapter mAudioSystem;
-    @Spy private SystemServerAdapter mSpySystemServer;
     private SettingsAdapter mSettingsAdapter;
+
+    @Spy private NoOpSystemServerAdapter mSpySystemServer;
     @Mock private AppOpsManager mMockAppOpsManager;
+    @Mock private AudioPolicyFacade mMockAudioPolicy;
+    @Mock private PermissionEnforcer mMockPermissionEnforcer;
+
     // the class being unit-tested here
     private AudioService mAudioService;
 
@@ -67,13 +79,12 @@
         }
         mContext = InstrumentationRegistry.getTargetContext();
         mAudioSystem = new NoOpAudioSystemAdapter();
-        mSpySystemServer = spy(new NoOpSystemServerAdapter());
         mSettingsAdapter = new NoOpSettingsAdapter();
-        mMockAppOpsManager = mock(AppOpsManager.class);
         when(mMockAppOpsManager.noteOp(anyInt(), anyInt(), anyString(), anyString(), anyString()))
                 .thenReturn(AppOpsManager.MODE_ALLOWED);
         mAudioService = new AudioService(mContext, mAudioSystem, mSpySystemServer,
-                mSettingsAdapter, null, mMockAppOpsManager);
+                mSettingsAdapter, mMockAudioPolicy, null, mMockAppOpsManager,
+                mMockPermissionEnforcer);
     }
 
     /**
@@ -153,4 +164,15 @@
         Assert.assertEquals(ringMaxVol, mAudioService.getStreamVolume(
                 AudioSystem.STREAM_NOTIFICATION));
     }
+
+    @Test
+    public void testAudioPolicyException() throws Exception {
+        Log.i(TAG, "running testAudioPolicyException");
+        Assert.assertNotNull(mAudioService);
+        // Ensure that AudioPolicy inavailability doesn't bring down SystemServer
+        when(mMockAudioPolicy.isHotwordStreamSupported(anyBoolean())).thenThrow(
+                    new IllegalStateException(), new IllegalStateException());
+        Assert.assertEquals(false, mAudioService.isHotwordStreamSupported(false));
+        Assert.assertEquals(false, mAudioService.isHotwordStreamSupported(true));
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
index d89c6d5..77a6286 100644
--- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.Mockito.mock;
 
 import android.annotation.NonNull;
 import android.content.Context;
@@ -47,6 +48,7 @@
     private SystemServerAdapter mSystemServer;
     private SettingsAdapter mSettingsAdapter;
     private TestLooper mTestLooper;
+    private AudioPolicyFacade mAudioPolicyMock = mock(AudioPolicyFacade.class);
 
     private AudioService mAudioService;
 
@@ -67,7 +69,7 @@
         mSystemServer = new NoOpSystemServerAdapter();
         mSettingsAdapter = new NoOpSettingsAdapter();
         mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer,
-                mSettingsAdapter, mTestLooper.getLooper());
+                mSettingsAdapter, mAudioPolicyMock, mTestLooper.getLooper());
         mTestLooper.dispatchAll();
     }