Merge changes Ia719fce3,If1a64a93,I90b025f5,I54449898,I32b7b585 into udc-dev

* changes:
  [flexiglass] Foundation for placeholder QS scene.
  [flexiglass] Foundation for placeholder shade scene.
  [flexiglass] Foundation for placeholder lock screen scene.
  [flexiglass] Foundation for placeholder bouncer scene.
  [flexiglass] Authentication logic.
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt
new file mode 100644
index 0000000..7c394a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.authentication
+
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryModule
+import dagger.Module
+
+@Module(
+    includes =
+        [
+            AuthenticationRepositoryModule::class,
+        ],
+)
+object AuthenticationModule
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
new file mode 100644
index 0000000..cd195f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.authentication.data.repository
+
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Defines interface for classes that can access authentication-related application state. */
+interface AuthenticationRepository {
+
+    /**
+     * Whether the device is unlocked.
+     *
+     * A device that is not yet unlocked requires unlocking by completing an authentication
+     * challenge according to the current authentication method.
+     *
+     * Note that this state has no real bearing on whether the lock screen is showing or dismissed.
+     */
+    val isUnlocked: StateFlow<Boolean>
+
+    /**
+     * The currently-configured authentication method. This determines how the authentication
+     * challenge is completed in order to unlock an otherwise locked device.
+     */
+    val authenticationMethod: StateFlow<AuthenticationMethodModel>
+
+    /**
+     * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
+     * dismisses once the authentication challenge is completed. For example, completing a biometric
+     * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
+     * lock screen.
+     */
+    val isBypassEnabled: StateFlow<Boolean>
+
+    /** See [isUnlocked]. */
+    fun setUnlocked(isUnlocked: Boolean)
+
+    /** See [authenticationMethod]. */
+    fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel)
+
+    /** See [isBypassEnabled]. */
+    fun setBypassEnabled(isBypassEnabled: Boolean)
+}
+
+class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository {
+    // TODO(b/280883900): get data from real data sources in SysUI.
+
+    private val _isUnlocked = MutableStateFlow(false)
+    override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()
+
+    private val _authenticationMethod =
+        MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.PIN(1234))
+    override val authenticationMethod: StateFlow<AuthenticationMethodModel> =
+        _authenticationMethod.asStateFlow()
+
+    private val _isBypassEnabled = MutableStateFlow(false)
+    override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
+
+    override fun setUnlocked(isUnlocked: Boolean) {
+        _isUnlocked.value = isUnlocked
+    }
+
+    override fun setBypassEnabled(isBypassEnabled: Boolean) {
+        _isBypassEnabled.value = isBypassEnabled
+    }
+
+    override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
+        _authenticationMethod.value = authenticationMethod
+    }
+}
+
+@Module
+interface AuthenticationRepositoryModule {
+    @Binds fun repository(impl: AuthenticationRepositoryImpl): AuthenticationRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
new file mode 100644
index 0000000..5aea930
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.authentication.domain.interactor
+
+import com.android.systemui.authentication.data.repository.AuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** Hosts application business logic related to authentication. */
+@SysUISingleton
+class AuthenticationInteractor
+@Inject
+constructor(
+    @Application applicationScope: CoroutineScope,
+    private val repository: AuthenticationRepository,
+) {
+    /**
+     * The currently-configured authentication method. This determines how the authentication
+     * challenge is completed in order to unlock an otherwise locked device.
+     */
+    val authenticationMethod: StateFlow<AuthenticationMethodModel> = repository.authenticationMethod
+
+    /**
+     * Whether the device is unlocked.
+     *
+     * A device that is not yet unlocked requires unlocking by completing an authentication
+     * challenge according to the current authentication method.
+     *
+     * Note that this state has no real bearing on whether the lock screen is showing or dismissed.
+     */
+    val isUnlocked: StateFlow<Boolean> =
+        combine(authenticationMethod, repository.isUnlocked) { authMethod, isUnlocked ->
+                isUnlockedWithAuthMethod(
+                    isUnlocked = isUnlocked,
+                    authMethod = authMethod,
+                )
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue =
+                    isUnlockedWithAuthMethod(
+                        isUnlocked = repository.isUnlocked.value,
+                        authMethod = repository.authenticationMethod.value,
+                    )
+            )
+
+    /**
+     * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
+     * dismisses once the authentication challenge is completed. For example, completing a biometric
+     * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
+     * lock screen.
+     */
+    val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
+
+    init {
+        // UNLOCKS WHEN AUTH METHOD REMOVED.
+        //
+        // Unlocks the device if the auth method becomes None.
+        applicationScope.launch {
+            repository.authenticationMethod.collect {
+                if (it is AuthenticationMethodModel.None) {
+                    unlockDevice()
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns `true` if the device currently requires authentication before content can be viewed;
+     * `false` if content can be displayed without unlocking first.
+     */
+    fun isAuthenticationRequired(): Boolean {
+        return !isUnlocked.value && authenticationMethod.value.isSecure
+    }
+
+    /**
+     * Unlocks the device, assuming that the authentication challenge has been completed
+     * successfully.
+     */
+    fun unlockDevice() {
+        repository.setUnlocked(true)
+    }
+
+    /**
+     * Locks the device. From now on, the device will remain locked until [authenticate] is called
+     * with the correct input.
+     */
+    fun lockDevice() {
+        repository.setUnlocked(false)
+    }
+
+    /**
+     * Attempts to authenticate the user and unlock the device.
+     *
+     * @param input The input from the user to try to authenticate with. This can be a list of
+     *   different things, based on the current authentication method.
+     * @return `true` if the authentication succeeded and the device is now unlocked; `false`
+     *   otherwise.
+     */
+    fun authenticate(input: List<Any>): Boolean {
+        val isSuccessful =
+            when (val authMethod = this.authenticationMethod.value) {
+                is AuthenticationMethodModel.PIN -> input.asCode() == authMethod.code
+                is AuthenticationMethodModel.Password -> input.asPassword() == authMethod.password
+                is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates
+                else -> true
+            }
+
+        if (isSuccessful) {
+            repository.setUnlocked(true)
+        }
+
+        return isSuccessful
+    }
+
+    /** Triggers a biometric-powered unlock of the device. */
+    fun biometricUnlock() {
+        // TODO(b/280883900): only allow this if the biometric is enabled and there's a match.
+        repository.setUnlocked(true)
+    }
+
+    /** See [authenticationMethod]. */
+    fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
+        repository.setAuthenticationMethod(authenticationMethod)
+    }
+
+    /** See [isBypassEnabled]. */
+    fun toggleBypassEnabled() {
+        repository.setBypassEnabled(!repository.isBypassEnabled.value)
+    }
+
+    companion object {
+        private fun isUnlockedWithAuthMethod(
+            isUnlocked: Boolean,
+            authMethod: AuthenticationMethodModel,
+        ): Boolean {
+            return if (authMethod is AuthenticationMethodModel.None) {
+                true
+            } else {
+                isUnlocked
+            }
+        }
+
+        /**
+         * Returns a PIN code from the given list. It's assumed the given list elements are all
+         * [Int].
+         */
+        private fun List<Any>.asCode(): Int? {
+            if (isEmpty()) {
+                return null
+            }
+
+            var code = 0
+            map { it as Int }.forEach { integer -> code = code * 10 + integer }
+
+            return code
+        }
+
+        /**
+         * Returns a password from the given list. It's assumed the given list elements are all
+         * [Char].
+         */
+        private fun List<Any>.asPassword(): String {
+            val anyList = this
+            return buildString { anyList.forEach { append(it as Char) } }
+        }
+
+        /**
+         * Returns a list of [AuthenticationMethodModel.Pattern.PatternCoordinate] from the given
+         * list. It's assumed the given list elements are all
+         * [AuthenticationMethodModel.Pattern.PatternCoordinate].
+         */
+        private fun List<Any>.asPattern():
+            List<AuthenticationMethodModel.Pattern.PatternCoordinate> {
+            val anyList = this
+            return buildList {
+                anyList.forEach { add(it as AuthenticationMethodModel.Pattern.PatternCoordinate) }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
new file mode 100644
index 0000000..83250b6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.authentication.shared.model
+
+/** Enumerates all known authentication methods. */
+sealed class AuthenticationMethodModel(
+    /**
+     * Whether the authentication method is considered to be "secure".
+     *
+     * "Secure" authentication methods require authentication to unlock the device. Non-secure auth
+     * methods simply require user dismissal.
+     */
+    open val isSecure: Boolean,
+) {
+    /** There is no authentication method on the device. We shouldn't even show the lock screen. */
+    object None : AuthenticationMethodModel(isSecure = false)
+
+    /** The most basic authentication method. The lock screen can be swiped away when displayed. */
+    object Swipe : AuthenticationMethodModel(isSecure = false)
+
+    data class PIN(val code: Int) : AuthenticationMethodModel(isSecure = true)
+
+    data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)
+
+    data class Pattern(val coordinates: List<PatternCoordinate>) :
+        AuthenticationMethodModel(isSecure = true) {
+
+        data class PatternCoordinate(
+            val x: Int,
+            val y: Int,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
new file mode 100644
index 0000000..4c817b2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.data.repo
+
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Provides access to bouncer-related application state. */
+@SysUISingleton
+class BouncerRepository @Inject constructor() {
+    private val _message = MutableStateFlow<String?>(null)
+    /** The user-facing message to show in the bouncer. */
+    val message: StateFlow<String?> = _message.asStateFlow()
+
+    fun setMessage(message: String?) {
+        _message.value = message
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
new file mode 100644
index 0000000..57ce580
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.domain.interactor
+
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+/** Encapsulates business logic and application state accessing use-cases. */
+class BouncerInteractor
+@AssistedInject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Application private val applicationContext: Context,
+    private val repository: BouncerRepository,
+    private val authenticationInteractor: AuthenticationInteractor,
+    private val sceneInteractor: SceneInteractor,
+    @Assisted private val containerName: String,
+) {
+
+    /** The user-facing message to show in the bouncer. */
+    val message: StateFlow<String?> = repository.message
+
+    /**
+     * The currently-configured authentication method. This determines how the authentication
+     * challenge is completed in order to unlock an otherwise locked device.
+     */
+    val authenticationMethod: StateFlow<AuthenticationMethodModel> =
+        authenticationInteractor.authenticationMethod
+
+    init {
+        applicationScope.launch {
+            combine(
+                    sceneInteractor.currentScene(containerName),
+                    authenticationInteractor.authenticationMethod,
+                    ::Pair,
+                )
+                .collect { (currentScene, authMethod) ->
+                    if (currentScene.key == SceneKey.Bouncer) {
+                        when (authMethod) {
+                            is AuthenticationMethodModel.None ->
+                                sceneInteractor.setCurrentScene(
+                                    containerName,
+                                    SceneModel(SceneKey.Gone),
+                                )
+                            is AuthenticationMethodModel.Swipe ->
+                                sceneInteractor.setCurrentScene(
+                                    containerName,
+                                    SceneModel(SceneKey.LockScreen),
+                                )
+                            else -> Unit
+                        }
+                    }
+                }
+        }
+    }
+
+    /**
+     * Either shows the bouncer or unlocks the device, if the bouncer doesn't need to be shown.
+     *
+     * @param containerName The name of the scene container to show the bouncer in.
+     * @param message An optional message to show to the user in the bouncer.
+     */
+    fun showOrUnlockDevice(
+        containerName: String,
+        message: String? = null,
+    ) {
+        if (authenticationInteractor.isAuthenticationRequired()) {
+            repository.setMessage(message ?: promptMessage(authenticationMethod.value))
+            sceneInteractor.setCurrentScene(
+                containerName = containerName,
+                scene = SceneModel(SceneKey.Bouncer),
+            )
+        } else {
+            authenticationInteractor.unlockDevice()
+            sceneInteractor.setCurrentScene(
+                containerName = containerName,
+                scene = SceneModel(SceneKey.Gone),
+            )
+        }
+    }
+
+    /**
+     * Resets the user-facing message back to the default according to the current authentication
+     * method.
+     */
+    fun resetMessage() {
+        repository.setMessage(promptMessage(authenticationMethod.value))
+    }
+
+    /** Removes the user-facing message. */
+    fun clearMessage() {
+        repository.setMessage(null)
+    }
+
+    /**
+     * Attempts to authenticate based on the given user input.
+     *
+     * If the input is correct, the device will be unlocked and the lock screen and bouncer will be
+     * dismissed and hidden.
+     */
+    fun authenticate(
+        input: List<Any>,
+    ) {
+        val isAuthenticated = authenticationInteractor.authenticate(input)
+        if (isAuthenticated) {
+            sceneInteractor.setCurrentScene(
+                containerName = containerName,
+                scene = SceneModel(SceneKey.Gone),
+            )
+        } else {
+            repository.setMessage(errorMessage(authenticationMethod.value))
+        }
+    }
+
+    private fun promptMessage(authMethod: AuthenticationMethodModel): String {
+        return when (authMethod) {
+            is AuthenticationMethodModel.PIN ->
+                applicationContext.getString(R.string.keyguard_enter_your_pin)
+            is AuthenticationMethodModel.Password ->
+                applicationContext.getString(R.string.keyguard_enter_your_password)
+            is AuthenticationMethodModel.Pattern ->
+                applicationContext.getString(R.string.keyguard_enter_your_pattern)
+            else -> ""
+        }
+    }
+
+    private fun errorMessage(authMethod: AuthenticationMethodModel): String {
+        return when (authMethod) {
+            is AuthenticationMethodModel.PIN -> applicationContext.getString(R.string.kg_wrong_pin)
+            is AuthenticationMethodModel.Password ->
+                applicationContext.getString(R.string.kg_wrong_password)
+            is AuthenticationMethodModel.Pattern ->
+                applicationContext.getString(R.string.kg_wrong_pattern)
+            else -> ""
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            containerName: String,
+        ): BouncerInteractor
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
new file mode 100644
index 0000000..8a183ae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.viewmodel
+
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.dagger.qualifiers.Application
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Holds UI state and handles user input on bouncer UIs. */
+class BouncerViewModel
+@AssistedInject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    interactorFactory: BouncerInteractor.Factory,
+    containerName: String,
+) {
+    private val interactor: BouncerInteractor = interactorFactory.create(containerName)
+
+    /** The user-facing message to show in the bouncer. */
+    val message: StateFlow<String> =
+        interactor.message
+            .map { it ?: "" }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = interactor.message.value ?: "",
+            )
+
+    /** Notifies that the authenticate button was clicked. */
+    fun onAuthenticateButtonClicked() {
+        // TODO(b/280877228): remove this and send the real input.
+        interactor.authenticate(
+            when (interactor.authenticationMethod.value) {
+                is AuthenticationMethodModel.PIN -> listOf(1, 2, 3, 4)
+                is AuthenticationMethodModel.Password -> "password".toList()
+                is AuthenticationMethodModel.Pattern ->
+                    listOf(
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(2, 0),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(2, 1),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(2, 2),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(0, 0),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(0, 1),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(0, 2),
+                    )
+                else -> emptyList()
+            }
+        )
+    }
+
+    /** Notifies that the emergency services button was clicked. */
+    fun onEmergencyServicesButtonClicked() {
+        // TODO(b/280877228): implement this.
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 97359d8..70c859e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -32,6 +32,7 @@
 import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule;
 import com.android.systemui.appops.dagger.AppOpsModule;
 import com.android.systemui.assist.AssistModule;
+import com.android.systemui.authentication.AuthenticationModule;
 import com.android.systemui.biometrics.AlternateUdfpsTouchProvider;
 import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
 import com.android.systemui.biometrics.FingerprintReEnrollNotification;
@@ -153,6 +154,7 @@
             AccessibilityRepositoryModule.class,
             AppOpsModule.class,
             AssistModule.class,
+            AuthenticationModule.class,
             BiometricsModule.class,
             BouncerViewModule.class,
             ClipboardOverlayModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt
new file mode 100644
index 0000000..6170180
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.util.kotlin.pairwise
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** Hosts business and application state accessing logic for the lock screen scene. */
+class LockScreenSceneInteractor
+@AssistedInject
+constructor(
+    @Application applicationScope: CoroutineScope,
+    private val authenticationInteractor: AuthenticationInteractor,
+    bouncerInteractorFactory: BouncerInteractor.Factory,
+    private val sceneInteractor: SceneInteractor,
+    @Assisted private val containerName: String,
+) {
+    private val bouncerInteractor: BouncerInteractor =
+        bouncerInteractorFactory.create(containerName)
+
+    /** Whether the device is currently locked. */
+    val isDeviceLocked: StateFlow<Boolean> =
+        authenticationInteractor.isUnlocked
+            .map { !it }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = !authenticationInteractor.isUnlocked.value,
+            )
+
+    /** Whether it's currently possible to swipe up to dismiss the lock screen. */
+    val isSwipeToDismissEnabled: StateFlow<Boolean> =
+        combine(
+                authenticationInteractor.isUnlocked,
+                authenticationInteractor.authenticationMethod,
+            ) { isUnlocked, authMethod ->
+                isSwipeToUnlockEnabled(
+                    isUnlocked = isUnlocked,
+                    authMethod = authMethod,
+                )
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue =
+                    isSwipeToUnlockEnabled(
+                        isUnlocked = authenticationInteractor.isUnlocked.value,
+                        authMethod = authenticationInteractor.authenticationMethod.value,
+                    ),
+            )
+
+    init {
+        // LOCKING SHOWS LOCK SCREEN.
+        //
+        // Move to the lock screen scene if the device becomes locked while in any scene.
+        applicationScope.launch {
+            authenticationInteractor.isUnlocked
+                .map { !it }
+                .distinctUntilChanged()
+                .collect { isLocked ->
+                    if (isLocked) {
+                        sceneInteractor.setCurrentScene(
+                            containerName = containerName,
+                            scene = SceneModel(SceneKey.LockScreen),
+                        )
+                    }
+                }
+        }
+
+        // BYPASS UNLOCK.
+        //
+        // Moves to the gone scene if bypass is enabled and the device becomes unlocked while in the
+        // lock screen scene.
+        applicationScope.launch {
+            combine(
+                    authenticationInteractor.isBypassEnabled,
+                    authenticationInteractor.isUnlocked,
+                    sceneInteractor.currentScene(containerName),
+                    ::Triple,
+                )
+                .collect { (isBypassEnabled, isUnlocked, currentScene) ->
+                    if (isBypassEnabled && isUnlocked && currentScene.key == SceneKey.LockScreen) {
+                        sceneInteractor.setCurrentScene(
+                            containerName = containerName,
+                            scene = SceneModel(SceneKey.Gone),
+                        )
+                    }
+                }
+        }
+
+        // SWIPE TO DISMISS LOCK SCREEN.
+        //
+        // If switched from the lock screen to the gone scene and the auth method was a swipe,
+        // unlocks the device.
+        applicationScope.launch {
+            combine(
+                    authenticationInteractor.authenticationMethod,
+                    sceneInteractor.currentScene(containerName).pairwise(),
+                    ::Pair,
+                )
+                .collect { (authMethod, scenes) ->
+                    val (previousScene, currentScene) = scenes
+                    if (
+                        authMethod is AuthenticationMethodModel.Swipe &&
+                            previousScene.key == SceneKey.LockScreen &&
+                            currentScene.key == SceneKey.Gone
+                    ) {
+                        authenticationInteractor.unlockDevice()
+                    }
+                }
+        }
+
+        // DISMISS LOCK SCREEN IF AUTH METHOD IS REMOVED.
+        //
+        // If the auth method becomes None while on the lock screen scene, dismisses the lock
+        // screen.
+        applicationScope.launch {
+            combine(
+                    authenticationInteractor.authenticationMethod,
+                    sceneInteractor.currentScene(containerName),
+                    ::Pair,
+                )
+                .collect { (authMethod, scene) ->
+                    if (
+                        scene.key == SceneKey.LockScreen &&
+                            authMethod == AuthenticationMethodModel.None
+                    ) {
+                        sceneInteractor.setCurrentScene(
+                            containerName = containerName,
+                            scene = SceneModel(SceneKey.Gone),
+                        )
+                    }
+                }
+        }
+    }
+
+    /** Attempts to dismiss the lock screen. This will cause the bouncer to show, if needed. */
+    fun dismissLockScreen() {
+        bouncerInteractor.showOrUnlockDevice(containerName = containerName)
+    }
+
+    private fun isSwipeToUnlockEnabled(
+        isUnlocked: Boolean,
+        authMethod: AuthenticationMethodModel,
+    ): Boolean {
+        return !isUnlocked && authMethod is AuthenticationMethodModel.Swipe
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            containerName: String,
+        ): LockScreenSceneInteractor
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt
new file mode 100644
index 0000000..08b9613
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Models UI state and handles user input for the lock screen scene. */
+class LockScreenSceneViewModel
+@AssistedInject
+constructor(
+    @Application applicationScope: CoroutineScope,
+    interactorFactory: LockScreenSceneInteractor.Factory,
+    @Assisted containerName: String,
+) {
+    private val interactor: LockScreenSceneInteractor = interactorFactory.create(containerName)
+
+    /** The icon for the "lock" button on the lock screen. */
+    val lockButtonIcon: StateFlow<Icon> =
+        interactor.isDeviceLocked
+            .map { isLocked -> lockIcon(isLocked = isLocked) }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = lockIcon(isLocked = interactor.isDeviceLocked.value),
+            )
+
+    /** The key of the scene we should switch to when swiping up. */
+    val upDestinationSceneKey: StateFlow<SceneKey> =
+        interactor.isSwipeToDismissEnabled
+            .map { isSwipeToUnlockEnabled -> upDestinationSceneKey(isSwipeToUnlockEnabled) }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = upDestinationSceneKey(interactor.isSwipeToDismissEnabled.value),
+            )
+
+    /** Notifies that the lock button on the lock screen was clicked. */
+    fun onLockButtonClicked() {
+        interactor.dismissLockScreen()
+    }
+
+    /** Notifies that some content on the lock screen was clicked. */
+    fun onContentClicked() {
+        interactor.dismissLockScreen()
+    }
+
+    private fun upDestinationSceneKey(
+        isSwipeToUnlockEnabled: Boolean,
+    ): SceneKey {
+        return if (isSwipeToUnlockEnabled) SceneKey.Gone else SceneKey.Bouncer
+    }
+
+    private fun lockIcon(
+        isLocked: Boolean,
+    ): Icon {
+        return Icon.Resource(
+            res =
+                if (isLocked) {
+                    R.drawable.ic_device_lock_on
+                } else {
+                    R.drawable.ic_device_lock_off
+                },
+            contentDescription =
+                ContentDescription.Resource(
+                    res =
+                        if (isLocked) {
+                            R.string.accessibility_lock_icon
+                        } else {
+                            R.string.accessibility_unlock_button
+                        }
+                )
+        )
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            containerName: String,
+        ): LockScreenSceneViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
new file mode 100644
index 0000000..6525a98
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** Models UI state and handles user input for the quick settings scene. */
+class QuickSettingsSceneViewModel
+@AssistedInject
+constructor(
+    lockScreenSceneInteractorFactory: LockScreenSceneInteractor.Factory,
+    @Assisted containerName: String,
+) {
+    private val lockScreenSceneInteractor: LockScreenSceneInteractor =
+        lockScreenSceneInteractorFactory.create(containerName)
+
+    /** Notifies that some content in quick settings was clicked. */
+    fun onContentClicked() {
+        lockScreenSceneInteractor.dismissLockScreen()
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            containerName: String,
+        ): QuickSettingsSceneViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
new file mode 100644
index 0000000..dcae258
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.ui.viewmodel
+
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Models UI state and handles user input for the shade scene. */
+class ShadeSceneViewModel
+@AssistedInject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    lockScreenSceneInteractorFactory: LockScreenSceneInteractor.Factory,
+    @Assisted private val containerName: String,
+) {
+    private val lockScreenInteractor: LockScreenSceneInteractor =
+        lockScreenSceneInteractorFactory.create(containerName)
+
+    /** The key of the scene we should switch to when swiping up. */
+    val upDestinationSceneKey: StateFlow<SceneKey> =
+        lockScreenInteractor.isDeviceLocked
+            .map { isLocked -> upDestinationSceneKey(isLocked = isLocked) }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue =
+                    upDestinationSceneKey(
+                        isLocked = lockScreenInteractor.isDeviceLocked.value,
+                    ),
+            )
+
+    /** Notifies that some content in the shade was clicked. */
+    fun onContentClicked() {
+        lockScreenInteractor.dismissLockScreen()
+    }
+
+    private fun upDestinationSceneKey(
+        isLocked: Boolean,
+    ): SceneKey {
+        return if (isLocked) SceneKey.LockScreen else SceneKey.Gone
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            containerName: String,
+        ): ShadeSceneViewModel
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
new file mode 100644
index 0000000..2e62beb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.authentication.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepository
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class AuthenticationInteractorTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val repository: AuthenticationRepository = AuthenticationRepositoryImpl()
+    private val underTest =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = repository,
+        )
+
+    @Test
+    fun authMethod() =
+        testScope.runTest {
+            val authMethod by collectLastValue(underTest.authenticationMethod)
+            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.PIN(1234))
+
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password("password"))
+        }
+
+    @Test
+    fun isUnlocked_whenAuthMethodIsNone_isTrue() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun unlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+
+            underTest.unlockDevice()
+            runCurrent()
+
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun biometricUnlock() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+
+            underTest.biometricUnlock()
+            runCurrent()
+
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun toggleBypassEnabled() =
+        testScope.runTest {
+            val isBypassEnabled by collectLastValue(underTest.isBypassEnabled)
+            assertThat(isBypassEnabled).isFalse()
+
+            underTest.toggleBypassEnabled()
+            assertThat(isBypassEnabled).isTrue()
+
+            underTest.toggleBypassEnabled()
+            assertThat(isBypassEnabled).isFalse()
+        }
+
+    @Test
+    fun isAuthenticationRequired_lockedAndSecured_true() =
+        testScope.runTest {
+            underTest.lockDevice()
+            runCurrent()
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+
+            assertThat(underTest.isAuthenticationRequired()).isTrue()
+        }
+
+    @Test
+    fun isAuthenticationRequired_lockedAndNotSecured_false() =
+        testScope.runTest {
+            underTest.lockDevice()
+            runCurrent()
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+
+            assertThat(underTest.isAuthenticationRequired()).isFalse()
+        }
+
+    @Test
+    fun isAuthenticationRequired_unlockedAndSecured_false() =
+        testScope.runTest {
+            underTest.unlockDevice()
+            runCurrent()
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+
+            assertThat(underTest.isAuthenticationRequired()).isFalse()
+        }
+
+    @Test
+    fun isAuthenticationRequired_unlockedAndNotSecured_false() =
+        testScope.runTest {
+            underTest.unlockDevice()
+            runCurrent()
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+
+            assertThat(underTest.isAuthenticationRequired()).isFalse()
+        }
+
+    @Test
+    fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(underTest.authenticate("password".toList())).isTrue()
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(underTest.authenticate("alohomora".toList())).isFalse()
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(
+                AuthenticationMethodModel.Pattern(
+                    listOf(
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 0,
+                        ),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 1,
+                        ),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 2,
+                        ),
+                    )
+                )
+            )
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(
+                    underTest.authenticate(
+                        listOf(
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 0,
+                                y = 0,
+                            ),
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 0,
+                                y = 1,
+                            ),
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 0,
+                                y = 2,
+                            ),
+                        )
+                    )
+                )
+                .isTrue()
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            underTest.setAuthenticationMethod(
+                AuthenticationMethodModel.Pattern(
+                    listOf(
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 0,
+                        ),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 1,
+                        ),
+                        AuthenticationMethodModel.Pattern.PatternCoordinate(
+                            x = 0,
+                            y = 2,
+                        ),
+                    )
+                )
+            )
+            assertThat(isUnlocked).isFalse()
+
+            assertThat(
+                    underTest.authenticate(
+                        listOf(
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 2,
+                                y = 0,
+                            ),
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 2,
+                                y = 1,
+                            ),
+                            AuthenticationMethodModel.Pattern.PatternCoordinate(
+                                x = 2,
+                                y = 2,
+                            ),
+                        )
+                    )
+                )
+                .isFalse()
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun unlocksDevice_whenAuthMethodBecomesNone() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+
+            repository.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+            assertThat(isUnlocked).isTrue()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
new file mode 100644
index 0000000..7dd376e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class BouncerInteractorTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val authenticationInteractor =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = AuthenticationRepositoryImpl(),
+        )
+    private val sceneInteractor =
+        SceneInteractor(
+            repository = fakeSceneContainerRepository(),
+        )
+    private val underTest =
+        BouncerInteractor(
+            applicationScope = testScope.backgroundScope,
+            applicationContext = context,
+            repository = BouncerRepository(),
+            authenticationInteractor = authenticationInteractor,
+            sceneInteractor = sceneInteractor,
+            containerName = "container1",
+        )
+
+    @Before
+    fun setUp() {
+        overrideResource(R.string.keyguard_enter_your_pin, MESSAGE_ENTER_YOUR_PIN)
+        overrideResource(R.string.keyguard_enter_your_password, MESSAGE_ENTER_YOUR_PASSWORD)
+        overrideResource(R.string.keyguard_enter_your_pattern, MESSAGE_ENTER_YOUR_PATTERN)
+        overrideResource(R.string.kg_wrong_pin, MESSAGE_WRONG_PIN)
+        overrideResource(R.string.kg_wrong_password, MESSAGE_WRONG_PASSWORD)
+        overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN)
+    }
+
+    @Test
+    fun pinAuthMethod() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            val message by collectLastValue(underTest.message)
+
+            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            authenticationInteractor.lockDevice()
+            underTest.showOrUnlockDevice("container1")
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
+
+            underTest.clearMessage()
+            assertThat(message).isNull()
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
+
+            // Wrong input.
+            underTest.authenticate(listOf(9, 8, 7))
+            assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
+
+            // Correct input.
+            underTest.authenticate(listOf(1, 2, 3, 4))
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun passwordAuthMethod() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            val message by collectLastValue(underTest.message)
+            authenticationInteractor.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+            authenticationInteractor.lockDevice()
+            underTest.showOrUnlockDevice("container1")
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
+
+            underTest.clearMessage()
+            assertThat(message).isNull()
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
+
+            // Wrong input.
+            underTest.authenticate("alohamora".toList())
+            assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
+
+            // Correct input.
+            underTest.authenticate("password".toList())
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun patternAuthMethod() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            val message by collectLastValue(underTest.message)
+            authenticationInteractor.setAuthenticationMethod(
+                AuthenticationMethodModel.Pattern(emptyList())
+            )
+            authenticationInteractor.lockDevice()
+            underTest.showOrUnlockDevice("container1")
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
+
+            underTest.clearMessage()
+            assertThat(message).isNull()
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
+
+            // Wrong input.
+            underTest.authenticate(
+                listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(3, 4))
+            )
+            assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
+
+            // Correct input.
+            underTest.authenticate(emptyList())
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun showOrUnlockDevice_notLocked_switchesToGoneScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            authenticationInteractor.unlockDevice()
+            runCurrent()
+
+            underTest.showOrUnlockDevice("container1")
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun showOrUnlockDevice_authMethodNotSecure_switchesToGoneScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            authenticationInteractor.lockDevice()
+
+            underTest.showOrUnlockDevice("container1")
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun showOrUnlockDevice_customMessageShown() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            val message by collectLastValue(underTest.message)
+            authenticationInteractor.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+            authenticationInteractor.lockDevice()
+
+            val customMessage = "Hello there!"
+            underTest.showOrUnlockDevice("container1", customMessage)
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(message).isEqualTo(customMessage)
+        }
+
+    companion object {
+        private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
+        private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
+        private const val MESSAGE_ENTER_YOUR_PATTERN = "Enter your pattern"
+        private const val MESSAGE_WRONG_PIN = "Wrong PIN"
+        private const val MESSAGE_WRONG_PASSWORD = "Wrong password"
+        private const val MESSAGE_WRONG_PATTERN = "Wrong pattern"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt
new file mode 100644
index 0000000..749e7a0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class LockScreenSceneInteractorTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val sceneInteractor =
+        SceneInteractor(
+            repository = fakeSceneContainerRepository(),
+        )
+    private val mAuthenticationInteractor =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = AuthenticationRepositoryImpl(),
+        )
+    private val underTest =
+        LockScreenSceneInteractor(
+            applicationScope = testScope.backgroundScope,
+            authenticationInteractor = mAuthenticationInteractor,
+            bouncerInteractorFactory =
+                object : BouncerInteractor.Factory {
+                    override fun create(containerName: String): BouncerInteractor {
+                        return BouncerInteractor(
+                            applicationScope = testScope.backgroundScope,
+                            applicationContext = context,
+                            repository = BouncerRepository(),
+                            authenticationInteractor = mAuthenticationInteractor,
+                            sceneInteractor = sceneInteractor,
+                            containerName = containerName,
+                        )
+                    }
+                },
+            sceneInteractor = sceneInteractor,
+            containerName = CONTAINER_NAME,
+        )
+
+    @Test
+    fun isDeviceLocked() =
+        testScope.runTest {
+            val isDeviceLocked by collectLastValue(underTest.isDeviceLocked)
+
+            mAuthenticationInteractor.lockDevice()
+            assertThat(isDeviceLocked).isTrue()
+
+            mAuthenticationInteractor.unlockDevice()
+            assertThat(isDeviceLocked).isFalse()
+        }
+
+    @Test
+    fun isSwipeToDismissEnabled_deviceLockedAndAuthMethodSwipe_true() =
+        testScope.runTest {
+            val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
+
+            mAuthenticationInteractor.lockDevice()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+
+            assertThat(isSwipeToDismissEnabled).isTrue()
+        }
+
+    @Test
+    fun isSwipeToDismissEnabled_deviceUnlockedAndAuthMethodSwipe_false() =
+        testScope.runTest {
+            val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
+
+            mAuthenticationInteractor.unlockDevice()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+
+            assertThat(isSwipeToDismissEnabled).isFalse()
+        }
+
+    @Test
+    fun dismissLockScreen_deviceLockedWithSecureAuthMethod_switchesToBouncer() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.lockDevice()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            underTest.dismissLockScreen()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    @Test
+    fun dismissLockScreen_deviceUnlocked_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.unlockDevice()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            underTest.dismissLockScreen()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun dismissLockScreen_deviceLockedWithInsecureAuthMethod_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.lockDevice()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            underTest.dismissLockScreen()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun deviceLockedInNonLockScreenScene_switchesToLockScreenScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            runCurrent()
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
+            runCurrent()
+            mAuthenticationInteractor.unlockDevice()
+            runCurrent()
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+
+            mAuthenticationInteractor.lockDevice()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+        }
+
+    @Test
+    fun deviceBiometricUnlockedInLockScreen_bypassEnabled_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.lockDevice()
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
+            if (!mAuthenticationInteractor.isBypassEnabled.value) {
+                mAuthenticationInteractor.toggleBypassEnabled()
+            }
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            mAuthenticationInteractor.biometricUnlock()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun deviceBiometricUnlockedInLockScreen_bypassNotEnabled_doesNotSwitch() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.lockDevice()
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
+            if (mAuthenticationInteractor.isBypassEnabled.value) {
+                mAuthenticationInteractor.toggleBypassEnabled()
+            }
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            mAuthenticationInteractor.biometricUnlock()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+        }
+
+    @Test
+    fun switchFromLockScreenToGone_authMethodSwipe_unlocksDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            assertThat(isUnlocked).isFalse()
+
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
+
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            assertThat(isUnlocked).isFalse()
+
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
+
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun switchFromNonLockScreenToGone_authMethodSwipe_doesNotUnlockDevice() =
+        testScope.runTest {
+            val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
+            runCurrent()
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Shade))
+            runCurrent()
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            runCurrent()
+            assertThat(isUnlocked).isFalse()
+
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
+
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun authMethodChangedToNone_onLockScreenScene_dismissesLockScreen() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
+
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun authMethodChangedToNone_notOnLockScreenScene_doesNotDismissLockScreen() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            runCurrent()
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.QuickSettings))
+            runCurrent()
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
+
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
+        }
+
+    companion object {
+        private const val CONTAINER_NAME = "container1"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt
new file mode 100644
index 0000000..d335b09
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class LockScreenSceneViewModelTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val sceneInteractor =
+        SceneInteractor(
+            repository = fakeSceneContainerRepository(),
+        )
+    private val mAuthenticationInteractor =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = AuthenticationRepositoryImpl(),
+        )
+
+    private val underTest =
+        LockScreenSceneViewModel(
+            applicationScope = testScope.backgroundScope,
+            interactorFactory =
+                object : LockScreenSceneInteractor.Factory {
+                    override fun create(containerName: String): LockScreenSceneInteractor {
+                        return LockScreenSceneInteractor(
+                            applicationScope = testScope.backgroundScope,
+                            authenticationInteractor = mAuthenticationInteractor,
+                            bouncerInteractorFactory =
+                                object : BouncerInteractor.Factory {
+                                    override fun create(containerName: String): BouncerInteractor {
+                                        return BouncerInteractor(
+                                            applicationScope = testScope.backgroundScope,
+                                            applicationContext = context,
+                                            repository = BouncerRepository(),
+                                            authenticationInteractor = mAuthenticationInteractor,
+                                            sceneInteractor = sceneInteractor,
+                                            containerName = containerName,
+                                        )
+                                    }
+                                },
+                            sceneInteractor = sceneInteractor,
+                            containerName = CONTAINER_NAME,
+                        )
+                    }
+                },
+            containerName = CONTAINER_NAME
+        )
+
+    @Test
+    fun lockButtonIcon_whenLocked() =
+        testScope.runTest {
+            val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
+            mAuthenticationInteractor.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+            mAuthenticationInteractor.lockDevice()
+
+            assertThat((lockButtonIcon as? Icon.Resource)?.res)
+                .isEqualTo(R.drawable.ic_device_lock_on)
+        }
+
+    @Test
+    fun lockButtonIcon_whenUnlocked() =
+        testScope.runTest {
+            val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
+            mAuthenticationInteractor.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+            mAuthenticationInteractor.unlockDevice()
+
+            assertThat((lockButtonIcon as? Icon.Resource)?.res)
+                .isEqualTo(R.drawable.ic_device_lock_off)
+        }
+
+    @Test
+    fun upTransitionSceneKey_swipeToUnlockedEnabled_gone() =
+        testScope.runTest {
+            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            mAuthenticationInteractor.lockDevice()
+
+            assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
+        }
+
+    @Test
+    fun upTransitionSceneKey_swipeToUnlockedNotEnabled_bouncer() =
+        testScope.runTest {
+            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+
+            assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer)
+        }
+
+    @Test
+    fun onLockButtonClicked_deviceLockedSecurely_switchesToBouncer() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+            runCurrent()
+
+            underTest.onLockButtonClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    @Test
+    fun onContentClicked_deviceUnlocked_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.unlockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    @Test
+    fun onLockButtonClicked_deviceUnlocked_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.unlockDevice()
+            runCurrent()
+
+            underTest.onLockButtonClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    companion object {
+        private const val CONTAINER_NAME = "container1"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
new file mode 100644
index 0000000..e8875be
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class QuickSettingsSceneViewModelTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val sceneInteractor =
+        SceneInteractor(
+            repository = fakeSceneContainerRepository(),
+        )
+    private val mAuthenticationInteractor =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = AuthenticationRepositoryImpl(),
+        )
+
+    private val underTest =
+        QuickSettingsSceneViewModel(
+            lockScreenSceneInteractorFactory =
+                object : LockScreenSceneInteractor.Factory {
+                    override fun create(containerName: String): LockScreenSceneInteractor {
+                        return LockScreenSceneInteractor(
+                            applicationScope = testScope.backgroundScope,
+                            authenticationInteractor = mAuthenticationInteractor,
+                            bouncerInteractorFactory =
+                                object : BouncerInteractor.Factory {
+                                    override fun create(containerName: String): BouncerInteractor {
+                                        return BouncerInteractor(
+                                            applicationScope = testScope.backgroundScope,
+                                            applicationContext = context,
+                                            repository = BouncerRepository(),
+                                            authenticationInteractor = mAuthenticationInteractor,
+                                            sceneInteractor = sceneInteractor,
+                                            containerName = containerName,
+                                        )
+                                    }
+                                },
+                            sceneInteractor = sceneInteractor,
+                            containerName = CONTAINER_NAME,
+                        )
+                    }
+                },
+            containerName = CONTAINER_NAME
+        )
+
+    @Test
+    fun onContentClicked_deviceUnlocked_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.unlockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    companion object {
+        private const val CONTAINER_NAME = "container1"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
new file mode 100644
index 0000000..688cce8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
+import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ShadeSceneViewModelTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val sceneInteractor =
+        SceneInteractor(
+            repository = fakeSceneContainerRepository(),
+        )
+    private val mAuthenticationInteractor =
+        AuthenticationInteractor(
+            applicationScope = testScope.backgroundScope,
+            repository = AuthenticationRepositoryImpl(),
+        )
+
+    private val underTest =
+        ShadeSceneViewModel(
+            applicationScope = testScope.backgroundScope,
+            lockScreenSceneInteractorFactory =
+                object : LockScreenSceneInteractor.Factory {
+                    override fun create(containerName: String): LockScreenSceneInteractor {
+                        return LockScreenSceneInteractor(
+                            applicationScope = testScope.backgroundScope,
+                            authenticationInteractor = mAuthenticationInteractor,
+                            bouncerInteractorFactory =
+                                object : BouncerInteractor.Factory {
+                                    override fun create(containerName: String): BouncerInteractor {
+                                        return BouncerInteractor(
+                                            applicationScope = testScope.backgroundScope,
+                                            applicationContext = context,
+                                            repository = BouncerRepository(),
+                                            authenticationInteractor = mAuthenticationInteractor,
+                                            sceneInteractor = sceneInteractor,
+                                            containerName = containerName,
+                                        )
+                                    }
+                                },
+                            sceneInteractor = sceneInteractor,
+                            containerName = CONTAINER_NAME,
+                        )
+                    }
+                },
+            containerName = CONTAINER_NAME
+        )
+
+    @Test
+    fun upTransitionSceneKey_deviceLocked_lockScreen() =
+        testScope.runTest {
+            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+
+            assertThat(upTransitionSceneKey).isEqualTo(SceneKey.LockScreen)
+        }
+
+    @Test
+    fun upTransitionSceneKey_deviceUnlocked_gone() =
+        testScope.runTest {
+            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.unlockDevice()
+
+            assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
+        }
+
+    @Test
+    fun onContentClicked_deviceUnlocked_switchesToGone() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.unlockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            mAuthenticationInteractor.lockDevice()
+            runCurrent()
+
+            underTest.onContentClicked()
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    companion object {
+        private const val CONTAINER_NAME = "container1"
+    }
+}