diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 80fd51643b98fd657ecb4d98851f62f257c79283..cf51e2193833fa23d4f08cb23b96235b077be76a 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -246,11 +246,9 @@ filegroup {
     srcs: [
         /* Status bar fakes */
         "tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt",
-        "tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt",
-        "tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt",
-        "tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt",
         "tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt",
         "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt",
+        "tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt",
 
         /* QS fakes */
         "tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt",
@@ -263,6 +261,7 @@ filegroup {
     srcs: [
         /* Keyguard converted tests */
         // data
+        "tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt",
         "tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt",
         "tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt",
         "tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt",
@@ -285,6 +284,7 @@ filegroup {
         "tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt",
         "tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt",
         "tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt",
+        "tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt",
         "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt",
         "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt",
         "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt",
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
index 814ea31ad510f6806b45c9d0aee7e375c0d2de3f..1a97912c77bb5efb036800a988e35f2a4df99ec4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
@@ -18,6 +18,11 @@
 
 package com.android.systemui.bouncer.ui.composable
 
+import android.app.AlertDialog
+import android.app.Dialog
+import android.view.Gravity
+import android.view.WindowManager
+import android.widget.TextView
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.VectorConverter
 import androidx.compose.animation.core.tween
@@ -26,11 +31,16 @@ import androidx.compose.animation.graphics.res.animatedVectorResource
 import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
 import androidx.compose.animation.graphics.vector.AnimatedImageVector
 import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
@@ -41,14 +51,21 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.colorResource
 import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.android.compose.PlatformOutlinedButton
 import com.android.compose.animation.Easings
 import com.android.keyguard.PinShapeAdapter
 import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
@@ -189,6 +206,10 @@ private fun RegularPinInputDisplay(
     shapeAnimations: ShapeAnimations,
     modifier: Modifier = Modifier,
 ) {
+    if (viewModel.isSimAreaVisible) {
+        SimArea(viewModel = viewModel)
+    }
+
     // Holds all currently [VisiblePinEntry] composables. This cannot be simply derived from
     // `viewModel.pinInput` at composition, since deleting a pin entry needs to play a remove
     // animation, thus the composable to be removed has to remain in the composition until fully
@@ -234,6 +255,94 @@ private fun RegularPinInputDisplay(
     pinInputRow.Content(modifier)
 }
 
+@Composable
+private fun SimArea(viewModel: PinBouncerViewModel) {
+    val isLockedEsim by viewModel.isLockedEsim.collectAsState()
+    val isSimUnlockingDialogVisible by viewModel.isSimUnlockingDialogVisible.collectAsState()
+    val errorDialogMessage by viewModel.errorDialogMessage.collectAsState()
+    var unlockDialog: Dialog? by remember { mutableStateOf(null) }
+    var errorDialog: Dialog? by remember { mutableStateOf(null) }
+    val context = LocalView.current.context
+
+    DisposableEffect(isSimUnlockingDialogVisible) {
+        if (isSimUnlockingDialogVisible) {
+            val builder =
+                AlertDialog.Builder(context).apply {
+                    setMessage(context.getString(R.string.kg_sim_unlock_progress_dialog_message))
+                    setCancelable(false)
+                }
+            unlockDialog =
+                builder.create().apply {
+                    window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG)
+                    show()
+                    findViewById<TextView>(android.R.id.message)?.gravity = Gravity.CENTER
+                }
+        } else {
+            unlockDialog?.hide()
+            unlockDialog = null
+        }
+
+        onDispose {
+            unlockDialog?.hide()
+            unlockDialog = null
+        }
+    }
+
+    DisposableEffect(errorDialogMessage) {
+        if (errorDialogMessage != null) {
+            val builder = AlertDialog.Builder(context)
+            builder.setMessage(errorDialogMessage)
+            builder.setCancelable(false)
+            builder.setNeutralButton(R.string.ok, null)
+            errorDialog =
+                builder.create().apply {
+                    window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG)
+                    setOnDismissListener { viewModel.onErrorDialogDismissed() }
+                    show()
+                }
+        } else {
+            errorDialog?.hide()
+            errorDialog = null
+        }
+
+        onDispose {
+            errorDialog?.hide()
+            errorDialog = null
+        }
+    }
+
+    Box(modifier = Modifier.padding(bottom = 20.dp)) {
+        // If isLockedEsim is null, then we do not show anything.
+        if (isLockedEsim == true) {
+            PlatformOutlinedButton(
+                onClick = { viewModel.onDisableEsimButtonClicked() },
+            ) {
+                Row(
+                    horizontalArrangement = Arrangement.spacedBy(10.dp),
+                    verticalAlignment = Alignment.CenterVertically
+                ) {
+                    Image(
+                        painter = painterResource(id = R.drawable.ic_no_sim),
+                        contentDescription = null,
+                        colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface)
+                    )
+                    Text(
+                        text = stringResource(R.string.disable_carrier_button_text),
+                        style = MaterialTheme.typography.bodyMedium,
+                        color = MaterialTheme.colorScheme.onSurface,
+                    )
+                }
+            }
+        } else if (isLockedEsim == false) {
+            Image(
+                painter = painterResource(id = R.drawable.ic_lockscreen_sim),
+                contentDescription = null,
+                colorFilter = ColorFilter.tint(colorResource(id = R.color.background_protected))
+            )
+        }
+    }
+}
+
 private class PinInputRow(
     val shapeAnimations: ShapeAnimations,
 ) {
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 7769dd9dc9ab791365928e7f972e8b784e94b3a0..d5c7f93e14135f89b3aa57a25d46486e228d5deb 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -32,6 +32,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.time.SystemClock
@@ -168,6 +169,7 @@ constructor(
     private val userRepository: UserRepository,
     private val lockPatternUtils: LockPatternUtils,
     broadcastDispatcher: BroadcastDispatcher,
+    mobileConnectionsRepository: MobileConnectionsRepository,
 ) : AuthenticationRepository {
 
     override val isAutoConfirmFeatureEnabled: StateFlow<Boolean> =
@@ -192,9 +194,11 @@ constructor(
         get() = getSelectedUserInfo().id
 
     override val authenticationMethod: Flow<AuthenticationMethodModel> =
-        userRepository.selectedUserInfo
-            .map { it.id }
-            .distinctUntilChanged()
+        combine(userRepository.selectedUserInfo, mobileConnectionsRepository.isAnySimSecure) {
+                selectedUserInfo,
+                _ ->
+                selectedUserInfo.id
+            }
             .flatMapLatest { selectedUserId ->
                 broadcastDispatcher
                     .broadcastFlow(
@@ -212,6 +216,7 @@ constructor(
                     blockingAuthenticationMethodInternal(selectedUserId)
                 }
             }
+            .distinctUntilChanged()
 
     override val minPatternLength: Int = LockPatternUtils.MIN_LOCK_PATTERN_SIZE
 
@@ -354,9 +359,9 @@ constructor(
         userId: Int,
     ): AuthenticationMethodModel {
         return when (getSecurityMode.apply(userId)) {
-            KeyguardSecurityModel.SecurityMode.PIN,
+            KeyguardSecurityModel.SecurityMode.PIN -> AuthenticationMethodModel.Pin
             KeyguardSecurityModel.SecurityMode.SimPin,
-            KeyguardSecurityModel.SecurityMode.SimPuk -> AuthenticationMethodModel.Pin
+            KeyguardSecurityModel.SecurityMode.SimPuk -> AuthenticationMethodModel.Sim
             KeyguardSecurityModel.SecurityMode.Password -> AuthenticationMethodModel.Password
             KeyguardSecurityModel.SecurityMode.Pattern -> AuthenticationMethodModel.Pattern
             KeyguardSecurityModel.SecurityMode.None -> AuthenticationMethodModel.None
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
index bb5b81d4d2f738a140170ba8916edf211007841a..3552a1957f1a19e8056cdd3cf10226e82e80b6ec 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
@@ -37,4 +37,6 @@ sealed class AuthenticationMethodModel(
     object Password : AuthenticationMethodModel(isSecure = true)
 
     object Pattern : AuthenticationMethodModel(isSecure = true)
+
+    object Sim : AuthenticationMethodModel(isSecure = true)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimBouncerModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimBouncerModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5fc510154681c93a712d54d4b87a4a9f3f2be12d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimBouncerModel.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.model
+
+/** Represents the locked sim card in the Bouncer. */
+data class SimBouncerModel(val isSimPukLocked: Boolean, val subscriptionId: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimPukInputModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimPukInputModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3cd88d6044d80ed7f776eff352c6f3d82898d475
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimPukInputModel.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.model
+
+/**
+ * Represents the user flow for unlocking a PUK locked sim card.
+ *
+ * After entering the puk code, we need to enter and confirm a new pin code for the sim card.
+ */
+data class SimPukInputModel(
+    val enteredSimPuk: String? = null,
+    val enteredSimPin: String? = null,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ff6321cad6701c2cce5679aa5f576f0635724b86
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryModule.kt
@@ -0,0 +1,27 @@
+/*
+ * 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 dagger.Binds
+import dagger.Module
+
+@Module
+interface BouncerRepositoryModule {
+    @Binds
+    fun provideSimRepository(simRepositoryImpl: SimBouncerRepositoryImpl): SimBouncerRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..269878b43dabdf877ec1740547b0dfe59144b389
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt
@@ -0,0 +1,218 @@
+/*
+ * 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.annotation.SuppressLint
+import android.content.IntentFilter
+import android.content.res.Resources
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyManager
+import android.telephony.euicc.EuiccManager
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.bouncer.data.model.SimBouncerModel
+import com.android.systemui.bouncer.data.model.SimPukInputModel
+import com.android.systemui.broadcast.BroadcastDispatcher
+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.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy
+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.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/** Handles data layer logic for locked sim cards. */
+interface SimBouncerRepository {
+    /** The subscription id of the current locked sim card. */
+    val subscriptionId: StateFlow<Int>
+    /** The active subscription of the current subscription id. */
+    val activeSubscriptionInfo: StateFlow<SubscriptionInfo?>
+    /**
+     * Determines if current sim card is an esim and is locked.
+     *
+     * A null value indicates that we do not know if we are esim locked or not.
+     */
+    val isLockedEsim: StateFlow<Boolean?>
+    /**
+     * Determines whether the current sim is locked requiring a PUK (Personal Unlocking Key) code.
+     */
+    val isSimPukLocked: StateFlow<Boolean>
+    /**
+     * The error message that should be displayed in an alert dialog.
+     *
+     * A null value indicates that the error dialog is not showing.
+     */
+    val errorDialogMessage: StateFlow<String?>
+    /** The state of the user flow on the SimPuk screen. */
+    val simPukInputModel: SimPukInputModel
+    /** Sets the state of the user flow on the SimPuk screen. */
+    fun setSimPukUserInput(enteredSimPuk: String? = null, enteredSimPin: String? = null)
+    /**
+     * Sets the error message when failing sim verification.
+     *
+     * A null value indicates that there is no error message to show.
+     */
+    fun setSimVerificationErrorMessage(msg: String?)
+}
+
+@SysUISingleton
+class SimBouncerRepositoryImpl
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @Main resources: Resources,
+    keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val subscriptionManager: SubscriptionManagerProxy,
+    broadcastDispatcher: BroadcastDispatcher,
+    euiccManager: EuiccManager,
+) : SimBouncerRepository {
+    private val isPukScreenAvailable: Boolean =
+        resources.getBoolean(com.android.internal.R.bool.config_enable_puk_unlock_screen)
+
+    private val simBouncerModel: Flow<SimBouncerModel?> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : KeyguardUpdateMonitorCallback() {
+                        override fun onSimStateChanged(subId: Int, slotId: Int, simState: Int) {
+                            trySend(Unit)
+                        }
+                    }
+                keyguardUpdateMonitor.registerCallback(callback)
+                awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+            }
+            .map {
+                // Check to see if there is a locked sim puk card.
+                val pukLockedSubId =
+                    withContext(backgroundDispatcher) {
+                        keyguardUpdateMonitor.getNextSubIdForState(
+                            TelephonyManager.SIM_STATE_PUK_REQUIRED
+                        )
+                    }
+                if (
+                    isPukScreenAvailable &&
+                        subscriptionManager.isValidSubscriptionId(pukLockedSubId)
+                ) {
+                    return@map (SimBouncerModel(isSimPukLocked = true, pukLockedSubId))
+                }
+
+                // If there is no locked sim puk card, check to see if there is a locked sim card.
+                val pinLockedSubId =
+                    withContext(backgroundDispatcher) {
+                        keyguardUpdateMonitor.getNextSubIdForState(
+                            TelephonyManager.SIM_STATE_PIN_REQUIRED
+                        )
+                    }
+                if (subscriptionManager.isValidSubscriptionId(pinLockedSubId)) {
+                    return@map SimBouncerModel(isSimPukLocked = false, pinLockedSubId)
+                }
+
+                return@map null // There is no locked sim.
+            }
+
+    override val subscriptionId: StateFlow<Int> =
+        simBouncerModel
+            .map { state -> state?.subscriptionId ?: INVALID_SUBSCRIPTION_ID }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = INVALID_SUBSCRIPTION_ID,
+            )
+
+    @SuppressLint("MissingPermission")
+    override val activeSubscriptionInfo: StateFlow<SubscriptionInfo?> =
+        subscriptionId
+            .map {
+                withContext(backgroundDispatcher) {
+                    subscriptionManager.getActiveSubscriptionInfo(it)
+                }
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = null,
+            )
+
+    @SuppressLint("MissingPermission")
+    override val isLockedEsim: StateFlow<Boolean?> =
+        activeSubscriptionInfo
+            .map { info -> info?.let { euiccManager.isEnabled && info.isEmbedded } }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = null,
+            )
+
+    override val isSimPukLocked: StateFlow<Boolean> =
+        simBouncerModel
+            .map { it?.isSimPukLocked == true }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = false,
+            )
+
+    private val disableEsimErrorMessage: Flow<String?> =
+        broadcastDispatcher.broadcastFlow(filter = IntentFilter(ACTION_DISABLE_ESIM)) { _, receiver
+            ->
+            if (receiver.resultCode != EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) {
+                resources.getString(R.string.error_disable_esim_msg)
+            } else {
+                null
+            }
+        }
+
+    private val simVerificationErrorMessage: MutableStateFlow<String?> = MutableStateFlow(null)
+
+    override val errorDialogMessage: StateFlow<String?> =
+        merge(disableEsimErrorMessage, simVerificationErrorMessage)
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = null,
+            )
+
+    private var _simPukInputModel: SimPukInputModel = SimPukInputModel()
+    override val simPukInputModel: SimPukInputModel
+        get() = _simPukInputModel
+
+    override fun setSimPukUserInput(enteredSimPuk: String?, enteredSimPin: String?) {
+        _simPukInputModel = SimPukInputModel(enteredSimPuk, enteredSimPin)
+    }
+
+    override fun setSimVerificationErrorMessage(msg: String?) {
+        simVerificationErrorMessage.value = msg
+    }
+
+    companion object {
+        const val ACTION_DISABLE_ESIM = "com.android.keyguard.disable_esim"
+        const val INVALID_SUBSCRIPTION_ID = SubscriptionManager.INVALID_SUBSCRIPTION_ID
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 138a76ccc07e6196cfafe68b9ef8441b3a631e58..d5ac48371ae9c14b1c0b01bce1e87a7a167aaaef 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -54,6 +54,7 @@ constructor(
     flags: SceneContainerFlags,
     private val falsingInteractor: FalsingInteractor,
     private val powerInteractor: PowerInteractor,
+    private val simBouncerInteractor: SimBouncerInteractor,
 ) {
 
     /** The user-facing message to show in the bouncer. */
@@ -148,6 +149,10 @@ constructor(
         )
     }
 
+    fun setMessage(message: String?) {
+        repository.setMessage(message)
+    }
+
     /**
      * Resets the user-facing message back to the default according to the current authentication
      * method.
@@ -186,6 +191,12 @@ constructor(
         if (input.isEmpty()) {
             return AuthenticationResult.SKIPPED
         }
+
+        if (authenticationInteractor.getAuthenticationMethod() == AuthenticationMethodModel.Sim) {
+            // We authenticate sim in SimInteractor
+            return AuthenticationResult.SKIPPED
+        }
+
         // Switching to the application scope here since this method is often called from
         // view-models, whose lifecycle (and thus scope) is shorter than this interactor.
         // This allows the task to continue running properly even when the calling scope has been
@@ -223,6 +234,7 @@ constructor(
 
     private fun promptMessage(authMethod: AuthenticationMethodModel): String {
         return when (authMethod) {
+            is AuthenticationMethodModel.Sim -> simBouncerInteractor.getDefaultMessage()
             is AuthenticationMethodModel.Pin ->
                 applicationContext.getString(R.string.keyguard_enter_your_pin)
             is AuthenticationMethodModel.Password ->
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
index e398c930e86e52f815176e2c62396e04ef0dcac7..efa77926a423007ff495e84bc44f68ab0d4475c2 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt
@@ -19,6 +19,7 @@ package com.android.systemui.bouncer.domain.interactor
 import android.content.Context
 import android.content.Intent
 import android.telecom.TelecomManager
+import android.telephony.euicc.EuiccManager
 import com.android.internal.util.EmergencyAffordanceManager
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -47,4 +48,9 @@ object BouncerInteractorModule {
     ): EmergencyAffordanceManager {
         return EmergencyAffordanceManager(applicationContext)
     }
+
+    @Provides
+    fun provideEuiccManager(@Application applicationContext: Context): EuiccManager {
+        return applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..99d1f1370f4fd69d68ddd6cc632b8b0126e951a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
@@ -0,0 +1,340 @@
+/*
+ * 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.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.UserHandle
+import android.telephony.PinResult
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyManager
+import android.telephony.euicc.EuiccManager
+import android.text.TextUtils
+import android.util.Log
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.bouncer.data.repository.SimBouncerRepository
+import com.android.systemui.bouncer.data.repository.SimBouncerRepositoryImpl
+import com.android.systemui.bouncer.data.repository.SimBouncerRepositoryImpl.Companion.ACTION_DISABLE_ESIM
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.util.icuMessageFormat
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/** Handles domain layer logic for locked sim cards. */
+@SuppressLint("WrongConstant")
+@SysUISingleton
+class SimBouncerInteractor
+@Inject
+constructor(
+    @Application private val applicationContext: Context,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val repository: SimBouncerRepository,
+    private val telephonyManager: TelephonyManager,
+    @Main private val resources: Resources,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val euiccManager: EuiccManager,
+    // TODO(b/307977401): Replace this with `MobileConnectionsInteractor` when available.
+    mobileConnectionsRepository: MobileConnectionsRepository,
+) {
+    val subId: StateFlow<Int> = repository.subscriptionId
+    val isAnySimSecure: Flow<Boolean> = mobileConnectionsRepository.isAnySimSecure
+    val isLockedEsim: StateFlow<Boolean?> = repository.isLockedEsim
+    val errorDialogMessage: StateFlow<String?> = repository.errorDialogMessage
+
+    /** Returns the default message for the sim pin screen. */
+    fun getDefaultMessage(): String {
+        val isEsimLocked = repository.isLockedEsim.value ?: false
+        val isPuk: Boolean = repository.isSimPukLocked.value
+        val subscriptionId = repository.subscriptionId.value
+
+        if (subscriptionId == INVALID_SUBSCRIPTION_ID) {
+            Log.e(TAG, "Trying to get default message from unknown sub id")
+            return ""
+        }
+
+        var count = telephonyManager.activeModemCount
+        val info: SubscriptionInfo? = repository.activeSubscriptionInfo.value
+        val displayName = info?.displayName
+        var msg: String =
+            when {
+                count < 2 && isPuk -> resources.getString(R.string.kg_puk_enter_puk_hint)
+                count < 2 -> resources.getString(R.string.kg_sim_pin_instructions)
+                else -> {
+                    when {
+                        !TextUtils.isEmpty(displayName) && isPuk ->
+                            resources.getString(R.string.kg_puk_enter_puk_hint_multi, displayName)
+                        !TextUtils.isEmpty(displayName) ->
+                            resources.getString(R.string.kg_sim_pin_instructions_multi, displayName)
+                        isPuk -> resources.getString(R.string.kg_puk_enter_puk_hint)
+                        else -> resources.getString(R.string.kg_sim_pin_instructions)
+                    }
+                }
+            }
+
+        if (isEsimLocked) {
+            msg = resources.getString(R.string.kg_sim_lock_esim_instructions, msg)
+        }
+
+        return msg
+    }
+
+    /** Resets the user flow when the sim screen is puk locked. */
+    fun resetSimPukUserInput() {
+        repository.setSimPukUserInput()
+        // Force a garbage collection in an attempt to erase any sim pin or sim puk codes left in
+        // memory. Do it asynchronously with a 5-sec delay to avoid making the keyguard
+        // dismiss animation janky.
+
+        applicationScope.launch(backgroundDispatcher) {
+            delay(5000)
+            System.gc()
+            System.runFinalization()
+            System.gc()
+        }
+    }
+
+    /** Disables the locked esim card so user can bypass the sim pin screen. */
+    fun disableEsim() {
+        val activeSubscription = repository.activeSubscriptionInfo.value
+        if (activeSubscription == null) {
+            val subId = repository.subscriptionId.value
+            Log.e(TAG, "No active subscription with subscriptionId: $subId")
+            return
+        }
+        val intent = Intent(ACTION_DISABLE_ESIM)
+        intent.setPackage(applicationContext.packageName)
+        val callbackIntent =
+            PendingIntent.getBroadcastAsUser(
+                applicationContext,
+                0 /* requestCode */,
+                intent,
+                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE_UNAUDITED,
+                UserHandle.SYSTEM
+            )
+        applicationScope.launch(backgroundDispatcher) {
+            euiccManager.switchToSubscription(
+                INVALID_SUBSCRIPTION_ID,
+                activeSubscription.portIndex,
+                callbackIntent,
+            )
+        }
+    }
+
+    /** Update state when error dialog is dismissed by the user. */
+    fun onErrorDialogDismissed() {
+        repository.setSimVerificationErrorMessage(null)
+    }
+
+    /**
+     * Based on sim state, unlock the locked sim with the given credentials.
+     *
+     * @return Any message that should show associated with the provided input. Null means that no
+     *   message needs to be shown.
+     */
+    suspend fun verifySim(input: List<Any>): String? {
+        if (repository.isSimPukLocked.value) {
+            return verifySimPuk(input.joinToString(separator = ""))
+        }
+
+        return verifySimPin(input.joinToString(separator = ""))
+    }
+
+    /**
+     * Verifies the input and unlocks the locked sim with a 4-8 digit pin code.
+     *
+     * @return Any message that should show associated with the provided input. Null means that no
+     *   message needs to be shown.
+     */
+    private suspend fun verifySimPin(input: String): String? {
+        val subscriptionId = repository.subscriptionId.value
+        // A SIM PIN is 4 to 8 decimal digits according to
+        // GSM 02.17 version 5.0.1, Section 5.6 PIN Management
+        if (input.length < MIN_SIM_PIN_LENGTH || input.length > MAX_SIM_PIN_LENGTH) {
+            return resources.getString(R.string.kg_invalid_sim_pin_hint)
+        }
+        val result =
+            withContext(backgroundDispatcher) {
+                val telephonyManager: TelephonyManager =
+                    telephonyManager.createForSubscriptionId(subscriptionId)
+                telephonyManager.supplyIccLockPin(input)
+            }
+        when (result.result) {
+            PinResult.PIN_RESULT_TYPE_SUCCESS ->
+                keyguardUpdateMonitor.reportSimUnlocked(subscriptionId)
+            PinResult.PIN_RESULT_TYPE_INCORRECT -> {
+                if (result.attemptsRemaining <= CRITICAL_NUM_OF_ATTEMPTS) {
+                    // Show a dialog to display the remaining number of attempts to verify the sim
+                    // pin to the user.
+                    repository.setSimVerificationErrorMessage(
+                        getPinPasswordErrorMessage(result.attemptsRemaining)
+                    )
+                } else {
+                    return getPinPasswordErrorMessage(result.attemptsRemaining)
+                }
+            }
+        }
+
+        return null
+    }
+
+    /**
+     * Verifies the input and unlocks the locked sim with a puk code instead of pin.
+     *
+     * This occurs after incorrectly verifying the sim pin multiple times.
+     *
+     * @return Any message that should show associated with the provided input. Null means that no
+     *   message needs to be shown.
+     */
+    private suspend fun verifySimPuk(entry: String): String? {
+        val (enteredSimPuk, enteredSimPin) = repository.simPukInputModel
+        val subscriptionId: Int = repository.subscriptionId.value
+
+        // Stage 1: Enter the sim puk code of the sim card.
+        if (enteredSimPuk == null) {
+            if (entry.length >= MIN_SIM_PUK_LENGTH) {
+                repository.setSimPukUserInput(enteredSimPuk = entry)
+                return resources.getString(R.string.kg_puk_enter_pin_hint)
+            } else {
+                return resources.getString(R.string.kg_invalid_sim_puk_hint)
+            }
+        }
+
+        // Stage 2: Set a new sim pin to lock the sim card.
+        if (enteredSimPin == null) {
+            if (entry.length in MIN_SIM_PIN_LENGTH..MAX_SIM_PIN_LENGTH) {
+                repository.setSimPukUserInput(
+                    enteredSimPuk = enteredSimPuk,
+                    enteredSimPin = entry,
+                )
+                return resources.getString(R.string.kg_enter_confirm_pin_hint)
+            } else {
+                return resources.getString(R.string.kg_invalid_sim_pin_hint)
+            }
+        }
+
+        // Stage 3: Confirm the newly set sim pin.
+        if (repository.simPukInputModel.enteredSimPin != entry) {
+            // The entered sim pins do not match. Enter desired sim pin again to confirm.
+            repository.setSimVerificationErrorMessage(
+                resources.getString(R.string.kg_invalid_confirm_pin_hint)
+            )
+            repository.setSimPukUserInput(enteredSimPuk = enteredSimPuk)
+            return resources.getString(R.string.kg_puk_enter_pin_hint)
+        }
+
+        val result =
+            withContext(backgroundDispatcher) {
+                val telephonyManager = telephonyManager.createForSubscriptionId(subscriptionId)
+                telephonyManager.supplyIccLockPuk(enteredSimPuk, enteredSimPin)
+            }
+        resetSimPukUserInput()
+
+        when (result.result) {
+            PinResult.PIN_RESULT_TYPE_SUCCESS ->
+                keyguardUpdateMonitor.reportSimUnlocked(subscriptionId)
+            PinResult.PIN_RESULT_TYPE_INCORRECT ->
+                if (result.attemptsRemaining <= CRITICAL_NUM_OF_ATTEMPTS) {
+                    // Show a dialog to display the remaining number of attempts to verify the sim
+                    // puk to the user.
+                    repository.setSimVerificationErrorMessage(
+                        getPukPasswordErrorMessage(
+                            result.attemptsRemaining,
+                            isDefault = false,
+                            isEsimLocked = repository.isLockedEsim.value == true
+                        )
+                    )
+                } else {
+                    return getPukPasswordErrorMessage(
+                        result.attemptsRemaining,
+                        isDefault = false,
+                        isEsimLocked = repository.isLockedEsim.value == true
+                    )
+                }
+            else -> return resources.getString(R.string.kg_password_puk_failed)
+        }
+
+        return null
+    }
+
+    private fun getPinPasswordErrorMessage(attemptsRemaining: Int): String {
+        var displayMessage: String =
+            if (attemptsRemaining == 0) {
+                resources.getString(R.string.kg_password_wrong_pin_code_pukked)
+            } else if (attemptsRemaining > 0) {
+                val msgId = R.string.kg_password_default_pin_message
+                icuMessageFormat(resources, msgId, attemptsRemaining)
+            } else {
+                val msgId = R.string.kg_sim_pin_instructions
+                resources.getString(msgId)
+            }
+        if (repository.isLockedEsim.value == true) {
+            displayMessage =
+                resources.getString(R.string.kg_sim_lock_esim_instructions, displayMessage)
+        }
+        return displayMessage
+    }
+
+    private fun getPukPasswordErrorMessage(
+        attemptsRemaining: Int,
+        isDefault: Boolean,
+        isEsimLocked: Boolean,
+    ): String {
+        var displayMessage: String =
+            if (attemptsRemaining == 0) {
+                resources.getString(R.string.kg_password_wrong_puk_code_dead)
+            } else if (attemptsRemaining > 0) {
+                val msgId =
+                    if (isDefault) R.string.kg_password_default_puk_message
+                    else R.string.kg_password_wrong_puk_code
+                icuMessageFormat(resources, msgId, attemptsRemaining)
+            } else {
+                val msgId =
+                    if (isDefault) R.string.kg_puk_enter_puk_hint
+                    else R.string.kg_password_puk_failed
+                resources.getString(msgId)
+            }
+        if (isEsimLocked) {
+            displayMessage =
+                resources.getString(R.string.kg_sim_lock_esim_instructions, displayMessage)
+        }
+        return displayMessage
+    }
+
+    companion object {
+        private const val TAG = "BouncerSimInteractor"
+        const val INVALID_SUBSCRIPTION_ID = SimBouncerRepositoryImpl.INVALID_SUBSCRIPTION_ID
+        const val MIN_SIM_PIN_LENGTH = 4
+        const val MAX_SIM_PIN_LENGTH = 8
+        const val MIN_SIM_PUK_LENGTH = 8
+        const val CRITICAL_NUM_OF_ATTEMPTS = 2
+    }
+}
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 09c94c81581bda8d85dc99924ab6d531140cc9e6..44ddd97401865bdd0ccb7b146449cf8e77004666 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
@@ -23,6 +23,7 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationInter
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
@@ -64,6 +65,7 @@ class BouncerViewModel(
     users: Flow<List<UserViewModel>>,
     userSwitcherMenu: Flow<List<UserActionViewModel>>,
     actionButtonInteractor: BouncerActionButtonInteractor,
+    private val simBouncerInteractor: SimBouncerInteractor,
 ) {
     val selectedUserImage: StateFlow<Bitmap?> =
         selectedUser
@@ -259,6 +261,17 @@ class BouncerViewModel(
                     viewModelScope = newViewModelScope,
                     interactor = bouncerInteractor,
                     isInputEnabled = isInputEnabled,
+                    simBouncerInteractor = simBouncerInteractor,
+                    authenticationMethod = authenticationMethod
+                )
+            is AuthenticationMethodModel.Sim ->
+                PinBouncerViewModel(
+                    applicationContext = applicationContext,
+                    viewModelScope = newViewModelScope,
+                    interactor = bouncerInteractor,
+                    isInputEnabled = isInputEnabled,
+                    simBouncerInteractor = simBouncerInteractor,
+                    authenticationMethod = authenticationMethod,
                 )
             is AuthenticationMethodModel.Password ->
                 PasswordBouncerViewModel(
@@ -316,6 +329,7 @@ object BouncerViewModelModule {
         flags: SceneContainerFlags,
         userSwitcherViewModel: UserSwitcherViewModel,
         actionButtonInteractor: BouncerActionButtonInteractor,
+        simBouncerInteractor: SimBouncerInteractor,
     ): BouncerViewModel {
         return BouncerViewModel(
             applicationContext = applicationContext,
@@ -328,6 +342,7 @@ object BouncerViewModelModule {
             users = userSwitcherViewModel.users,
             userSwitcherMenu = userSwitcherViewModel.menu,
             actionButtonInteractor = actionButtonInteractor,
+            simBouncerInteractor = simBouncerInteractor,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index b2b8049e3cffa57f3b00c2f6dbbd75290b554829..e25e82fe04c38842106acfe56d4b11e4ed44a22f 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -14,20 +14,26 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.bouncer.ui.viewmodel
 
 import android.content.Context
 import com.android.keyguard.PinShapeAdapter
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
 import com.android.systemui.res.R
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the PIN code bouncer UI. */
 class PinBouncerViewModel(
@@ -35,13 +41,23 @@ class PinBouncerViewModel(
     viewModelScope: CoroutineScope,
     interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
+    private val simBouncerInteractor: SimBouncerInteractor,
+    authenticationMethod: AuthenticationMethodModel,
 ) :
     AuthMethodBouncerViewModel(
         viewModelScope = viewModelScope,
         interactor = interactor,
         isInputEnabled = isInputEnabled,
     ) {
-
+    /**
+     * Whether the sim-related UI in the pin view is showing.
+     *
+     * This UI is used to unlock a locked sim.
+     */
+    val isSimAreaVisible = authenticationMethod == AuthenticationMethodModel.Sim
+    val isLockedEsim: StateFlow<Boolean?> = simBouncerInteractor.isLockedEsim
+    val errorDialogMessage: StateFlow<String?> = simBouncerInteractor.errorDialogMessage
+    val isSimUnlockingDialogVisible: MutableStateFlow<Boolean> = MutableStateFlow(false)
     val pinShapes = PinShapeAdapter(applicationContext)
     private val mutablePinInput = MutableStateFlow(PinInputViewModel.empty())
 
@@ -49,7 +65,13 @@ class PinBouncerViewModel(
     val pinInput: StateFlow<PinInputViewModel> = mutablePinInput
 
     /** The length of the PIN for which we should show a hint. */
-    val hintedPinLength: StateFlow<Int?> = interactor.hintedPinLength
+    val hintedPinLength: StateFlow<Int?> =
+        if (isSimAreaVisible) {
+                flowOf(null)
+            } else {
+                interactor.hintedPinLength
+            }
+            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
 
     /** Appearance of the backspace button. */
     val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> =
@@ -80,10 +102,19 @@ class PinBouncerViewModel(
                 initialValue = ActionButtonAppearance.Hidden,
             )
 
-    override val authenticationMethod = AuthenticationMethodModel.Pin
+    override val authenticationMethod: AuthenticationMethodModel = authenticationMethod
 
     override val throttlingMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message
 
+    init {
+        viewModelScope.launch { simBouncerInteractor.subId.collect { onResetSimFlow() } }
+    }
+
+    /** Notifies that the user dismissed the sim pin error dialog. */
+    fun onErrorDialogDismissed() {
+        viewModelScope.launch { simBouncerInteractor.onErrorDialogDismissed() }
+    }
+
     /**
      * Whether the digit buttons should be animated when touched. Note that this doesn't affect the
      * delete or enter buttons; those should always animate.
@@ -123,7 +154,28 @@ class PinBouncerViewModel(
 
     /** Notifies that the user clicked the "enter" button. */
     fun onAuthenticateButtonClicked() {
-        tryAuthenticate(useAutoConfirm = false)
+        if (authenticationMethod == AuthenticationMethodModel.Sim) {
+            viewModelScope.launch {
+                isSimUnlockingDialogVisible.value = true
+                val msg = simBouncerInteractor.verifySim(getInput())
+                interactor.setMessage(msg)
+                isSimUnlockingDialogVisible.value = false
+                clearInput()
+            }
+        } else {
+            tryAuthenticate(useAutoConfirm = false)
+        }
+    }
+
+    fun onDisableEsimButtonClicked() {
+        viewModelScope.launch { simBouncerInteractor.disableEsim() }
+    }
+
+    /** Resets the sim screen and shows a default message. */
+    private fun onResetSimFlow() {
+        simBouncerInteractor.resetSimPukUserInput()
+        interactor.resetMessage()
+        clearInput()
     }
 
     override fun clearInput() {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 1dcc5402e7472d2488637aafeed19821a509f826..f93efa1debb320b9ebd886aefa9511b987d7d69c 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -37,13 +37,14 @@ import com.android.systemui.biometrics.FingerprintReEnrollNotification;
 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.data.repository.BouncerRepositoryModule;
 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;
+import com.android.systemui.common.CommonModule;
 import com.android.systemui.communal.dagger.CommunalModule;
 import com.android.systemui.complication.dagger.ComplicationComponent;
-import com.android.systemui.common.CommonModule;
 import com.android.systemui.controls.dagger.ControlsModule;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.SystemUser;
@@ -171,6 +172,7 @@ import javax.inject.Named;
         BiometricsModule.class,
         BiometricsDomainLayerModule.class,
         BouncerInteractorModule.class,
+        BouncerRepositoryModule.class,
         BouncerViewModule.class,
         ClipboardOverlayModule.class,
         ClockRegistryModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 298811baba6c6671c5785ab34092a9112c9d5384..715fb17c7c2d05298a5281ab10a41c0201fd4868 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -74,7 +74,8 @@ constructor(
                 repository.isUnlocked,
                 authenticationInteractor.authenticationMethod,
             ) { isUnlocked, authenticationMethod ->
-                !authenticationMethod.isSecure || isUnlocked
+                (!authenticationMethod.isSecure || isUnlocked) &&
+                    authenticationMethod != AuthenticationMethodModel.Sim
             }
             .stateIn(
                 scope = applicationScope,
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index ca2828b99d95da68b75786a45a7471c16af2981f..8def457423e4fd421f93a70542a1b2f8630c06c3 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -19,7 +19,10 @@
 package com.android.systemui.scene.domain.startable
 
 import com.android.systemui.CoreStartable
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorActual
 import com.android.systemui.dagger.SysUISingleton
@@ -72,6 +75,8 @@ constructor(
     private val sceneLogger: SceneLogger,
     @FalsingCollectorActual private val falsingCollector: FalsingCollector,
     private val powerInteractor: PowerInteractor,
+    private val simBouncerInteractor: SimBouncerInteractor,
+    private val authenticationInteractor: AuthenticationInteractor,
 ) : CoreStartable {
 
     override fun start() {
@@ -131,6 +136,33 @@ constructor(
                 }
             }
         }
+        applicationScope.launch {
+            simBouncerInteractor.isAnySimSecure.collect { isAnySimLocked ->
+                val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value
+                val isUnlocked = deviceEntryInteractor.isUnlocked.value
+
+                when {
+                    isAnySimLocked -> {
+                        switchToScene(
+                            targetSceneKey = SceneKey.Bouncer,
+                            loggingReason = "Need to authenticate locked sim card."
+                        )
+                    }
+                    isUnlocked && !canSwipeToEnter -> {
+                        switchToScene(
+                            targetSceneKey = SceneKey.Gone,
+                            loggingReason = "Sim cards are unlocked."
+                        )
+                    }
+                    else -> {
+                        switchToScene(
+                            targetSceneKey = SceneKey.Lockscreen,
+                            loggingReason = "Sim cards are unlocked."
+                        )
+                    }
+                }
+            }
+        }
         applicationScope.launch {
             deviceEntryInteractor.isUnlocked
                 .mapNotNull { isUnlocked ->
@@ -206,6 +238,14 @@ constructor(
                                 "device is waking up while unlocked without the ability" +
                                     " to swipe up on lockscreen to enter.",
                         )
+                    } else if (
+                        authenticationInteractor.getAuthenticationMethod() ==
+                            AuthenticationMethodModel.Sim
+                    ) {
+                        switchToScene(
+                            targetSceneKey = SceneKey.Bouncer,
+                            loggingReason = "device is starting to wake up with a locked sim"
+                        )
                     }
                 }
             }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt
index 22d048343bc9ef280ec695d32886d0739e68fa16..a2f5701d7eca88fd34c6fc92724fa3989d40c37e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt
@@ -16,15 +16,41 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.util
 
+import android.annotation.SuppressLint
+import android.content.Context
+import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
 
 interface SubscriptionManagerProxy {
     fun getDefaultDataSubscriptionId(): Int
+    fun isValidSubscriptionId(subId: Int): Boolean
+    suspend fun getActiveSubscriptionInfo(subId: Int): SubscriptionInfo?
 }
 
 /** Injectable proxy class for [SubscriptionManager]'s static methods */
-class SubscriptionManagerProxyImpl @Inject constructor() : SubscriptionManagerProxy {
+class SubscriptionManagerProxyImpl
+@Inject
+constructor(
+    @Application private val applicationContext: Context,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val subscriptionManager: SubscriptionManager,
+) : SubscriptionManagerProxy {
     /** The system default data subscription id, or INVALID_SUBSCRIPTION_ID on error */
     override fun getDefaultDataSubscriptionId() = SubscriptionManager.getDefaultDataSubscriptionId()
+
+    override fun isValidSubscriptionId(subId: Int): Boolean {
+        return SubscriptionManager.isValidSubscriptionId(subId)
+    }
+
+    @SuppressLint("MissingPermission")
+    override suspend fun getActiveSubscriptionInfo(subId: Int): SubscriptionInfo? {
+        return withContext(backgroundDispatcher) {
+            subscriptionManager.getActiveSubscriptionInfo(subId)
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
index 87ab5b0d157f0a9a61e386a20c7926b96f3ec897..64ddbc7828accf942b988d2d0b03b8beaa1b72d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -29,7 +29,10 @@ import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
+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.data.repository.FakeUserRepository
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -51,10 +54,12 @@ class AuthenticationRepositoryTest : SysuiTestCase() {
 
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
     @Mock private lateinit var getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>
+    @Mock private lateinit var tableLogger: TableLogBuffer
 
     private val testUtils = SceneTestUtils(this)
     private val testScope = testUtils.testScope
     private val userRepository = FakeUserRepository()
+    private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
 
     private lateinit var underTest: AuthenticationRepository
 
@@ -67,6 +72,8 @@ class AuthenticationRepositoryTest : SysuiTestCase() {
         userRepository.setUserInfos(USER_INFOS)
         runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[0]) }
         whenever(getSecurityMode.apply(anyInt())).thenAnswer { currentSecurityMode }
+        mobileConnectionsRepository =
+            FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger)
 
         underTest =
             AuthenticationRepositoryImpl(
@@ -76,6 +83,7 @@ class AuthenticationRepositoryTest : SysuiTestCase() {
                 userRepository = userRepository,
                 lockPatternUtils = lockPatternUtils,
                 broadcastDispatcher = fakeBroadcastDispatcher,
+                mobileConnectionsRepository = mobileConnectionsRepository,
             )
     }
 
@@ -97,6 +105,11 @@ class AuthenticationRepositoryTest : SysuiTestCase() {
             assertThat(authMethod).isEqualTo(AuthenticationMethodModel.None)
             assertThat(underTest.getAuthenticationMethod())
                 .isEqualTo(AuthenticationMethodModel.None)
+
+            currentSecurityMode = KeyguardSecurityModel.SecurityMode.SimPin
+            mobileConnectionsRepository.isAnySimSecure.value = true
+            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Sim)
+            assertThat(underTest.getAuthenticationMethod()).isEqualTo(AuthenticationMethodModel.Sim)
         }
 
     @Test
@@ -157,8 +170,7 @@ class AuthenticationRepositoryTest : SysuiTestCase() {
 
             userRepository.setSelectedUserInfo(USER_INFOS[1])
             assertThat(values.last()).isTrue()
-
-    }
+        }
 
     private fun setSecurityModeAndDispatchBroadcast(
         securityMode: KeyguardSecurityModel.SecurityMode,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b391b5a457992fb75acec3fb45ce396e644e174a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt
@@ -0,0 +1,201 @@
+/*
+ * 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.telephony.TelephonyManager
+import android.telephony.euicc.EuiccManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeSubscriptionManagerProxy
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SimBouncerRepositoryTest : SysuiTestCase() {
+    @Mock lateinit var euiccManager: EuiccManager
+    @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+    private val fakeSubscriptionManagerProxy = FakeSubscriptionManagerProxy()
+    private val keyguardUpdateMonitorCallbacks = mutableListOf<KeyguardUpdateMonitorCallback>()
+
+    private lateinit var underTest: SimBouncerRepositoryImpl
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(/* testClass = */ this)
+        whenever(keyguardUpdateMonitor.registerCallback(any())).thenAnswer {
+            val cb = it.arguments[0] as KeyguardUpdateMonitorCallback
+            keyguardUpdateMonitorCallbacks.add(cb)
+        }
+        whenever(keyguardUpdateMonitor.removeCallback(any())).thenAnswer {
+            keyguardUpdateMonitorCallbacks.remove(it.arguments[0])
+        }
+        underTest =
+            SimBouncerRepositoryImpl(
+                applicationScope = testScope.backgroundScope,
+                backgroundDispatcher = dispatcher,
+                resources = context.resources,
+                keyguardUpdateMonitor = keyguardUpdateMonitor,
+                subscriptionManager = fakeSubscriptionManagerProxy,
+                broadcastDispatcher = fakeBroadcastDispatcher,
+                euiccManager = euiccManager,
+            )
+    }
+
+    @Test
+    fun subscriptionId() =
+        testScope.runTest {
+            val subscriptionId =
+                emitSubscriptionIdAndCollectLastValue(underTest.subscriptionId, subId = 2)
+            assertThat(subscriptionId).isEqualTo(2)
+        }
+
+    @Test
+    fun activeSubscriptionInfo() =
+        testScope.runTest {
+            fakeSubscriptionManagerProxy.setActiveSubscriptionInfo(subId = 2)
+            val activeSubscriptionInfo =
+                emitSubscriptionIdAndCollectLastValue(underTest.activeSubscriptionInfo, subId = 2)
+
+            assertThat(activeSubscriptionInfo?.subscriptionId).isEqualTo(2)
+        }
+
+    @Test
+    fun isLockedEsim_initialValue_isNull() =
+        testScope.runTest {
+            val isLockedEsim by collectLastValue(underTest.isLockedEsim)
+            assertThat(isLockedEsim).isNull()
+        }
+
+    @Test
+    fun isLockedEsim() =
+        testScope.runTest {
+            whenever(euiccManager.isEnabled).thenReturn(true)
+            fakeSubscriptionManagerProxy.setActiveSubscriptionInfo(subId = 2, isEmbedded = true)
+            val isLockedEsim =
+                emitSubscriptionIdAndCollectLastValue(underTest.isLockedEsim, subId = 2)
+            assertThat(isLockedEsim).isTrue()
+        }
+
+    @Test
+    fun isLockedEsim_notEmbedded() =
+        testScope.runTest {
+            fakeSubscriptionManagerProxy.setActiveSubscriptionInfo(subId = 2, isEmbedded = false)
+            val isLockedEsim =
+                emitSubscriptionIdAndCollectLastValue(underTest.isLockedEsim, subId = 2)
+            assertThat(isLockedEsim).isFalse()
+        }
+
+    @Test
+    fun isSimPukLocked() =
+        testScope.runTest {
+            val isSimPukLocked =
+                emitSubscriptionIdAndCollectLastValue(
+                    underTest.isSimPukLocked,
+                    subId = 2,
+                    isSimPuk = true
+                )
+            assertThat(isSimPukLocked).isTrue()
+        }
+
+    @Test
+    fun setSimPukUserInput() {
+        val pukCode = "00000000"
+        val pinCode = "1234"
+        underTest.setSimPukUserInput(pukCode, pinCode)
+        assertThat(underTest.simPukInputModel.enteredSimPuk).isEqualTo(pukCode)
+        assertThat(underTest.simPukInputModel.enteredSimPin).isEqualTo(pinCode)
+    }
+
+    @Test
+    fun setSimPukUserInput_nullPuk() {
+        val pukCode = null
+        val pinCode = "1234"
+        underTest.setSimPukUserInput(pukCode, pinCode)
+        assertThat(underTest.simPukInputModel.enteredSimPuk).isNull()
+        assertThat(underTest.simPukInputModel.enteredSimPin).isEqualTo(pinCode)
+    }
+
+    @Test
+    fun setSimPukUserInput_nullPin() {
+        val pukCode = "00000000"
+        val pinCode = null
+        underTest.setSimPukUserInput(pukCode, pinCode)
+        assertThat(underTest.simPukInputModel.enteredSimPuk).isEqualTo(pukCode)
+        assertThat(underTest.simPukInputModel.enteredSimPin).isNull()
+    }
+
+    @Test
+    fun setSimPukUserInput_nullCodes() {
+        underTest.setSimPukUserInput()
+        assertThat(underTest.simPukInputModel.enteredSimPuk).isNull()
+        assertThat(underTest.simPukInputModel.enteredSimPin).isNull()
+    }
+
+    @Test
+    fun setSimPinVerificationErrorMessage() =
+        testScope.runTest {
+            val errorMsg = "error"
+            underTest.setSimVerificationErrorMessage(errorMsg)
+            val msg by collectLastValue(underTest.errorDialogMessage)
+            assertThat(msg).isEqualTo(errorMsg)
+        }
+
+    /** Emits a new sim card state and collects the last value of the flow argument. */
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private fun <T> TestScope.emitSubscriptionIdAndCollectLastValue(
+        flow: Flow<T>,
+        subId: Int = 1,
+        isSimPuk: Boolean = false
+    ): T? {
+        val value by collectLastValue(flow)
+        runCurrent()
+        val simState =
+            if (isSimPuk) {
+                TelephonyManager.SIM_STATE_PUK_REQUIRED
+            } else {
+                TelephonyManager.SIM_STATE_PIN_REQUIRED
+            }
+        whenever(keyguardUpdateMonitor.getNextSubIdForState(anyInt())).thenReturn(-1)
+        whenever(keyguardUpdateMonitor.getNextSubIdForState(simState)).thenReturn(subId)
+        keyguardUpdateMonitorCallbacks.forEach {
+            it.onSimStateChanged(subId, /* slotId= */ 0, simState)
+        }
+        runCurrent()
+        return value
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 296f9669144753a81a60ad9753b6b59e4cfd06f7..6e2e6377db42b5b83fad40f957cb3729d02fc769 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -87,6 +87,19 @@ class BouncerInteractorTest : SysuiTestCase() {
                 .isEqualTo(AuthenticationResult.SUCCEEDED)
         }
 
+    @Test
+    fun pinAuthMethod_sim_skipsAuthentication() =
+        testScope.runTest {
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim)
+            runCurrent()
+
+            // We rely on TelephonyManager to authenticate the sim card.
+            // Additionally, authenticating the sim card does not unlock the device.
+            // Thus, when auth method is sim, we expect to skip here.
+            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+                .isEqualTo(AuthenticationResult.SKIPPED)
+        }
+
     @Test
     fun pinAuthMethod_tryAutoConfirm_withAutoConfirmPin() =
         testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8c53c0e3f267266daf0db93698f9c4fc1e8844a5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.domain.interactor
+
+import android.content.res.Resources
+import android.telephony.PinResult
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyManager
+import android.telephony.euicc.EuiccManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeSimBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor.Companion.INVALID_SUBSCRIPTION_ID
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.res.R
+import com.android.systemui.scene.SceneTestUtils
+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
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class SimBouncerInteractorTest : SysuiTestCase() {
+    @Mock lateinit var telephonyManager: TelephonyManager
+    @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock lateinit var euiccManager: EuiccManager
+
+    private val utils = SceneTestUtils(this)
+    private val bouncerSimRepository = FakeSimBouncerRepository()
+    private val resources: Resources = context.resources
+    private val testScope = utils.testScope
+
+    private lateinit var underTest: SimBouncerInteractor
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            SimBouncerInteractor(
+                context,
+                testScope.backgroundScope,
+                utils.testDispatcher,
+                bouncerSimRepository,
+                telephonyManager,
+                resources,
+                keyguardUpdateMonitor,
+                euiccManager,
+                utils.mobileConnectionsRepository,
+            )
+    }
+
+    @Test
+    fun getDefaultMessage() {
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(
+            SubscriptionInfo.Builder().setDisplayName("sim").build()
+        )
+        whenever(telephonyManager.activeModemCount).thenReturn(1)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_sim_pin_instructions))
+    }
+
+    @Test
+    fun getDefaultMessage_isPuk() {
+        bouncerSimRepository.setSimPukLocked(true)
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(
+            SubscriptionInfo.Builder().setDisplayName("sim").build()
+        )
+        whenever(telephonyManager.activeModemCount).thenReturn(1)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_puk_enter_puk_hint))
+    }
+
+    @Test
+    fun getDefaultMessage_isEsimLocked() {
+        bouncerSimRepository.setLockedEsim(true)
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(
+            SubscriptionInfo.Builder().setDisplayName("sim").build()
+        )
+        whenever(telephonyManager.activeModemCount).thenReturn(1)
+
+        val msg = resources.getString(R.string.kg_sim_pin_instructions)
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_sim_lock_esim_instructions, msg))
+    }
+
+    @Test
+    fun getDefaultMessage_multipleSims() {
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(
+            SubscriptionInfo.Builder().setDisplayName("sim").build()
+        )
+        whenever(telephonyManager.activeModemCount).thenReturn(2)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_sim_pin_instructions_multi, "sim"))
+    }
+
+    @Test
+    fun getDefaultMessage_multipleSims_isPuk() {
+        bouncerSimRepository.setSimPukLocked(true)
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(
+            SubscriptionInfo.Builder().setDisplayName("sim").build()
+        )
+        whenever(telephonyManager.activeModemCount).thenReturn(2)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_puk_enter_puk_hint_multi, "sim"))
+    }
+
+    @Test
+    fun getDefaultMessage_multipleSims_emptyDisplayName() {
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(SubscriptionInfo.Builder().build())
+        whenever(telephonyManager.activeModemCount).thenReturn(2)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_sim_pin_instructions))
+    }
+
+    @Test
+    fun getDefaultMessage_multipleSims_emptyDisplayName_isPuk() {
+        bouncerSimRepository.setSimPukLocked(true)
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(SubscriptionInfo.Builder().build())
+        whenever(telephonyManager.activeModemCount).thenReturn(2)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_puk_enter_puk_hint))
+    }
+
+    @Test
+    fun resetSimPukUserInput() {
+        bouncerSimRepository.setSimPukUserInput("00000000", "1234")
+
+        assertThat(bouncerSimRepository.simPukInputModel.enteredSimPuk).isEqualTo("00000000")
+        assertThat(bouncerSimRepository.simPukInputModel.enteredSimPin).isEqualTo("1234")
+
+        underTest.resetSimPukUserInput()
+
+        assertThat(bouncerSimRepository.simPukInputModel.enteredSimPuk).isNull()
+        assertThat(bouncerSimRepository.simPukInputModel.enteredSimPin).isNull()
+    }
+
+    @Test
+    fun disableEsim() =
+        testScope.runTest {
+            val portIndex = 1
+            bouncerSimRepository.setActiveSubscriptionInfo(
+                SubscriptionInfo.Builder().setPortIndex(portIndex).build()
+            )
+
+            underTest.disableEsim()
+            runCurrent()
+
+            verify(euiccManager)
+                .switchToSubscription(
+                    eq(INVALID_SUBSCRIPTION_ID),
+                    eq(portIndex),
+                    ArgumentMatchers.any()
+                )
+        }
+
+    @Test
+    fun verifySimPin() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(false)
+            whenever(telephonyManager.createForSubscriptionId(anyInt()))
+                .thenReturn(telephonyManager)
+            whenever(telephonyManager.supplyIccLockPin(anyString()))
+                .thenReturn(PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 1))
+
+            val msg: String? = underTest.verifySim(listOf(0, 0, 0, 0))
+            runCurrent()
+            assertThat(msg).isNull()
+
+            verify(keyguardUpdateMonitor).reportSimUnlocked(1)
+        }
+
+    @Test
+    fun verifySimPin_incorrect_oneRemainingAttempt() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(false)
+            whenever(telephonyManager.createForSubscriptionId(anyInt()))
+                .thenReturn(telephonyManager)
+            whenever(telephonyManager.supplyIccLockPin(anyString()))
+                .thenReturn(
+                    PinResult(
+                        PinResult.PIN_RESULT_TYPE_INCORRECT,
+                        1,
+                    )
+                )
+
+            val msg: String? = underTest.verifySim(listOf(0, 0, 0, 0))
+            runCurrent()
+
+            assertThat(msg).isNull()
+            val errorDialogMessage by collectLastValue(bouncerSimRepository.errorDialogMessage)
+            assertThat(errorDialogMessage)
+                .isEqualTo(
+                    "Enter SIM PIN. You have 1 remaining attempt before you must contact" +
+                        " your carrier to unlock your device."
+                )
+        }
+
+    @Test
+    fun verifySimPin_incorrect_threeRemainingAttempts() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(false)
+            whenever(telephonyManager.createForSubscriptionId(anyInt()))
+                .thenReturn(telephonyManager)
+            whenever(telephonyManager.supplyIccLockPin(anyString()))
+                .thenReturn(
+                    PinResult(
+                        PinResult.PIN_RESULT_TYPE_INCORRECT,
+                        3,
+                    )
+                )
+
+            val msg = underTest.verifySim(listOf(0, 0, 0, 0))
+            runCurrent()
+
+            assertThat(msg).isEqualTo("Enter SIM PIN. You have 3 remaining attempts.")
+        }
+
+    @Test
+    fun verifySimPin_notCorrectLength_tooShort() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(false)
+
+            val msg = underTest.verifySim(listOf(0))
+
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint))
+        }
+
+    @Test
+    fun verifySimPin_notCorrectLength_tooLong() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(false)
+
+            val msg = underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0))
+
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint))
+        }
+
+    @Test
+    fun verifySimPuk() =
+        testScope.runTest {
+            whenever(telephonyManager.createForSubscriptionId(anyInt()))
+                .thenReturn(telephonyManager)
+            whenever(telephonyManager.supplyIccLockPuk(anyString(), anyString()))
+                .thenReturn(PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 1))
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(true)
+
+            var msg = underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0))
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_puk_enter_pin_hint))
+
+            msg = underTest.verifySim(listOf(0, 0, 0, 0))
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_enter_confirm_pin_hint))
+
+            msg = underTest.verifySim(listOf(0, 0, 0, 0))
+            assertThat(msg).isNull()
+
+            runCurrent()
+            verify(keyguardUpdateMonitor).reportSimUnlocked(1)
+        }
+
+    @Test
+    fun verifySimPuk_inputTooShort() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(true)
+            val msg = underTest.verifySim(listOf(0, 0, 0, 0))
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_puk_hint))
+        }
+
+    @Test
+    fun verifySimPuk_pinNotCorrectLength() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(true)
+
+            underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0))
+
+            val msg = underTest.verifySim(listOf(0, 0, 0))
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint))
+        }
+
+    @Test
+    fun verifySimPuk_confirmedPinDoesNotMatch() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(true)
+
+            underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0))
+            underTest.verifySim(listOf(0, 0, 0, 0))
+
+            val msg = underTest.verifySim(listOf(0, 0, 0, 1))
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_puk_enter_pin_hint))
+        }
+
+    @Test
+    fun onErrorDialogDismissed_clearsErrorDialogMessageInRepository() {
+        bouncerSimRepository.setSimVerificationErrorMessage("abc")
+        assertThat(bouncerSimRepository.errorDialogMessage.value).isNotNull()
+
+        underTest.onErrorDialogDismissed()
+
+        assertThat(bouncerSimRepository.errorDialogMessage.value).isNull()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index cfcb5457414464faa558f7bfb110542a0bb583bc..63c992bd78545e5080cda98feb1fd5c2ddf5bdb3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -48,6 +48,8 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() {
             viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true),
+            simBouncerInteractor = utils.simBouncerInteractor,
+            authenticationMethod = AuthenticationMethodModel.Pin,
         )
 
     @Test
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 f4346b56676ddccb312967d89e0b77520532e29b..75d6a007b4aac8ef6c6fd4cf633f077d9b785c51 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
@@ -233,6 +233,7 @@ class BouncerViewModelTest : SysuiTestCase() {
             AuthenticationMethodModel.Pin,
             AuthenticationMethodModel.Password,
             AuthenticationMethodModel.Pattern,
+            AuthenticationMethodModel.Sim,
         )
     }
 }
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 7a9cb6cc18c263e6f2e35d3807aa16c191b39120..52844cf7f79afb6e26f67f0429acde525d9b9370 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,8 @@ class PinBouncerViewModelTest : SysuiTestCase() {
             viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
+            simBouncerInteractor = utils.simBouncerInteractor,
+            authenticationMethod = AuthenticationMethodModel.Pin,
         )
 
     @Before
