Merge "[flexiglass] Bouncer action button ("Emergency Call"/"Return to Call")." into main
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index d668c69..defaa20 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -25,7 +25,9 @@
 import androidx.compose.animation.core.snap
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.Image
+import androidx.compose.foundation.combinedClickable
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -68,7 +70,6 @@
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.LayoutDirection
@@ -77,7 +78,9 @@
 import com.android.compose.PlatformButton
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.thenIf
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
+import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
 import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
@@ -195,6 +198,7 @@
     val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
     val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState()
     var dialog: Dialog? by remember { mutableStateOf(null) }
+    val actionButton: BouncerActionButtonModel? by viewModel.actionButton.collectAsState()
 
     Column(
         horizontalAlignment = Alignment.CenterHorizontally,
@@ -221,21 +225,7 @@
             )
         }
 
-        if (viewModel.isEmergencyButtonVisible) {
-            Button(
-                onClick = viewModel::onEmergencyServicesButtonClicked,
-                colors =
-                    ButtonDefaults.buttonColors(
-                        containerColor = MaterialTheme.colorScheme.tertiaryContainer,
-                        contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
-                    ),
-            ) {
-                Text(
-                    text = stringResource(com.android.internal.R.string.lockscreen_emergency_call),
-                    style = MaterialTheme.typography.bodyMedium,
-                )
-            }
-        }
+        actionButton?.let { BouncerActionButton(viewModel = it) }
 
         if (dialogMessage != null) {
             if (dialog == null) {
@@ -320,6 +310,37 @@
     }
 }
 