@@ -91,6 +93,52 @@ class PinBouncerViewModelTest : SysuiTestCase() {
             assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin)
         }
 
+    @Test
+    fun simBouncerViewModel_simAreaIsVisible() =
+        testScope.runTest {
+            val underTest =
+                PinBouncerViewModel(
+                    applicationContext = context,
+                    viewModelScope = testScope.backgroundScope,
+                    interactor = bouncerInteractor,
+                    isInputEnabled = MutableStateFlow(true).asStateFlow(),
+                    simBouncerInteractor = utils.simBouncerInteractor,
+                    authenticationMethod = AuthenticationMethodModel.Sim,
+                )
+
+            assertThat(underTest.isSimAreaVisible).isTrue()
+        }
+
+    @Test
+    fun onErrorDialogDismissed_clearsDialogMessage() =
+        testScope.runTest {
+            val dialogMessage by collectLastValue(underTest.errorDialogMessage)
+            utils.simBouncerRepository.setSimVerificationErrorMessage("abc")
+            assertThat(dialogMessage).isEqualTo("abc")
+
+            underTest.onErrorDialogDismissed()
+
+            assertThat(dialogMessage).isNull()
+        }
+
+    @Test
+    fun simBouncerViewModel_autoConfirmEnabled_hintedPinLengthIsNull() =
+        testScope.runTest {
+            val underTest =
+                PinBouncerViewModel(
+                    applicationContext = context,
+                    viewModelScope = testScope.backgroundScope,
+                    interactor = bouncerInteractor,
+                    isInputEnabled = MutableStateFlow(true).asStateFlow(),
+                    simBouncerInteractor = utils.simBouncerInteractor,
+                    authenticationMethod = AuthenticationMethodModel.Sim,
+                )
+            utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
+            val hintedPinLength by collectLastValue(underTest.hintedPinLength)
+
+            assertThat(hintedPinLength).isNull()
+        }
+
     @Test
     fun onPinButtonClicked() =
         testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index abd9f2846d2f4a8220b0a4e2491b4887bbf9d736..0004f52bc1c1aa74fb3a10eea95857a7a7689d7f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -89,6 +89,16 @@ class DeviceEntryInteractorTest : SysuiTestCase() {
             assertThat(isUnlocked).isTrue()
         }
 
+    @Test
+    fun isUnlocked_whenAuthMethodIsSimAndUnlocked_isFalse() =
+        testScope.runTest {
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim)
+            utils.deviceEntryRepository.setUnlocked(true)
+
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+        }
+
     @Test
     fun isDeviceEntered_onLockscreenWithSwipe_isFalse() =
         testScope.runTest {
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 cef888bcc362748a48e16903376da7d5e16f22ee..6a054cd9aff76e36a6a8f81710571d8ec06d661d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -256,6 +256,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
                 falsingCollector = utils.falsingCollector(),
                 powerInteractor = powerInteractor,
                 bouncerInteractor = bouncerInteractor,
+                simBouncerInteractor = utils.simBouncerInteractor,
+                authenticationInteractor = utils.authenticationInteractor()
             )
         startable.start()
 
@@ -483,6 +485,32 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
             verify(telecomManager).showInCallScreen(any())
         }
 
+    @Test
+    fun showBouncer_whenLockedSimIntroduced() =
+        testScope.runTest {
+            setAuthMethod(AuthenticationMethodModel.None)
+            introduceLockedSim()
+            assertCurrentScene(SceneKey.Bouncer)
+        }
+
+    @Test
+    fun goesToGone_whenSimUnlocked_whileDeviceUnlocked() =
+        testScope.runTest {
+            introduceLockedSim()
+            emulateUiSceneTransition(expectedVisible = true)
+            enterSimPin(authMethodAfterSimUnlock = AuthenticationMethodModel.None)
+            assertCurrentScene(SceneKey.Gone)
+        }
+
+    @Test
+    fun showLockscreen_whenSimUnlocked_whileDeviceLocked() =
+        testScope.runTest {
+            introduceLockedSim()
+            emulateUiSceneTransition(expectedVisible = true)
+            enterSimPin(authMethodAfterSimUnlock = AuthenticationMethodModel.Pin)
+            assertCurrentScene(SceneKey.Lockscreen)
+        }
+
     /**
      * Asserts that the current scene in the view-model matches what's expected.
      *
@@ -683,6 +711,35 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
         runCurrent()
     }
 
+    /**
+     * Enters the correct PIN in the sim bouncer UI.
+     *
+     * Asserts that the current scene is [SceneKey.Bouncer] and that the current bouncer UI is a PIN
+     * before proceeding.
+     *
+     * Does not assert that the device is locked or unlocked.
+     */
+    private fun TestScope.enterSimPin(
+        authMethodAfterSimUnlock: AuthenticationMethodModel = AuthenticationMethodModel.None
+    ) {
+        assertWithMessage("Cannot enter PIN when not on the Bouncer scene!")
+            .that(getCurrentSceneInUi())
+            .isEqualTo(SceneKey.Bouncer)
+        val authMethodViewModel by collectLastValue(bouncerViewModel.authMethodViewModel)
+        assertWithMessage("Cannot enter PIN when not using a PIN authentication method!")
+            .that(authMethodViewModel)
+            .isInstanceOf(PinBouncerViewModel::class.java)
+
+        val pinBouncerViewModel = authMethodViewModel as PinBouncerViewModel
+        FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
+            pinBouncerViewModel.onPinButtonClicked(digit)
+        }
+        pinBouncerViewModel.onAuthenticateButtonClicked()
+        setAuthMethod(authMethodAfterSimUnlock)
+        utils.mobileConnectionsRepository.isAnySimSecure.value = false
+        runCurrent()
+    }
+
     /** Changes device wakefulness state from asleep to awake, going through intermediary states. */
     private fun TestScope.wakeUpDevice() {
         val wakefulnessModel = powerInteractor.detailedWakefulness.value
@@ -723,4 +780,10 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
             runCurrent()
         }
     }