+/**
+ * Renders the action button on the bouncer, which triggers either Return to Call or Emergency Call.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun BouncerActionButton(
+    viewModel: BouncerActionButtonModel,
+    modifier: Modifier = Modifier,
+) {
+    Button(
+        onClick = viewModel.onClick,
+        modifier =
+            modifier.thenIf(viewModel.onLongClick != null) {
+                Modifier.combinedClickable(
+                    onClick = viewModel.onClick,
+                    onLongClick = viewModel.onLongClick,
+                )
+            },
+        colors =
+            ButtonDefaults.buttonColors(
+                containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+                contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
+            ),
+    ) {
+        Text(
+            text = viewModel.label,
+            style = MaterialTheme.typography.bodyMedium,
+        )
+    }
+}
+
 /** Renders the UI of the user switcher that's displayed on large screens next to the bouncer UI. */
 @Composable
 private fun UserSwitcher(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt
new file mode 100644
index 0000000..c2117ae
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 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.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.scene.SceneTestUtils
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class EmergencyServicesRepositoryImplTest : SysuiTestCase() {
+
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
+
+    private lateinit var underTest: EmergencyServicesRepository
+
+    @Before
+    fun setUp() {
+        overrideResource(
+            R.bool.config_enable_emergency_call_while_sim_locked,
+            ENABLE_EMERGENCY_CALL_WHILE_SIM_LOCKED
+        )
+
+        underTest =
+            EmergencyServicesRepository(
+                resources = context.resources,
+                applicationScope = testScope.backgroundScope,
+                configurationRepository = utils.configurationRepository,
+            )
+    }
+
+    @Test
+    fun enableEmergencyCallWhileSimLocked() =
+        testScope.runTest {
+            val enableEmergencyCallWhileSimLocked by
+                collectLastValue(underTest.enableEmergencyCallWhileSimLocked)
+
+            setEmergencyCallWhileSimLocked(isEnabled = false)
+            assertThat(enableEmergencyCallWhileSimLocked).isFalse()
+
+            setEmergencyCallWhileSimLocked(isEnabled = true)
+            assertThat(enableEmergencyCallWhileSimLocked).isTrue()
+        }
+
+    private fun TestScope.setEmergencyCallWhileSimLocked(isEnabled: Boolean) {
+        overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, isEnabled)
+        utils.configurationRepository.onConfigurationChange()
+        runCurrent()
+    }
+
+    companion object {
+        private const val ENABLE_EMERGENCY_CALL_WHILE_SIM_LOCKED = true
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
new file mode 100644
index 0000000..fde3ad7
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 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.app.ActivityTaskManager
+import android.telecom.TelecomManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.internal.logging.nano.MetricsProto
+import com.android.internal.logging.testing.FakeMetricsLogger
+import com.android.internal.util.EmergencyAffordanceManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.model.AuthenticationMethodModel
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags.REFACTOR_GETCURRENTUSER
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BouncerActionButtonInteractorTest : SysuiTestCase() {
+
+    @Mock private lateinit var activityTaskManager: ActivityTaskManager
+    @Mock private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager
+    @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor
+    @Mock private lateinit var tableLogger: TableLogBuffer
+    @Mock private lateinit var telecomManager: TelecomManager
+
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
+    private val metricsLogger = FakeMetricsLogger()
+    private var currentUserId: Int = 0
+    private var needsEmergencyAffordance = true
+
+    private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
+
+    private lateinit var underTest: BouncerActionButtonInteractor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        overrideResource(R.string.lockscreen_emergency_call, MESSAGE_EMERGENCY_CALL)
+        overrideResource(R.string.lockscreen_return_to_call, MESSAGE_RETURN_TO_CALL)
+        overrideResource(
+            R.bool.config_enable_emergency_call_while_sim_locked,
+            ENABLE_EMERGENCY_CALL_WHILE_SIM_LOCKED
+        )
+        whenever(selectedUserInteractor.getSelectedUserId()).thenReturn(currentUserId)
+        whenever(emergencyAffordanceManager.needsEmergencyAffordance())
+            .thenReturn(needsEmergencyAffordance)
+        whenever(telecomManager.isInCall).thenReturn(false)
+
+        utils.featureFlags.set(REFACTOR_GETCURRENTUSER, true)
+
+        mobileConnectionsRepository =
+            FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger)
+
+        utils.telephonyRepository.setHasTelephonyRadio(true)
+
+        underTest =
+            utils.bouncerActionButtonInteractor(
+                mobileConnectionsRepository = mobileConnectionsRepository,
+                activityTaskManager = activityTaskManager,
+                telecomManager = telecomManager,
+                emergencyAffordanceManager = emergencyAffordanceManager,
+                metricsLogger = metricsLogger,
+            )
+    }
+
+    @Test
+    fun noTelephonyRadio_noButton() =
+        testScope.runTest {
+            utils.telephonyRepository.setHasTelephonyRadio(false)
+            underTest =
+                utils.bouncerActionButtonInteractor(
+                    mobileConnectionsRepository = mobileConnectionsRepository,
+                    activityTaskManager = activityTaskManager,
+                    telecomManager = telecomManager,
+                )
+
+            val actionButton by collectLastValue(underTest.actionButton)
+            assertThat(actionButton).isNull()
+        }
+
+    @Test
+    fun noTelecomManager_noButton() =
+        testScope.runTest {
+            underTest =
+                utils.bouncerActionButtonInteractor(
+                    mobileConnectionsRepository = mobileConnectionsRepository,
+                    activityTaskManager = activityTaskManager,
+                    telecomManager = null,
+                )
+            val actionButton by collectLastValue(underTest.actionButton)
+            assertThat(actionButton).isNull()
+        }
+
+    @Test
+    fun duringCall_returnToCallButton() =
+        testScope.runTest {
+            val actionButton by collectLastValue(underTest.actionButton)
+            utils.telephonyRepository.setIsInCall(true)
+
+            assertThat(actionButton).isNotNull()
+            assertThat(actionButton?.label).isEqualTo(MESSAGE_RETURN_TO_CALL)
+            assertThat(actionButton?.onClick).isNotNull()
+            assertThat(actionButton?.onLongClick).isNull()
+
+            actionButton?.onClick?.invoke()
+
+            assertThat(metricsLogger.logs.size).isEqualTo(1)
+            assertThat(metricsLogger.logs.element().category)
+                .isEqualTo(MetricsProto.MetricsEvent.ACTION_EMERGENCY_CALL)
+            verify(activityTaskManager).stopSystemLockTaskMode()
+            verify(telecomManager).showInCallScreen(eq(false))
+        }
+
+    @Test
+    fun noCall_secureAuthMethod_emergencyCallButton() =
+        testScope.runTest {
+            val actionButton by collectLastValue(underTest.actionButton)
+            mobileConnectionsRepository.isAnySimSecure.value = false
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+            utils.telephonyRepository.setIsInCall(false)
+
+            assertThat(actionButton).isNotNull()
+            assertThat(actionButton?.label).isEqualTo(MESSAGE_EMERGENCY_CALL)
+            assertThat(actionButton?.onClick).isNotNull()
+            assertThat(actionButton?.onLongClick).isNotNull()
+
+            actionButton?.onClick?.invoke()
+
+            assertThat(metricsLogger.logs.size).isEqualTo(1)
+            assertThat(metricsLogger.logs.element().category)
+                .isEqualTo(MetricsProto.MetricsEvent.ACTION_EMERGENCY_CALL)
+            verify(activityTaskManager).stopSystemLockTaskMode()
+
+            // TODO(b/25189994): Test the activity has been started once we switch to the
+            //  ActivityStarter interface here.
+            verify(emergencyAffordanceManager, never()).performEmergencyCall()
+
+            actionButton?.onLongClick?.invoke()
+            verify(emergencyAffordanceManager).performEmergencyCall()
+        }
+
+    @Test
+    fun noCall_insecureAuthMethodButSecureSim_emergencyCallButton() =
+        testScope.runTest {
+            val actionButton by collectLastValue(underTest.actionButton)
+            mobileConnectionsRepository.isAnySimSecure.value = true
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+            utils.telephonyRepository.setIsInCall(false)
+
+            assertThat(actionButton).isNotNull()
+            assertThat(actionButton?.label).isEqualTo(MESSAGE_EMERGENCY_CALL)
+            assertThat(actionButton?.onClick).isNotNull()
+            assertThat(actionButton?.onLongClick).isNotNull()
+        }
+
+    @Test
+    fun noCall_insecure_noButton() =
+        testScope.runTest {
+            val actionButton by collectLastValue(underTest.actionButton)
+            mobileConnectionsRepository.isAnySimSecure.value = false
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+            utils.telephonyRepository.setIsInCall(false)
+
+            assertThat(actionButton).isNull()
+        }
+
+    @Test
+    fun noCall_simSecureButEmergencyNotSupported_noButton() =
+        testScope.runTest {
+            val actionButton by collectLastValue(underTest.actionButton)
+            mobileConnectionsRepository.isAnySimSecure.value = true
+            overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, false)
+            utils.configurationRepository.onConfigurationChange()
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+            utils.telephonyRepository.setIsInCall(false)
+            runCurrent()
+
+            assertThat(actionButton).isNull()
+        }
+
+    companion object {
+        private const val MESSAGE_EMERGENCY_CALL = "Emergency"
+        private const val MESSAGE_RETURN_TO_CALL = "Return to call"
+        private const val ENABLE_EMERGENCY_CALL_WHILE_SIM_LOCKED = true
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
new file mode 100644
index 0000000..262795f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.systemui.telephony.data.repository
+
+import android.telecom.TelecomManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.telephony.TelephonyListenerManager
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TelephonyRepositoryImplTest : SysuiTestCase() {
+
+    @Mock private lateinit var manager: TelephonyListenerManager
+    @Mock private lateinit var telecomManager: TelecomManager
+
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
+
+    private lateinit var underTest: TelephonyRepositoryImpl
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(telecomManager.isInCall).thenReturn(false)
+
+        underTest =
+            TelephonyRepositoryImpl(
+                applicationScope = testScope.backgroundScope,
+                applicationContext = context,
+                backgroundDispatcher = utils.testDispatcher,
+                manager = manager,
+                telecomManager = telecomManager,
+            )
+    }
+
+    @Test
+    fun callState() =
+        testScope.runTest {
+            val callState by collectLastValue(underTest.callState)
+            runCurrent()
+
+            val listenerCaptor = kotlinArgumentCaptor<TelephonyCallback.CallStateListener>()
+            verify(manager).addCallStateListener(listenerCaptor.capture())
+            val listener = listenerCaptor.value
+
+            listener.onCallStateChanged(0)
+            assertThat(callState).isEqualTo(0)
+
+            listener.onCallStateChanged(1)
+            assertThat(callState).isEqualTo(1)
+
+            listener.onCallStateChanged(2)
+            assertThat(callState).isEqualTo(2)
+        }
+
+    @Test
+    fun isInCall() =
+        testScope.runTest {
+            val isInCall by collectLastValue(underTest.isInCall)
+            runCurrent()
+
+            val listenerCaptor = kotlinArgumentCaptor<TelephonyCallback.CallStateListener>()
+            verify(manager).addCallStateListener(listenerCaptor.capture())
+            val listener = listenerCaptor.value
+            whenever(telecomManager.isInCall).thenReturn(true)
+            listener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK)
+
+            assertThat(isInCall).isTrue()
+
+            whenever(telecomManager.isInCall).thenReturn(false)
+            listener.onCallStateChanged(TelephonyManager.CALL_STATE_IDLE)
+
+            assertThat(isInCall).isFalse()
+        }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java
index 5de370f..7733841 100644
--- a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java
+++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java
@@ -28,7 +28,6 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.telecom.TelecomManager;
-import android.telephony.TelephonyManager;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
@@ -54,21 +53,20 @@
 /** View Controller for {@link com.android.keyguard.EmergencyButton}. */
 @KeyguardBouncerScope
 public class EmergencyButtonController extends ViewController<EmergencyButton> {
-    static final String LOG_TAG = "EmergencyButton";
+    private static final String TAG = "EmergencyButton";
     private final ConfigurationController mConfigurationController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private final TelephonyManager mTelephonyManager;
     private final PowerManager mPowerManager;
     private final ActivityTaskManager mActivityTaskManager;
-    private ShadeController mShadeController;
+    private final ShadeController mShadeController;
     private final TelecomManager mTelecomManager;
     private final MetricsLogger mMetricsLogger;
 
     private EmergencyButtonCallback mEmergencyButtonCallback;
-    private LockPatternUtils mLockPatternUtils;
-    private Executor mMainExecutor;
-    private Executor mBackgroundExecutor;
-    private SelectedUserInteractor mSelectedUserInteractor;
+    private final LockPatternUtils mLockPatternUtils;
+    private final Executor mMainExecutor;
+    private final Executor mBackgroundExecutor;
+    private final SelectedUserInteractor mSelectedUserInteractor;
 
     private final KeyguardUpdateMonitorCallback mInfoCallback =
             new KeyguardUpdateMonitorCallback() {
@@ -93,17 +91,18 @@
     @VisibleForTesting
     public EmergencyButtonController(@Nullable EmergencyButton view,
             ConfigurationController configurationController,
-            KeyguardUpdateMonitor keyguardUpdateMonitor, TelephonyManager telephonyManager,
-            PowerManager powerManager, ActivityTaskManager activityTaskManager,
+            KeyguardUpdateMonitor keyguardUpdateMonitor,
+            PowerManager powerManager,
+            ActivityTaskManager activityTaskManager,
             ShadeController shadeController,
-            @Nullable TelecomManager telecomManager, MetricsLogger metricsLogger,
+            @Nullable TelecomManager telecomManager,
+            MetricsLogger metricsLogger,
             LockPatternUtils lockPatternUtils,
             Executor mainExecutor, Executor backgroundExecutor,
             SelectedUserInteractor selectedUserInteractor) {
         super(view);
         mConfigurationController = configurationController;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mTelephonyManager = telephonyManager;
         mPowerManager = powerManager;
         mActivityTaskManager = activityTaskManager;
         mShadeController = shadeController;
@@ -183,7 +182,7 @@
                 } else {
                     mKeyguardUpdateMonitor.reportEmergencyCallAction(true /* bypassHandler */);
                     if (mTelecomManager == null) {
-                        Log.wtf(LOG_TAG, "TelecomManager was null, cannot launch emergency dialer");
+                        Log.wtf(TAG, "TelecomManager was null, cannot launch emergency dialer");
                         return;
                     }
                     Intent emergencyDialIntent =
@@ -212,10 +211,9 @@
     public static class Factory {
         private final ConfigurationController mConfigurationController;
         private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-        private final TelephonyManager mTelephonyManager;
         private final PowerManager mPowerManager;
         private final ActivityTaskManager mActivityTaskManager;
-        private ShadeController mShadeController;
+        private final ShadeController mShadeController;
         @Nullable
         private final TelecomManager mTelecomManager;
         private final MetricsLogger mMetricsLogger;
@@ -226,10 +224,12 @@
 
         @Inject
         public Factory(ConfigurationController configurationController,
-                KeyguardUpdateMonitor keyguardUpdateMonitor, TelephonyManager telephonyManager,
-                PowerManager powerManager, ActivityTaskManager activityTaskManager,
+                KeyguardUpdateMonitor keyguardUpdateMonitor,
+                PowerManager powerManager,
+                ActivityTaskManager activityTaskManager,
                 ShadeController shadeController,
-                @Nullable TelecomManager telecomManager, MetricsLogger metricsLogger,
+                @Nullable TelecomManager telecomManager,
+                MetricsLogger metricsLogger,
                 LockPatternUtils lockPatternUtils,
                 @Main Executor mainExecutor,
                 @Background Executor backgroundExecutor,
@@ -237,7 +237,6 @@
 
             mConfigurationController = configurationController;
             mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-            mTelephonyManager = telephonyManager;
             mPowerManager = powerManager;
             mActivityTaskManager = activityTaskManager;
             mShadeController = shadeController;
@@ -252,9 +251,9 @@
         /** Construct an {@link com.android.keyguard.EmergencyButtonController}. */
         public EmergencyButtonController create(EmergencyButton view) {
             return new EmergencyButtonController(view, mConfigurationController,
-                    mKeyguardUpdateMonitor, mTelephonyManager, mPowerManager, mActivityTaskManager,
-                    mShadeController, mTelecomManager, mMetricsLogger, mLockPatternUtils,
-                    mMainExecutor, mBackgroundExecutor, mSelectedUserInteractor);
+                    mKeyguardUpdateMonitor, mPowerManager, mActivityTaskManager, mShadeController,
+                    mTelecomManager, mMetricsLogger, mLockPatternUtils, mMainExecutor,
+                    mBackgroundExecutor, mSelectedUserInteractor);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepository.kt
new file mode 100644
index 0000000..bba0050
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepository.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.repository
+
+import android.content.res.Resources
+import com.android.internal.R
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Encapsulates Emergency Services related state. */
+@SysUISingleton
+class EmergencyServicesRepository
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Main private val resources: Resources,
+    configurationRepository: ConfigurationRepository,
+) {
+    /**
+     * Whether to enable emergency services calls while the SIM card is locked. This is disabled in
+     * certain countries that don't support this.
+     */
+    val enableEmergencyCallWhileSimLocked: StateFlow<Boolean> =
+        configurationRepository.onConfigurationChange
+            .map { getEnableEmergencyCallWhileSimLocked() }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = getEnableEmergencyCallWhileSimLocked()
+            )
+
+    private fun getEnableEmergencyCallWhileSimLocked(): Boolean {
+        return resources.getBoolean(R.bool.config_enable_emergency_call_while_sim_locked)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
new file mode 100644
index 0000000..f36ef66
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.annotation.SuppressLint
+import android.app.ActivityOptions
+import android.app.ActivityTaskManager
+import android.content.Context
+import android.content.Intent
+import android.os.UserHandle
+import android.telecom.TelecomManager
+import com.android.internal.R
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent
+import com.android.internal.util.EmergencyAffordanceManager
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository
+import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.doze.DozeLogger
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.EmergencyDialerConstants
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.withContext
+
+/**
+ * Encapsulates business logic and application state for the bouncer action button. The action
+ * button can support multiple different actions, depending on device state.
+ */
+@SysUISingleton
+class BouncerActionButtonInteractor
+@Inject
+constructor(
+    @Application private val applicationContext: Context,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val repository: EmergencyServicesRepository,
+    // TODO(b/307977401): Replace with `MobileConnectionsInteractor` when available.
+    private val mobileConnectionsRepository: MobileConnectionsRepository,
+    private val telephonyInteractor: TelephonyInteractor,
+    private val authenticationInteractor: AuthenticationInteractor,
+    private val selectedUserInteractor: SelectedUserInteractor,
+    private val activityTaskManager: ActivityTaskManager,
+    private val telecomManager: TelecomManager?,
+    private val emergencyAffordanceManager: EmergencyAffordanceManager,
+    private val emergencyDialerIntentFactory: EmergencyDialerIntentFactory,
+    private val metricsLogger: MetricsLogger,
+    private val dozeLogger: DozeLogger,
+) {
+    /** The bouncer action button. If `null`, the button should not be shown. */
+    val actionButton: Flow<BouncerActionButtonModel?> =
+        if (telecomManager == null || !telephonyInteractor.hasTelephonyRadio) {
+            flowOf(null)
+        } else {
+            merge(
+                    telephonyInteractor.isInCall.asUnitFlow,
+                    mobileConnectionsRepository.isAnySimSecure.asUnitFlow,
+                    authenticationInteractor.authenticationMethod.asUnitFlow,
+                    repository.enableEmergencyCallWhileSimLocked.asUnitFlow,
+                )
+                .map {
+                    when {
+                        isReturnToCallButton() -> returnToCallButtonModel
+                        isEmergencyCallButton() -> emergencyCallButtonModel
+                        else -> null // Do not show the button.
+                    }
+                }
+                .distinctUntilChanged()
+        }
+
+    private val returnToCallButtonModel: BouncerActionButtonModel by lazy {
+        BouncerActionButtonModel(
+            label = applicationContext.getString(R.string.lockscreen_return_to_call),
+            onClick = {
+                prepareToPerformAction()
+                returnToCall()
+            },
+            onLongClick = null
+        )
+    }
+
+    private val emergencyCallButtonModel: BouncerActionButtonModel by lazy {
+        BouncerActionButtonModel(
+            label = applicationContext.getString(R.string.lockscreen_emergency_call),
+            onClick = {
+                prepareToPerformAction()
+                dozeLogger.logEmergencyCall()
+                startEmergencyDialerActivity()
+            },
+            // TODO(b/308001302): The long click detector doesn't work properly, investigate.
+            onLongClick = {
+                if (emergencyAffordanceManager.needsEmergencyAffordance()) {
+                    prepareToPerformAction()
+
+                    // TODO(b/298026988): Check that !longPressWasDragged before invoking.
+                    emergencyAffordanceManager.performEmergencyCall()
+                }
+            }
+        )
+    }
+
+    private fun startEmergencyDialerActivity() {
+        emergencyDialerIntentFactory()?.apply {
+            flags =
+                Intent.FLAG_ACTIVITY_NEW_TASK or
+                    Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or
+                    Intent.FLAG_ACTIVITY_CLEAR_TOP
+
+            putExtra(
+                EmergencyDialerConstants.EXTRA_ENTRY_TYPE,
+                EmergencyDialerConstants.ENTRY_TYPE_LOCKSCREEN_BUTTON,
+            )
+
+            // TODO(b/25189994): Use the ActivityStarter interface instead.
+            applicationContext.startActivityAsUser(
+                this,
+                ActivityOptions.makeCustomAnimation(applicationContext, 0, 0).toBundle(),
+                UserHandle(selectedUserInteractor.getSelectedUserId())
+            )
+        }
+    }
+
+    private fun isReturnToCallButton() = telephonyInteractor.isInCall.value
+
+    private suspend fun isEmergencyCallButton(): Boolean {
+        return if (mobileConnectionsRepository.getIsAnySimSecure()) {
+            // Some countries can't handle emergency calls while SIM is locked.
+            repository.enableEmergencyCallWhileSimLocked.value
+        } else {
+            // Only show if there is a secure screen (password/pin/pattern/SIM pin/SIM puk).
+            withContext(backgroundDispatcher) {
+                authenticationInteractor.getAuthenticationMethod().isSecure
+            }
+        }
+    }
+
+    private fun prepareToPerformAction() {
+        // TODO(b/308001302): Trigger occlusion and resetting bouncer state.
+        metricsLogger.action(MetricsEvent.ACTION_EMERGENCY_CALL)
+        activityTaskManager.stopSystemLockTaskMode()
+    }
+
+    @SuppressLint("MissingPermission")
+    private fun returnToCall() {
+        telecomManager?.showInCallScreen(/* showDialpad = */ false)
+    }
+
+    private val <T> Flow<T>.asUnitFlow: Flow<Unit>
+        get() = map {}
+}
+
+/**
+ * Creates an intent to launch the Emergency Services dialer. If no [TelecomManager] is present,
+ * returns `null`.
+ */
+interface EmergencyDialerIntentFactory {
+    operator fun invoke(): Intent?
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt
new file mode 100644
index 0000000..e398c93
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 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 android.content.Intent
+import android.telecom.TelecomManager
+import com.android.internal.util.EmergencyAffordanceManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import dagger.Module
+import dagger.Provides
+
+/** Module for providing interactor-related objects for the bouncer. */
+@Module
+object BouncerInteractorModule {
+
+    @Provides
+    fun emergencyDialerIntentFactory(
+        telecomManager: TelecomManager?
+    ): EmergencyDialerIntentFactory {
+        return object : EmergencyDialerIntentFactory {
+            override fun invoke(): Intent? {
+                return telecomManager?.createLaunchEmergencyDialerIntent(/* number = */ null)
+            }
+        }
+    }
+
+    @Provides
+    @SysUISingleton
+    fun emergencyAffordanceManager(
+        @Application applicationContext: Context,
+    ): EmergencyAffordanceManager {
+        return EmergencyAffordanceManager(applicationContext)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerActionButtonModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerActionButtonModel.kt
new file mode 100644
index 0000000..7f1730c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerActionButtonModel.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 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.shared.model
+
+/** Models the action button on the bouncer. */
+data class BouncerActionButtonModel(
+    /** The text to be shown on the button. */
+    val label: String,
+
+    /** The action to perform when the user clicks on the button. */
+    val onClick: () -> Unit,
+
+    /**
+     * The action to perform when the user long-clicks on the button. When not provided, long-clicks
+     * will be treated as regular clicks.
+     */
+    val onLongClick: (() -> Unit)? = null,
+)
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
index ef0609a..0f77724 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -21,14 +21,15 @@
 import androidx.core.graphics.drawable.toBitmap
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
-import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
 import com.android.systemui.user.ui.viewmodel.UserViewModel
@@ -59,10 +60,10 @@
     private val bouncerInteractor: BouncerInteractor,
     authenticationInteractor: AuthenticationInteractor,
     flags: SceneContainerFlags,
-    private val telephonyInteractor: TelephonyInteractor,
     selectedUser: Flow<UserViewModel>,
     users: Flow<List<UserViewModel>>,
     userSwitcherMenu: Flow<List<UserActionViewModel>>,
+    actionButtonInteractor: BouncerActionButtonInteractor,
 ) {
     val selectedUserImage: StateFlow<Bitmap?> =
         selectedUser
@@ -150,8 +151,16 @@
                     ),
             )
 
-    val isEmergencyButtonVisible: Boolean
-        get() = telephonyInteractor.hasTelephonyRadio
+    /**
+     * The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not
+     * be shown.
+     */
+    val actionButton: StateFlow<BouncerActionButtonModel?> =
+        actionButtonInteractor.actionButton.stateIn(
+            scope = applicationScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = null
+        )
 
     init {
         if (flags.isEnabled()) {
@@ -176,11 +185,6 @@
         }
     }
 
-    /** Notifies that the emergency services button was clicked. */
-    fun onEmergencyServicesButtonClicked() {
-        // TODO(b/280877228): implement this
-    }
-
     /** Notifies that a throttling dialog has been dismissed by the user. */
     fun onThrottlingDialogDismissed() {
         _throttlingDialogMessage.value = null
@@ -271,8 +275,8 @@
         bouncerInteractor: BouncerInteractor,
         authenticationInteractor: AuthenticationInteractor,
         flags: SceneContainerFlags,
-        telephonyInteractor: TelephonyInteractor,
         userSwitcherViewModel: UserSwitcherViewModel,
+        actionButtonInteractor: BouncerActionButtonInteractor,
     ): BouncerViewModel {
         return BouncerViewModel(
             applicationContext = applicationContext,
@@ -281,10 +285,10 @@
             bouncerInteractor = bouncerInteractor,
             authenticationInteractor = authenticationInteractor,
             flags = flags,
-            telephonyInteractor = telephonyInteractor,
             selectedUser = userSwitcherViewModel.selectedUser,
             users = userSwitcherViewModel.users,
             userSwitcherMenu = userSwitcherViewModel.menu,
+            actionButtonInteractor = actionButtonInteractor,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
index e449274..7fa762a 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
@@ -44,11 +44,15 @@
 interface ConfigurationRepository {
     /** Called whenever ui mode, theme or configuration has changed. */
     val onAnyConfigurationChange: Flow<Unit>
+
+    /** Called whenever the configuration has changed. */
+    val onConfigurationChange: Flow<Unit>
+
     val scaleForResolution: Flow<Float>
 
     fun getResolutionScale(): Float
 
-    /** Convience to context.resources.getDimensionPixelSize() */
+    /** Convenience to context.resources.getDimensionPixelSize() */
     fun getDimensionPixelSize(id: Int): Int
 }
 
@@ -87,7 +91,7 @@
             awaitClose { configurationController.removeCallback(callback) }
         }
 
-    private val configurationChange: Flow<Unit> =
+    override val onConfigurationChange: Flow<Unit> =
         ConflatedCallbackFlow.conflatedCallbackFlow {
             val callback =
                 object : ConfigurationController.ConfigurationListener {
@@ -100,7 +104,7 @@
         }
 
     override val scaleForResolution: StateFlow<Float> =
-        configurationChange
+        onConfigurationChange
             .mapLatest { getResolutionScale() }
             .distinctUntilChanged()
             .stateIn(scope, SharingStarted.WhileSubscribed(), getResolutionScale())
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 58dcf069..8eb80ad 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -37,6 +37,7 @@
 import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
 import com.android.systemui.biometrics.dagger.BiometricsModule;
 import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule;
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractorModule;
 import com.android.systemui.bouncer.ui.BouncerViewModule;
 import com.android.systemui.classifier.FalsingModule;
 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
@@ -166,6 +167,7 @@
         AuthenticationModule.class,
         BiometricsModule.class,
         BiometricsDomainLayerModule.class,
+        BouncerInteractorModule.class,
         BouncerViewModule.class,
         ClipboardOverlayModule.class,
         ClockRegistryModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt
index 934f310..8242087 100644
--- a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt
@@ -32,12 +32,12 @@
 import com.android.systemui.power.shared.model.WakefulnessModel
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import javax.inject.Inject
 
 /** Defines interface for classes that act as source of truth for power-related data. */
 interface PowerRepository {
@@ -67,15 +67,22 @@
     /** Wakes up the device. */
     fun wakeUp(why: String, @PowerManager.WakeReason wakeReason: Int)
 
-    /** Notifies the power repository that a user touch happened. */
-    fun userTouch()
+    /**
+     * Notifies the power repository that a user touch happened.
+     *
+     * @param noChangeLights If true, does not cause the keyboard backlight to turn on because of
+     *   this event. This is set when the power key is pressed. We want the device to stay on while
+     *   the button is down, but we're about to turn off the screen so we don't want the keyboard
+     *   backlight to turn on again. Otherwise the lights flash on and then off and it looks weird.
+     */
+    fun userTouch(noChangeLights: Boolean = false)
 
     /** Updates the wakefulness state, keeping previous values by default. */
     fun updateWakefulness(
-            rawState: WakefulnessState = wakefulness.value.internalWakefulnessState,
-            lastWakeReason: WakeSleepReason = wakefulness.value.lastWakeReason,
-            lastSleepReason: WakeSleepReason = wakefulness.value.lastSleepReason,
-            powerButtonLaunchGestureTriggered: Boolean =
+        rawState: WakefulnessState = wakefulness.value.internalWakefulnessState,
+        lastWakeReason: WakeSleepReason = wakefulness.value.lastWakeReason,
+        lastSleepReason: WakeSleepReason = wakefulness.value.lastSleepReason,
+        powerButtonLaunchGestureTriggered: Boolean =
             wakefulness.value.powerButtonLaunchGestureTriggered,
     )
 
@@ -121,10 +128,10 @@
     override val wakefulness = _wakefulness.asStateFlow()
 
     override fun updateWakefulness(
-            rawState: WakefulnessState,
-            lastWakeReason: WakeSleepReason,
-            lastSleepReason: WakeSleepReason,
-            powerButtonLaunchGestureTriggered: Boolean,
+        rawState: WakefulnessState,
+        lastWakeReason: WakeSleepReason,
+        lastSleepReason: WakeSleepReason,
+        powerButtonLaunchGestureTriggered: Boolean,
     ) {
         _wakefulness.value =
             WakefulnessModel(
@@ -150,11 +157,11 @@
         )
     }
 
-    override fun userTouch() {
+    override fun userTouch(noChangeLights: Boolean) {
         manager.userActivity(
             systemClock.uptimeMillis(),
             PowerManager.USER_ACTIVITY_EVENT_TOUCH,
-            /* flags= */ 0,
+            if (noChangeLights) PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS else 0,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
index 13c9964..9471574 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -100,6 +100,17 @@
     val isAnySimSecure: Flow<Boolean>
 
     /**
+     * Returns whether any active SIM on the device is in
+     * [android.telephony.TelephonyManager.SIM_STATE_PIN_REQUIRED] or
+     * [android.telephony.TelephonyManager.SIM_STATE_PUK_REQUIRED] or
+     * [android.telephony.TelephonyManager.SIM_STATE_PERM_DISABLED].
+     *
+     * Note: Unfortunately, we cannot name this [isAnySimSecure] due to a conflict with the flow
+     * name above (Java code-gen is having issues with it).
+     */
+    fun getIsAnySimSecure(): Boolean
+
+    /**
      * Checks if any subscription has [android.telephony.TelephonyManager.getEmergencyCallbackMode]
      * == true
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
index 87dd17e..8a8e33e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
@@ -152,6 +152,7 @@
         activeRepo.flatMapLatest { it.defaultMobileIconGroup }
 
     override val isAnySimSecure: Flow<Boolean> = activeRepo.flatMapLatest { it.isAnySimSecure }
+    override fun getIsAnySimSecure(): Boolean = activeRepo.value.getIsAnySimSecure()
 
     override val defaultDataSubId: StateFlow<Int> =
         activeRepo
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
index 2ecd435..2b3c632 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -136,7 +136,8 @@
 
     override val defaultMobileIconGroup = flowOf(TelephonyIcons.THREE_G)
 
-    override val isAnySimSecure: Flow<Boolean> = flowOf(false)
+    override val isAnySimSecure: Flow<Boolean> = flowOf(getIsAnySimSecure())
+    override fun getIsAnySimSecure(): Boolean = false
 
     override val defaultMobileIconMapping = MutableStateFlow(TelephonyIcons.ICON_NAME_TO_ICON)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index cf7bf86..2a510e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -262,7 +262,7 @@
                     object : KeyguardUpdateMonitorCallback() {
                         override fun onSimStateChanged(subId: Int, slotId: Int, simState: Int) {
                             logger.logOnSimStateChanged()
-                            trySend(keyguardUpdateMonitor.isSimPinSecure)
+                            trySend(getIsAnySimSecure())
                         }
                     }
                 keyguardUpdateMonitor.registerCallback(callback)
@@ -277,6 +277,8 @@
             )
             .distinctUntilChanged()
 
+    override fun getIsAnySimSecure() = keyguardUpdateMonitor.isSimPinSecure
+
     override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository =
         getOrCreateRepoForSubId(subId)
 
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
index 3b300249..b1b6014 100644
--- a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
@@ -17,23 +17,40 @@
 
 package com.android.systemui.telephony.data.repository
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.content.pm.PackageManager
+import android.telecom.TelecomManager
 import android.telephony.Annotation
 import android.telephony.TelephonyCallback
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.telephony.TelephonyListenerManager
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
 
 /** Defines interface for classes that encapsulate _some_ telephony-related state. */
 interface TelephonyRepository {
     /** The state of the current call. */
     @Annotation.CallState val callState: Flow<Int>
 
+    /**
+     * Whether there is an ongoing phone call (can be in dialing, ringing, active or holding states)
+     * originating from either a manager or self-managed {@link ConnectionService}.
+     */
+    val isInCall: StateFlow<Boolean>
+
     /** Whether the device has a radio that can be used for telephony. */
     val hasTelephonyRadio: Boolean
 }
@@ -49,18 +66,35 @@
 class TelephonyRepositoryImpl
 @Inject
 constructor(
+    @Application private val applicationScope: CoroutineScope,
     @Application private val applicationContext: Context,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val manager: TelephonyListenerManager,
+    private val telecomManager: TelecomManager?,
 ) : TelephonyRepository {
+
     @Annotation.CallState
     override val callState: Flow<Int> = conflatedCallbackFlow {
-        val listener = TelephonyCallback.CallStateListener { state -> trySend(state) }
+        val listener = TelephonyCallback.CallStateListener(::trySend)
 
         manager.addCallStateListener(listener)
 
         awaitClose { manager.removeCallStateListener(listener) }
     }
 
-    override val hasTelephonyRadio: Boolean
-        get() = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
+    @SuppressLint("MissingPermission")
+    override val isInCall: StateFlow<Boolean> =
+        if (telecomManager == null) {
+                flowOf(false)
+            } else {
+                callState.map { withContext(backgroundDispatcher) { telecomManager.isInCall } }
+            }
+            .stateIn(
+                applicationScope,
+                SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
+
+    override val hasTelephonyRadio =
+        applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt
index 4642f55..4b0e5d1 100644
--- a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt
@@ -22,17 +22,18 @@
 import com.android.systemui.telephony.data.repository.TelephonyRepository
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 
 /** Hosts business logic related to telephony. */
 @SysUISingleton
 class TelephonyInteractor
 @Inject
 constructor(
-    private val repository: TelephonyRepository,
+    repository: TelephonyRepository,
 ) {
     @Annotation.CallState val callState: Flow<Int> = repository.callState
 
-    /** Whether the device has a radio that can be used for telephony. */
-    val hasTelephonyRadio: Boolean
-        get() = repository.hasTelephonyRadio
+    val isInCall: StateFlow<Boolean> = repository.isInCall
+
+    val hasTelephonyRadio: Boolean = repository.hasTelephonyRadio
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
index 78fb7a4..3ed05aa 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
@@ -34,10 +34,10 @@
     @UserIdInt
     @JvmOverloads
     fun getSelectedUserId(bypassFlag: Boolean = false): Int {
-        if (bypassFlag || flags.isEnabled(REFACTOR_GETCURRENTUSER)) {
-            return repository.getSelectedUserInfo().id
+        return if (bypassFlag || flags.isEnabled(REFACTOR_GETCURRENTUSER)) {
+            repository.getSelectedUserInfo().id
         } else {
-            return KeyguardUpdateMonitor.getCurrentUser()
+            KeyguardUpdateMonitor.getCurrentUser()
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt
index c61b11a..9a95b17 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt
@@ -48,7 +48,6 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class EmergencyButtonControllerTest : SysuiTestCase() {
-    lateinit var underTest: EmergencyButtonController
     @Mock lateinit var emergencyButton: EmergencyButton
     @Mock lateinit var configurationController: ConfigurationController
     @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@@ -61,10 +60,13 @@
     @Mock lateinit var lockPatternUtils: LockPatternUtils
     @Mock lateinit var packageManager: PackageManager
     @Mock lateinit var mSelectedUserInteractor: SelectedUserInteractor
+
     val fakeSystemClock = FakeSystemClock()
     val mainExecutor = FakeExecutor(fakeSystemClock)
     val backgroundExecutor = FakeExecutor(fakeSystemClock)
 
+    lateinit var underTest: EmergencyButtonController
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -73,7 +75,6 @@
                 emergencyButton,
                 configurationController,
                 keyguardUpdateMonitor,
-                telephonyManager,
                 powerManager,
                 activityTaskManager,
                 shadeController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt
index a049191..44c57f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt
@@ -2,15 +2,13 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.keyguard.ViewMediatorCallback
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -18,32 +16,34 @@
 import org.mockito.Mockito
 import org.mockito.MockitoAnnotations
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class KeyguardBouncerRepositoryTest : SysuiTestCase() {
 
     @Mock private lateinit var systemClock: SystemClock
-    @Mock private lateinit var viewMediatorCallback: ViewMediatorCallback
     @Mock private lateinit var bouncerLogger: TableLogBuffer
+
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
+
     lateinit var underTest: KeyguardBouncerRepository
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        val testCoroutineScope = TestCoroutineScope()
         underTest =
             KeyguardBouncerRepositoryImpl(
                 systemClock,
-                testCoroutineScope,
+                testScope.backgroundScope,
                 bouncerLogger,
             )
     }
 
     @Test
-    fun changingFlowValueTriggersLogging() = runBlocking {
-        underTest.setPrimaryShow(true)
-        Mockito.verify(bouncerLogger)
-            .logChange(eq(""), eq("PrimaryBouncerShow"), value = eq(false), any())
-    }
+    fun changingFlowValueTriggersLogging() =
+        testScope.runTest {
+            underTest.setPrimaryShow(true)
+            Mockito.verify(bouncerLogger)
+                .logChange(eq(""), eq("PrimaryBouncerShow"), value = eq(false), any())
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 2c97809..c159b66 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -45,6 +45,7 @@
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
+    private val actionButtonInteractor = utils.bouncerActionButtonInteractor()
     private val deviceEntryInteractor =
         utils.deviceEntryInteractor(
             authenticationInteractor = authenticationInteractor,
@@ -60,6 +61,7 @@
         utils.bouncerViewModel(
             bouncerInteractor = bouncerInteractor,
             authenticationInteractor = authenticationInteractor,
+            actionButtonInteractor = actionButtonInteractor,
         )
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
index b75f3e0..2cc8f0a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
@@ -52,8 +52,7 @@
 @RunWith(AndroidJUnit4::class)
 @kotlinx.coroutines.ExperimentalCoroutinesApi
 class KeyguardBouncerViewModelTest : SysuiTestCase() {
-    lateinit var underTest: KeyguardBouncerViewModel
-    lateinit var bouncerInteractor: PrimaryBouncerInteractor
+
     @Mock lateinit var bouncerView: BouncerView
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
@@ -62,9 +61,13 @@
     @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
     @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+
+    lateinit var bouncerInteractor: PrimaryBouncerInteractor
     private val mainHandler = FakeHandler(Looper.getMainLooper())
     val repository = FakeKeyguardBouncerRepository()
 
+    lateinit var underTest: KeyguardBouncerViewModel
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index ba8dcef..390742031 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -61,7 +61,9 @@
         utils.bouncerViewModel(
             bouncerInteractor = bouncerInteractor,
             authenticationInteractor = authenticationInteractor,
+            actionButtonInteractor = utils.bouncerActionButtonInteractor(),
         )
+
     private val underTest =
         PasswordBouncerViewModel(
             viewModelScope = testScope.backgroundScope,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index bfaa6ed..47db4f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -64,6 +64,7 @@
         utils.bouncerViewModel(
             bouncerInteractor = bouncerInteractor,
             authenticationInteractor = authenticationInteractor,
+            actionButtonInteractor = utils.bouncerActionButtonInteractor(),
         )
     private val underTest =
         PatternBouncerViewModel(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 7873899..3ddac7e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -63,6 +63,7 @@
         utils.bouncerViewModel(
             bouncerInteractor = bouncerInteractor,
             authenticationInteractor = authenticationInteractor,
+            actionButtonInteractor = utils.bouncerActionButtonInteractor(),
         )
     private val underTest =
         PinBouncerViewModel(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt
index f566efe6..f3b114d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt
@@ -210,6 +210,22 @@
         assertThat(flagsCaptor.value).isNotEqualTo(PowerManager.USER_ACTIVITY_FLAG_INDIRECT)
     }
 
+    @Test
+    fun userActivity_notifiesPowerManager_noChangeLightsTrue() {
+        systemClock.setUptimeMillis(345000)
+
+        underTest.userTouch(noChangeLights = true)
+
+        val flagsCaptor = argumentCaptor<Int>()
+        verify(manager)
+            .userActivity(
+                eq(345000L),
+                eq(PowerManager.USER_ACTIVITY_EVENT_TOUCH),
+                capture(flagsCaptor)
+            )
+        assertThat(flagsCaptor.value).isEqualTo(PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS)
+    }
+
     private fun verifyRegistered() {
         // We must verify with all arguments, even those that are optional because they have default
         // values because Mockito is forcing us to. Once we can use mockito-kotlin, we should be
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 3feb5bf..e84d274 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -18,17 +18,23 @@
 
 package com.android.systemui.scene
 
+import android.telecom.TelecomManager
+import android.telephony.TelephonyManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.internal.util.EmergencyAffordanceManager
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
+import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.model.SysUiState
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
@@ -49,7 +55,9 @@
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -63,6 +71,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
 
 /**
  * Integration test cases for the Scene Framework.
@@ -87,6 +99,10 @@
 @RunWith(AndroidJUnit4::class)
 class SceneFrameworkIntegrationTest : SysuiTestCase() {
 
+    @Mock private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager
+    @Mock private lateinit var tableLogger: TableLogBuffer
+    @Mock private lateinit var telecomManager: TelecomManager
+
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
     private val sceneContainerConfig = utils.fakeSceneContainerConfig()
@@ -123,11 +139,10 @@
             authenticationInteractor = authenticationInteractor,
             sceneInteractor = sceneInteractor,
         )
-    private val bouncerViewModel =
-        utils.bouncerViewModel(
-            bouncerInteractor = bouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-        )
+
+    private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
+    private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor
+    private lateinit var bouncerViewModel: BouncerViewModel
 
     private val lockscreenSceneViewModel =
         LockscreenSceneViewModel(
@@ -141,7 +156,6 @@
         )
 
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
-    private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
 
     private var mobileIconsViewModel: MobileIconsViewModel =
         MobileIconsViewModel(
@@ -155,7 +169,7 @@
                     FakeMobileConnectionsRepository(),
                 ),
             constants = mock(),
-            flags,
+            utils.featureFlags,
             scope = testScope.backgroundScope,
         )
 
@@ -173,6 +187,39 @@
 
     @Before
     fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, true)
+        whenever(telecomManager.isInCall).thenReturn(false)
+        whenever(emergencyAffordanceManager.needsEmergencyAffordance()).thenReturn(true)
+
+        utils.featureFlags.apply {
+            set(Flags.NEW_NETWORK_SLICE_UI, false)
+            set(Flags.REFACTOR_GETCURRENTUSER, true)
+        }
+
+        mobileConnectionsRepository =
+            FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger)
+        mobileConnectionsRepository.isAnySimSecure.value = true
+
+        utils.telephonyRepository.apply {
+            setHasTelephonyRadio(true)
+            setCallState(TelephonyManager.CALL_STATE_IDLE)
+            setIsInCall(false)
+        }
+
+        bouncerActionButtonInteractor =
+            utils.bouncerActionButtonInteractor(
+                mobileConnectionsRepository = mobileConnectionsRepository,
+                telecomManager = telecomManager,
+                emergencyAffordanceManager = emergencyAffordanceManager,
+            )
+        bouncerViewModel =
+            utils.bouncerViewModel(
+                bouncerInteractor = bouncerInteractor,
+                authenticationInteractor = authenticationInteractor,
+                actionButtonInteractor = bouncerActionButtonInteractor,
+            )
+
         shadeHeaderViewModel =
             ShadeHeaderViewModel(
                 applicationScope = testScope.backgroundScope,
@@ -395,6 +442,45 @@
             emulateUiSceneTransition()
         }
 
+    @Test
+    fun bouncerActionButtonClick_opensEmergencyServicesDialer() =
+        testScope.runTest {
+            setAuthMethod(DomainLayerAuthenticationMethodModel.Password)
+            val upDestinationSceneKey by
+                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
+            assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Bouncer)
+            emulateUserDrivenTransition(to = upDestinationSceneKey)
+
+            val bouncerActionButton by collectLastValue(bouncerViewModel.actionButton)
+            assertWithMessage("Bouncer action button not visible")
+                .that(bouncerActionButton)
+                .isNotNull()
+            bouncerActionButton?.onClick?.invoke()
+            runCurrent()
+
+            // TODO(b/298026988): Assert that an activity was started once we use ActivityStarter.
+        }
+
+    @Test
+    fun bouncerActionButtonClick_duringCall_returnsToCall() =
+        testScope.runTest {
+            setAuthMethod(DomainLayerAuthenticationMethodModel.Password)
+            startPhoneCall()
+            val upDestinationSceneKey by
+                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
+            assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Bouncer)
+            emulateUserDrivenTransition(to = upDestinationSceneKey)
+
+            val bouncerActionButton by collectLastValue(bouncerViewModel.actionButton)
+            assertWithMessage("Bouncer action button not visible during call")
+                .that(bouncerActionButton)
+                .isNotNull()
+            bouncerActionButton?.onClick?.invoke()
+            runCurrent()
+
+            verify(telecomManager).showInCallScreen(any())
+        }
+
     /**
      * Asserts that the current scene in the view-model matches what's expected.
      *
@@ -438,6 +524,17 @@
         runCurrent()
     }
 
+    /** Emulates a phone call in progress. */
+    private fun TestScope.startPhoneCall() {
+        whenever(telecomManager.isInCall).thenReturn(true)
+        utils.telephonyRepository.apply {
+            setHasTelephonyRadio(true)
+            setIsInCall(true)
+            setCallState(TelephonyManager.CALL_STATE_OFFHOOK)
+        }
+        runCurrent()
+    }
+
     /**
      * Emulates a complete transition in the UI from whatever the current scene is in the UI to
      * whatever the current scene should be, based on the value in
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 74b2723..cce038f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -94,6 +94,7 @@
     override val defaultMobileIconGroup = _defaultMobileIconGroup
 
     override val isAnySimSecure = MutableStateFlow(false)
+    override fun getIsAnySimSecure(): Boolean = isAnySimSecure.value
 
     private var isInEcmMode: Boolean = false
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index ac75d4f..03f3005 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -92,7 +92,6 @@
 // to run the callback and this makes the looper place nicely with TestScope etc.
 @TestableLooper.RunWithLooper
 class MobileConnectionsRepositoryTest : SysuiTestCase() {
-    private lateinit var underTest: MobileConnectionsRepositoryImpl
 
     private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory
     private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory
@@ -101,6 +100,7 @@
     private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
     private lateinit var wifiRepository: WifiRepository
     private lateinit var carrierConfigRepository: CarrierConfigRepository
+
     @Mock private lateinit var connectivityManager: ConnectivityManager
     @Mock private lateinit var subscriptionManager: SubscriptionManager
     @Mock private lateinit var telephonyManager: TelephonyManager
@@ -115,6 +115,8 @@
     private val dispatcher = StandardTestDispatcher()
     private val testScope = TestScope(dispatcher)
 
+    private lateinit var underTest: MobileConnectionsRepositoryImpl
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -1179,6 +1181,16 @@
         }
 
     @Test
+    fun getIsAnySimSecure_delegatesCallToKeyguardUpdateMonitor() =
+        testScope.runTest {
+            assertThat(underTest.getIsAnySimSecure()).isFalse()
+
+            whenever(updateMonitor.isSimPinSecure).thenReturn(true)
+
+            assertThat(underTest.getIsAnySimSecure()).isTrue()
+        }
+
+    @Test
     fun noSubscriptionsInEcmMode_notInEcmMode() =
         testScope.runTest {
             whenever(telephonyManager.emergencyCallbackMode).thenReturn(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
deleted file mode 100644
index 0209030..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
+++ /dev/null
@@ -1,83 +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.systemui.telephony.data.repository
-
-import android.telephony.TelephonyCallback
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.telephony.TelephonyListenerManager
-import com.android.systemui.util.mockito.kotlinArgumentCaptor
-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 org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(JUnit4::class)
-class TelephonyRepositoryImplTest : SysuiTestCase() {
-
-    @Mock private lateinit var manager: TelephonyListenerManager
-
-    private lateinit var underTest: TelephonyRepositoryImpl
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        underTest =
-            TelephonyRepositoryImpl(
-                applicationContext = context,
-                manager = manager,
-            )
-    }
-
-    @Test
-    fun callState() =
-        runBlocking(IMMEDIATE) {
-            var callState: Int? = null
-            val job = underTest.callState.onEach { callState = it }.launchIn(this)
-            val listenerCaptor = kotlinArgumentCaptor<TelephonyCallback.CallStateListener>()
-            verify(manager).addCallStateListener(listenerCaptor.capture())
-            val listener = listenerCaptor.value
-
-            listener.onCallStateChanged(0)
-            assertThat(callState).isEqualTo(0)
-
-            listener.onCallStateChanged(1)
-            assertThat(callState).isEqualTo(1)
-
-            listener.onCallStateChanged(2)
-            assertThat(callState).isEqualTo(2)
-
-            job.cancel()
-
-            verify(manager).removeCallStateListener(listener)
-        }
-
-    companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
index 1968d75..017eefe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
@@ -38,16 +38,14 @@
 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
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.res.R
+import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
-import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
-import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.data.source.UserRecord
@@ -65,9 +63,6 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestDispatcher
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -102,18 +97,16 @@
     @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
 
-    private lateinit var underTest: UserSwitcherInteractor
-
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private lateinit var spyContext: Context
-    private lateinit var testScope: TestScope
     private lateinit var userRepository: FakeUserRepository
     private lateinit var keyguardReply: KeyguardInteractorFactory.WithDependencies
     private lateinit var keyguardRepository: FakeKeyguardRepository
-    private lateinit var telephonyRepository: FakeTelephonyRepository
-    private lateinit var testDispatcher: TestDispatcher
-    private lateinit var featureFlags: FakeFeatureFlags
     private lateinit var refreshUsersScheduler: RefreshUsersScheduler
 
+    private lateinit var underTest: UserSwitcherInteractor
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -127,22 +120,17 @@
             SUPERVISED_USER_CREATION_APP_PACKAGE,
         )
 
-        featureFlags =
-            FakeFeatureFlags().apply {
-                set(Flags.FULL_SCREEN_USER_SWITCHER, false)
-                set(Flags.FACE_AUTH_REFACTOR, true)
-            }
+        utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+        utils.featureFlags.set(Flags.FACE_AUTH_REFACTOR, true)
+
         spyContext = spy(context)
-        keyguardReply = KeyguardInteractorFactory.create(featureFlags = featureFlags)
+        keyguardReply = KeyguardInteractorFactory.create(featureFlags = utils.featureFlags)
         keyguardRepository = keyguardReply.repository
         userRepository = FakeUserRepository()
-        telephonyRepository = FakeTelephonyRepository()
-        testDispatcher = StandardTestDispatcher()
-        testScope = TestScope(testDispatcher)
         refreshUsersScheduler =
             RefreshUsersScheduler(
                 applicationScope = testScope.backgroundScope,
-                mainDispatcher = testDispatcher,
+                mainDispatcher = utils.testDispatcher,
                 repository = userRepository,
             )
     }
@@ -372,7 +360,7 @@
     fun actions_deviceUnlocked_fullScreen() {
         createUserInteractor()
         testScope.runTest {
-            featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 2, includeGuest = false)
 
             userRepository.setUserInfos(userInfos)
@@ -456,7 +444,7 @@
     fun actions_deviceLockedAddFromLockscreenSet_fullList_fullScreen() {
         createUserInteractor()
         testScope.runTest {
-            featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -649,7 +637,7 @@
 
             val refreshUsersCallCount = userRepository.refreshUsersCallCount
 
-            telephonyRepository.setCallState(1)
+            utils.telephonyRepository.setCallState(1)
             runCurrent()
 
             assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
@@ -801,7 +789,7 @@
     fun userRecordsFullScreen() {
         createUserInteractor()
         testScope.runTest {
-            featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 3, includeGuest = false)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             userRepository.setUserInfos(userInfos)
@@ -910,7 +898,7 @@
     fun showUserSwitcher_fullScreenEnabled_launchesFullScreenDialog() {
         createUserInteractor()
         testScope.runTest {
-            featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
 
             val expandable = mock<Expandable>()
             underTest.showUserSwitcher(expandable)
@@ -1125,21 +1113,18 @@
                 manager = manager,
                 headlessSystemUserMode = headlessSystemUserMode,
                 applicationScope = testScope.backgroundScope,
-                telephonyInteractor =
-                    TelephonyInteractor(
-                        repository = telephonyRepository,
-                    ),
+                telephonyInteractor = utils.telephonyInteractor(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
                 keyguardUpdateMonitor = keyguardUpdateMonitor,
-                backgroundDispatcher = testDispatcher,
+                backgroundDispatcher = utils.testDispatcher,
                 activityManager = activityManager,
                 refreshUsersScheduler = refreshUsersScheduler,
                 guestUserInteractor =
                     GuestUserInteractor(
                         applicationContext = spyContext,
                         applicationScope = testScope.backgroundScope,
-                        mainDispatcher = testDispatcher,
-                        backgroundDispatcher = testDispatcher,
+                        mainDispatcher = utils.testDispatcher,
+                        backgroundDispatcher = utils.testDispatcher,
                         manager = manager,
                         repository = userRepository,
                         deviceProvisionedController = deviceProvisionedController,
@@ -1150,7 +1135,7 @@
                         resetOrExitSessionReceiver = resetOrExitSessionReceiver,
                     ),
                 uiEventLogger = uiEventLogger,
-                featureFlags = featureFlags,
+                featureFlags = utils.featureFlags,
                 userRestrictionChecker = mock(),
             )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
index 8353cf7..d0fa27e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
@@ -20,6 +20,7 @@
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
+import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -31,16 +32,23 @@
     private val _onAnyConfigurationChange = MutableSharedFlow<Unit>()
     override val onAnyConfigurationChange: Flow<Unit> = _onAnyConfigurationChange.asSharedFlow()
 
+    private val _onConfigurationChange =
+        MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+    override val onConfigurationChange: Flow<Unit> = _onConfigurationChange.asSharedFlow()
+
     private val _scaleForResolution = MutableStateFlow(1f)
     override val scaleForResolution: Flow<Float> = _scaleForResolution.asStateFlow()
 
     private val pixelSizes = mutableMapOf<Int, MutableStateFlow<Int>>()
-    private val colors = mutableMapOf<Int, MutableStateFlow<Int>>()
 
     fun onAnyConfigurationChange() {
         _onAnyConfigurationChange.tryEmit(Unit)
     }
 
+    fun onConfigurationChange() {
+        _onConfigurationChange.tryEmit(Unit)
+    }
+
     fun setScaleForResolution(scale: Float) {
         _scaleForResolution.value = scale
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt
index 957fbbd..ace6500 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt
@@ -18,11 +18,11 @@
 package com.android.systemui.power.data.repository
 
 import android.os.PowerManager
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.power.shared.model.ScreenPowerState
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessModel
 import com.android.systemui.power.shared.model.WakefulnessState
-import com.android.systemui.dagger.SysUISingleton
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
@@ -55,15 +55,15 @@
         lastWakeReason = wakeReason
     }
 
-    override fun userTouch() {
+    override fun userTouch(noChangeLights: Boolean) {
         userTouchRegistered = true
     }
 
     override fun updateWakefulness(
-            rawState: WakefulnessState,
-            lastWakeReason: WakeSleepReason,
-            lastSleepReason: WakeSleepReason,
-            powerButtonLaunchGestureTriggered: Boolean
+        rawState: WakefulnessState,
+        lastWakeReason: WakeSleepReason,
+        lastSleepReason: WakeSleepReason,
+        powerButtonLaunchGestureTriggered: Boolean
     ) {
         _wakefulness.value =
             WakefulnessModel(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 14c4b86..36ec18f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -16,10 +16,15 @@
 
 package com.android.systemui.scene
 
+import android.app.ActivityTaskManager
 import android.content.Context
+import android.content.Intent
 import android.content.pm.UserInfo
 import android.graphics.Bitmap
 import android.graphics.drawable.BitmapDrawable
+import android.telecom.TelecomManager
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.util.EmergencyAffordanceManager
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.AuthenticationRepository
@@ -27,8 +32,11 @@
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
 import com.android.systemui.bouncer.data.repository.BouncerRepository
+import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.EmergencyDialerIntentFactory
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorFake
@@ -41,6 +49,7 @@
 import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.doze.DozeLogger
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
@@ -62,10 +71,13 @@
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
+import com.android.systemui.telephony.data.repository.TelephonyRepository
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
 import com.android.systemui.user.ui.viewmodel.UserViewModel
 import com.android.systemui.util.mockito.mock
@@ -102,12 +114,23 @@
             currentTime = { testScope.currentTime },
         )
     }
+    val configurationRepository: FakeConfigurationRepository by lazy {
+        FakeConfigurationRepository()
+    }
+    private val emergencyServicesRepository: EmergencyServicesRepository by lazy {
+        EmergencyServicesRepository(
+            applicationScope = applicationScope(),
+            resources = context.resources,
+            configurationRepository = configurationRepository,
+        )
+    }
+    val telephonyRepository: FakeTelephonyRepository by lazy { FakeTelephonyRepository() }
 
     val communalRepository: FakeCommunalRepository by lazy { FakeCommunalRepository() }
     val keyguardRepository: FakeKeyguardRepository by lazy { FakeKeyguardRepository() }
     val powerRepository: FakePowerRepository by lazy { FakePowerRepository() }
 
-    private val userRepository: UserRepository by lazy {
+    val userRepository: UserRepository by lazy {
         FakeUserRepository().apply {
             val users = listOf(UserInfo(/* id=  */ 0, "name", /* flags= */ 0))
             setUserInfos(users)
@@ -183,7 +206,7 @@
             featureFlags = featureFlags,
             sceneContainerFlags = sceneContainerFlags,
             bouncerRepository = FakeKeyguardBouncerRepository(),
-            configurationRepository = FakeConfigurationRepository(),
+            configurationRepository = configurationRepository,
             shadeRepository = FakeShadeRepository(),
             sceneInteractorProvider = { sceneInteractor() },
             powerInteractor = PowerInteractorFactory.create().powerInteractor,
@@ -217,6 +240,7 @@
     fun bouncerViewModel(
         bouncerInteractor: BouncerInteractor,
         authenticationInteractor: AuthenticationInteractor,
+        actionButtonInteractor: BouncerActionButtonInteractor,
         users: List<UserViewModel> = createUsers(),
     ): BouncerViewModel {
         return BouncerViewModel(
@@ -229,13 +253,16 @@
             selectedUser = flowOf(users.first { it.isSelectionMarkerVisible }),
             users = flowOf(users),
             userSwitcherMenu = flowOf(createMenuActions()),
-            telephonyInteractor =
-                TelephonyInteractor(
-                    repository = FakeTelephonyRepository(),
-                ),
+            actionButtonInteractor = actionButtonInteractor,
         )
     }
 
+    fun telephonyInteractor(
+        repository: TelephonyRepository = telephonyRepository,
+    ): TelephonyInteractor {
+        return TelephonyInteractor(repository = repository)
+    }
+
     fun falsingInteractor(collector: FalsingCollector = falsingCollector()): FalsingInteractor {
         return falsingInteractor ?: FalsingInteractor(collector).also { falsingInteractor = it }
     }
@@ -285,6 +312,40 @@
         }
     }
 
+    fun selectedUserInteractor(): SelectedUserInteractor {
+        return SelectedUserInteractor(userRepository, featureFlags)
+    }
+
+    fun bouncerActionButtonInteractor(
+        mobileConnectionsRepository: MobileConnectionsRepository = mock(),
+        activityTaskManager: ActivityTaskManager = mock(),
+        telecomManager: TelecomManager? = null,
+        emergencyAffordanceManager: EmergencyAffordanceManager =
+            EmergencyAffordanceManager(context),
+        emergencyDialerIntentFactory: EmergencyDialerIntentFactory =
+            object : EmergencyDialerIntentFactory {
+                override fun invoke(): Intent = Intent()
+            },
+        metricsLogger: MetricsLogger = mock(),
+        dozeLogger: DozeLogger = mock(),
+    ): BouncerActionButtonInteractor {
+        return BouncerActionButtonInteractor(
+            applicationContext = context,
+            backgroundDispatcher = testDispatcher,
+            repository = emergencyServicesRepository,
+            mobileConnectionsRepository = mobileConnectionsRepository,
+            telephonyInteractor = telephonyInteractor(),
+            authenticationInteractor = authenticationInteractor(),
+            selectedUserInteractor = selectedUserInteractor(),
+            activityTaskManager = activityTaskManager,
+            telecomManager = telecomManager,
+            emergencyAffordanceManager = emergencyAffordanceManager,
+            emergencyDialerIntentFactory = emergencyDialerIntentFactory,
+            metricsLogger = metricsLogger,
+            dozeLogger = dozeLogger,
+        )
+    }
+
     companion object {
         fun DomainLayerAuthenticationMethodModel.toDataLayer(): DataLayerAuthenticationMethodModel {
             return when (this) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
index 992ac62..5cde386 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
@@ -23,6 +23,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
 @SysUISingleton
@@ -31,6 +32,9 @@
     private val _callState = MutableStateFlow(0)
     override val callState: Flow<Int> = _callState.asStateFlow()
 
+    private val _isInCall = MutableStateFlow(false)
+    override val isInCall: StateFlow<Boolean> = _isInCall.asStateFlow()
+
     override var hasTelephonyRadio: Boolean = true
         private set
 
@@ -38,8 +42,12 @@
         _callState.value = value
     }
 
-    fun setHasRadio(hasRadio: Boolean) {
-        this.hasTelephonyRadio = hasRadio
+    fun setIsInCall(isInCall: Boolean) {
+        _isInCall.value = isInCall
+    }
+
+    fun setHasTelephonyRadio(hasTelephonyRadio: Boolean) {
+        this.hasTelephonyRadio = hasTelephonyRadio
     }
 }