+
+    private fun TestScope.introduceLockedSim() {
+        setAuthMethod(AuthenticationMethodModel.Sim)
+        utils.mobileConnectionsRepository.isAnySimSecure.value = true
+        runCurrent()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 2f654e22aec6b3128f5fbf37789ab11b3a7bdcca..c4ec56c906c34c0a995308af2460ad3c7286f566 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -89,6 +89,8 @@ class SceneContainerStartableTest : SysuiTestCase() {
             falsingCollector = falsingCollector,
             powerInteractor = powerInteractor,
             bouncerInteractor = bouncerInteractor,
+            simBouncerInteractor = utils.simBouncerInteractor,
+            authenticationInteractor = authenticationInteractor,
         )
 
     @Before
@@ -587,6 +589,64 @@ class SceneContainerStartableTest : SysuiTestCase() {
             verify(falsingCollector, times(2)).onBouncerHidden()
         }
 
+    @Test
+    fun switchesToBouncer_whenSimBecomesLocked() =
+        testScope.runTest {
+            val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
+
+            prepareState(
+                initialSceneKey = SceneKey.Lockscreen,
+                authenticationMethod = AuthenticationMethodModel.Pin,
+                isDeviceUnlocked = false,
+            )
+            underTest.start()
+            runCurrent()
+
+            utils.mobileConnectionsRepository.isAnySimSecure.value = true
+            runCurrent()
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer)
+        }
+
+    @Test
+    fun switchesToLockscreen_whenSimBecomesUnlocked() =
+        testScope.runTest {
+            utils.mobileConnectionsRepository.isAnySimSecure.value = true
+            val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
+
+            prepareState(
+                initialSceneKey = SceneKey.Bouncer,
+                authenticationMethod = AuthenticationMethodModel.Pin,
+                isDeviceUnlocked = false,
+            )
+            underTest.start()
+            runCurrent()
+            utils.mobileConnectionsRepository.isAnySimSecure.value = false
+            runCurrent()
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen)
+        }
+
+    @Test
+    fun switchesToGone_whenSimBecomesUnlocked_ifDeviceUnlockedAndLockscreenDisabled() =
+        testScope.runTest {
+            utils.mobileConnectionsRepository.isAnySimSecure.value = true
+            val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
+
+            prepareState(
+                initialSceneKey = SceneKey.Lockscreen,
+                authenticationMethod = AuthenticationMethodModel.None,
+                isDeviceUnlocked = true,
+                isLockscreenEnabled = false,
+            )
+            underTest.start()
+            runCurrent()
+            utils.mobileConnectionsRepository.isAnySimSecure.value = false
+            runCurrent()
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Gone)
+        }
+
     private fun TestScope.prepareState(
         isDeviceUnlocked: Boolean = false,
         isBypassEnabled: Boolean = false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt
index 3dc7de6884464da764a7b07201eaaaaf8a7c0e59..a80238167b8551f60996ea6386c3a8edc4025981 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt
@@ -16,12 +16,28 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.util
 
+import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 
 /** Fake of [SubscriptionManagerProxy] for easy testing */
 class FakeSubscriptionManagerProxy(
     /** Set the default data subId to be returned in [getDefaultDataSubscriptionId] */
-    var defaultDataSubId: Int = INVALID_SUBSCRIPTION_ID
+    var defaultDataSubId: Int = INVALID_SUBSCRIPTION_ID,
+    var activeSubscriptionInfo: SubscriptionInfo? = null
 ) : SubscriptionManagerProxy {
     override fun getDefaultDataSubscriptionId(): Int = defaultDataSubId
+
+    override fun isValidSubscriptionId(subId: Int): Boolean {
+        return subId > -1
+    }
+
+    override suspend fun getActiveSubscriptionInfo(subId: Int): SubscriptionInfo? {
+        return activeSubscriptionInfo
+    }
+
+    /** Sets the active subscription info. */
+    fun setActiveSubscriptionInfo(subId: Int, isEmbedded: Boolean = false) {
+        activeSubscriptionInfo =
+            SubscriptionInfo.Builder().setId(subId).setEmbedded(isEmbedded).build()
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
index af1930ef143e4e56197f1e051eef748d0f126cfb..c0dbeca423ac01bec1e3d624520affc283c9b2f1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -178,6 +178,7 @@ class FakeAuthenticationRepository(
                 is AuthenticationMethodModel.Password -> SecurityMode.Password
                 is AuthenticationMethodModel.Pattern -> SecurityMode.Pattern
                 is AuthenticationMethodModel.None -> SecurityMode.None
+                is AuthenticationMethodModel.Sim -> SecurityMode.SimPin
             }
         }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeSimBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeSimBouncerRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..890e69dced0b273e5d335d695412b3bdeae2dbbf
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeSimBouncerRepository.kt
@@ -0,0 +1,68 @@
+/*
+ * 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 android.telephony.SubscriptionInfo
+import com.android.systemui.bouncer.data.model.SimPukInputModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/** Fakes the SimBouncerRepository. */
+class FakeSimBouncerRepository : SimBouncerRepository {
+    private val _subscriptionId: MutableStateFlow<Int> = MutableStateFlow(-1)
+    override val subscriptionId: StateFlow<Int> = _subscriptionId
+    private val _activeSubscriptionInfo: MutableStateFlow<SubscriptionInfo?> =
+        MutableStateFlow(null)
+    override val activeSubscriptionInfo: StateFlow<SubscriptionInfo?> = _activeSubscriptionInfo
+    private val _isLockedEsim: MutableStateFlow<Boolean?> = MutableStateFlow(null)
+    override val isLockedEsim: StateFlow<Boolean?> = _isLockedEsim
+    private val _isSimPukLocked: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val isSimPukLocked: StateFlow<Boolean> = _isSimPukLocked
+    private val _errorDialogMessage: MutableStateFlow<String?> = MutableStateFlow(null)
+    override val errorDialogMessage: StateFlow<String?> = _errorDialogMessage
+    private var _simPukInputModel = SimPukInputModel()
+    override val simPukInputModel: SimPukInputModel
+        get() = _simPukInputModel
+
+    fun setSubscriptionId(subId: Int) {
+        _subscriptionId.value = subId
+    }
+
+    fun setActiveSubscriptionInfo(subscriptioninfo: SubscriptionInfo) {
+        _activeSubscriptionInfo.value = subscriptioninfo
+    }
+
+    fun setLockedEsim(isLockedEsim: Boolean) {
+        _isLockedEsim.value = isLockedEsim
+    }
+
+    fun setSimPukLocked(isSimPukLocked: Boolean) {
+        _isSimPukLocked.value = isSimPukLocked
+    }
+
+    fun setErrorDialogMessage(msg: String?) {
+        _errorDialogMessage.value = msg
+    }
+
+    override fun setSimPukUserInput(enteredSimPuk: String?, enteredSimPin: String?) {
+        _simPukInputModel = SimPukInputModel(enteredSimPuk, enteredSimPin)
+    }
+
+    override fun setSimVerificationErrorMessage(msg: String?) {
+        _errorDialogMessage.value = msg
+    }
+}
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 c8869aaa018f276c8188989337af4216f3a7c83c..29e73b548b0bfdb2a2622f699bf35651875ed041 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
@@ -23,6 +23,10 @@ import android.content.pm.UserInfo
 import android.graphics.Bitmap
 import android.graphics.drawable.BitmapDrawable
 import android.telecom.TelecomManager
+import android.telephony.PinResult
+import android.telephony.PinResult.PIN_RESULT_TYPE_SUCCESS
+import android.telephony.TelephonyManager
+import android.telephony.euicc.EuiccManager
 import com.android.internal.logging.MetricsLogger
 import com.android.internal.util.EmergencyAffordanceManager
 import com.android.systemui.SysuiTestCase
@@ -32,9 +36,11 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationInter
 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.data.repository.FakeSimBouncerRepository
 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.domain.interactor.SimBouncerInteractor
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorFake
@@ -73,6 +79,7 @@ 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.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 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
@@ -89,6 +96,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.currentTime
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito
 
 /**
  * Utilities for creating scene container framework related repositories, interactors, and
@@ -127,9 +137,33 @@ class SceneTestUtils(
     }
     val telephonyRepository: FakeTelephonyRepository by lazy { FakeTelephonyRepository() }
 
+    val bouncerRepository = BouncerRepository(featureFlags)
     val communalRepository: FakeCommunalRepository by lazy { FakeCommunalRepository() }
     val keyguardRepository: FakeKeyguardRepository by lazy { FakeKeyguardRepository() }
     val powerRepository: FakePowerRepository by lazy { FakePowerRepository() }
+    val simBouncerRepository: FakeSimBouncerRepository by lazy { FakeSimBouncerRepository() }
+    val telephonyManager: TelephonyManager =
+        Mockito.mock(TelephonyManager::class.java).apply {
+            whenever(createForSubscriptionId(anyInt())).thenReturn(this)
+            whenever(supplyIccLockPin(anyString()))
+                .thenReturn(PinResult(PIN_RESULT_TYPE_SUCCESS, 3))
+        }
+    val mobileConnectionsRepository: FakeMobileConnectionsRepository by lazy {
+        FakeMobileConnectionsRepository(mock(), mock())
+    }
+
+    val simBouncerInteractor =
+        SimBouncerInteractor(
+            applicationContext = context,
+            backgroundDispatcher = testDispatcher,
+            applicationScope = applicationScope(),
+            repository = simBouncerRepository,
+            telephonyManager = telephonyManager,
+            resources = context.resources,
+            keyguardUpdateMonitor = mock(),
+            euiccManager = context.getSystemService(Context.EUICC_SERVICE) as EuiccManager,
+            mobileConnectionsRepository = mobileConnectionsRepository,
+        )
 
     val userRepository: UserRepository by lazy {
         FakeUserRepository().apply {
@@ -228,11 +262,12 @@ class SceneTestUtils(
         return BouncerInteractor(
             applicationScope = applicationScope(),
             applicationContext = context,
-            repository = BouncerRepository(featureFlags),
+            repository = bouncerRepository,
             authenticationInteractor = authenticationInteractor,
             flags = sceneContainerFlags,
             falsingInteractor = falsingInteractor(),
-            powerInteractor = powerInteractor()
+            powerInteractor = powerInteractor(),
+            simBouncerInteractor = simBouncerInteractor,
         )
     }
 
@@ -253,6 +288,7 @@ class SceneTestUtils(
             users = flowOf(users),
             userSwitcherMenu = flowOf(createMenuActions()),
             actionButtonInteractor = actionButtonInteractor,
+            simBouncerInteractor = simBouncerInteractor,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt