Split up KeyguardBottomArea

This splits the KeyguardBottomArea into many pieces:
- Left shortcut
- Right shortcut
- Ambient indication container
- Customize Lockscreen button

Each of the elements above are now able to be independently laid out.

The alpha of the entire KeyguardRootView can now be easily modified, rather than having to adjust the alpha of each sub-element individually.

Fixes: 278057014
Test: atest KeyguardQuickAffordancesCombinedViewModelTest.kt
Test: atest KeyguardRootViewModelTest.kt
Change-Id: I098174809144cdf09dd71665ab724f8c508ae9e9
This commit is contained in:
Brad Hinegardner
2023-07-05 19:22:51 +00:00
parent 9ae559836f
commit a3a7fabf3b
32 changed files with 2821 additions and 117 deletions

View File

@@ -65,17 +65,17 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include layout="@layout/status_bar_expanded"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
<!-- Root for all keyguard content. It was previously located within the shade. -->
<com.android.systemui.keyguard.ui.view.KeyguardRootView
android:id="@id/keyguard_root_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include layout="@layout/status_bar_expanded"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
<!-- Shared container for the notification stack. Can be positioned by either
the keyguard_root_view or notification_panel -->
<com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer

View File

@@ -821,6 +821,12 @@
<dimen name="keyguard_affordance_fixed_radius">24dp</dimen>
<dimen name="keyguard_affordance_fixed_padding">12dp</dimen>
<!-- The width/height/padding of the keyguard settings popup menu -->
<dimen name="keyguard_settings_popup_menu_icon_height">24dp</dimen>
<dimen name="keyguard_settings_popup_menu_icon_width">24dp</dimen>
<dimen name="keyguard_settings_popup_menu_icon_end_margin">8dp</dimen>
<dimen name="keyguard_settings_popup_menu_padding">12dp</dimen>
<!-- Amount the button should shake when it's not long-pressed for long enough. -->
<dimen name="keyguard_affordance_shake_amplitude">8dp</dimen>

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyguard
import android.content.res.Configuration
import android.view.View
import android.view.ViewGroup
import com.android.systemui.CoreStartable
@@ -24,18 +25,29 @@ import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.ui.binder.KeyguardAmbientIndicationAreaViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardSettingsViewBinder
import com.android.systemui.keyguard.ui.view.KeyguardRootView
import com.android.systemui.keyguard.ui.viewmodel.KeyguardAmbientIndicationViewModel
import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManager
import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManagerCommandListener
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
import com.android.systemui.shade.NotificationShadeWindowView
import com.android.systemui.statusbar.KeyguardIndicationController
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import javax.inject.Inject
import kotlinx.coroutines.DisposableHandle
@@ -49,26 +61,44 @@ class KeyguardViewConfigurator
constructor(
private val keyguardRootView: KeyguardRootView,
private val sharedNotificationContainer: SharedNotificationContainer,
private val keyguardRootViewModel: KeyguardRootViewModel,
private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel,
private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
private val keyguardAmbientIndicationViewModel: KeyguardAmbientIndicationViewModel,
private val notificationShadeWindowView: NotificationShadeWindowView,
private val featureFlags: FeatureFlags,
private val indicationController: KeyguardIndicationController,
private val keyguardLayoutManager: KeyguardLayoutManager,
private val keyguardLayoutManagerCommandListener: KeyguardLayoutManagerCommandListener,
private val keyguardQuickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel,
private val falsingManager: FalsingManager,
private val vibratorHelper: VibratorHelper,
private val keyguardStateController: KeyguardStateController,
private val keyguardSettingsMenuViewModel: KeyguardSettingsMenuViewModel,
private val activityStarter: ActivityStarter,
private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
private val chipbarCoordinator: ChipbarCoordinator,
) : CoreStartable {
private var rootViewHandle: DisposableHandle? = null
private var indicationAreaHandle: DisposableHandle? = null
private var leftShortcutHandle: KeyguardQuickAffordanceViewBinder.Binding? = null
private var rightShortcutHandle: KeyguardQuickAffordanceViewBinder.Binding? = null
private var ambientIndicationAreaHandle: KeyguardAmbientIndicationAreaViewBinder.Binding? = null
private var settingsPopupMenuHandle: DisposableHandle? = null
override fun start() {
bindKeyguardRootView()
val notificationPanel =
notificationShadeWindowView.requireViewById(R.id.notification_panel) as ViewGroup
bindIndicationArea(notificationPanel)
unbindKeyguardBottomArea(notificationPanel)
bindIndicationArea()
bindLockIconView(notificationPanel)
setupNotificationStackScrollLayout(notificationPanel)
bindLeftShortcut()
bindRightShortcut()
bindAmbientIndicationArea()
bindSettingsPopupMenu()
keyguardLayoutManager.layoutViews()
keyguardLayoutManagerCommandListener.start()
@@ -90,16 +120,21 @@ constructor(
}
}
fun bindIndicationArea(legacyParent: ViewGroup) {
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
leftShortcutHandle?.onConfigurationChanged()
rightShortcutHandle?.onConfigurationChanged()
ambientIndicationAreaHandle?.onConfigurationChanged()
keyguardLayoutManager.layoutViews()
}
fun bindIndicationArea() {
indicationAreaHandle?.dispose()
// At startup, 2 views with the ID `R.id.keyguard_indication_area` will be available.
// Disable one of them
if (featureFlags.isEnabled(Flags.MIGRATE_INDICATION_AREA)) {
legacyParent.findViewById<View>(R.id.keyguard_indication_area)?.let {
legacyParent.removeView(it)
}
} else {
if (!featureFlags.isEnabled(Flags.MIGRATE_INDICATION_AREA)) {
keyguardRootView.findViewById<View?>(R.id.keyguard_indication_area)?.let {
keyguardRootView.removeView(it)
}
@@ -109,10 +144,24 @@ constructor(
KeyguardIndicationAreaBinder.bind(
notificationShadeWindowView,
keyguardIndicationAreaViewModel,
indicationController
keyguardRootViewModel,
indicationController,
featureFlags,
)
}
private fun bindKeyguardRootView() {
rootViewHandle?.dispose()
rootViewHandle = KeyguardRootViewBinder.bind(
keyguardRootView,
keyguardRootViewModel,
featureFlags,
occludingAppDeviceEntryMessageViewModel,
chipbarCoordinator,
keyguardStateController,
)
}
private fun bindLockIconView(legacyParent: ViewGroup) {
if (featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) {
legacyParent.requireViewById<View>(R.id.lock_icon_view).let {
@@ -125,12 +174,84 @@ constructor(
}
}
private fun bindKeyguardRootView() {
KeyguardRootViewBinder.bind(
keyguardRootView,
featureFlags,
occludingAppDeviceEntryMessageViewModel,
chipbarCoordinator,
)
private fun bindAmbientIndicationArea() {
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
ambientIndicationAreaHandle?.destroy()
ambientIndicationAreaHandle =
KeyguardAmbientIndicationAreaViewBinder.bind(
notificationShadeWindowView,
keyguardAmbientIndicationViewModel,
keyguardRootViewModel,
)
} else {
keyguardRootView.findViewById<View?>(R.id.ambient_indication_container)?.let {
keyguardRootView.removeView(it)
}
}
}
private fun bindSettingsPopupMenu() {
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
settingsPopupMenuHandle?.dispose()
settingsPopupMenuHandle =
KeyguardSettingsViewBinder.bind(
keyguardRootView,
keyguardSettingsMenuViewModel,
vibratorHelper,
activityStarter,
)
} else {
keyguardRootView.findViewById<View?>(R.id.keyguard_settings_button)?.let {
keyguardRootView.removeView(it)
}
}
}
private fun unbindKeyguardBottomArea(legacyParent: ViewGroup) {
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
legacyParent.requireViewById<View>(R.id.keyguard_bottom_area).let {
legacyParent.removeView(it)
}
}
}
private fun bindLeftShortcut() {
leftShortcutHandle?.destroy()
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
leftShortcutHandle =
KeyguardQuickAffordanceViewBinder.bind(
keyguardRootView.requireViewById(R.id.start_button),
keyguardQuickAffordancesCombinedViewModel.startButton,
keyguardRootViewModel.alpha,
falsingManager,
vibratorHelper,
) {
indicationController.showTransientIndication(it)
}
} else {
keyguardRootView.findViewById<View?>(R.id.start_button)?.let {
keyguardRootView.removeView(it)
}
}
}
private fun bindRightShortcut() {
rightShortcutHandle?.destroy()
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
rightShortcutHandle =
KeyguardQuickAffordanceViewBinder.bind(
keyguardRootView.requireViewById(R.id.end_button),
keyguardQuickAffordancesCombinedViewModel.endButton,
keyguardRootViewModel.alpha,
falsingManager,
vibratorHelper,
) {
indicationController.showTransientIndication(it)
}
} else {
keyguardRootView.findViewById<View?>(R.id.end_button)?.let {
keyguardRootView.removeView(it)
}
}
}
}

View File

@@ -37,6 +37,7 @@ import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardRootViewVisibilityState
import com.android.systemui.keyguard.shared.model.ScreenModel
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.WakefulnessModel
@@ -76,6 +77,8 @@ interface KeyguardRepository {
*/
val bottomAreaAlpha: StateFlow<Float>
val keyguardAlpha: StateFlow<Float>
/**
* Observable of the relative offset of the lock-screen clock from its natural position on the
* screen.
@@ -170,6 +173,9 @@ interface KeyguardRepository {
/** Whether quick settings or quick-quick settings is visible. */
val isQuickSettingsVisible: Flow<Boolean>
/** Represents the current state of the KeyguardRootView visibility */
val keyguardRootViewVisibility: Flow<KeyguardRootViewVisibilityState>
/** Receive an event for doze time tick */
val dozeTimeTick: Flow<Unit>
@@ -194,8 +200,14 @@ interface KeyguardRepository {
fun setAnimateDozingTransitions(animate: Boolean)
/** Sets the current amount of alpha that should be used for rendering the bottom area. */
@Deprecated("Deprecated as part of b/278057014")
fun setBottomAreaAlpha(alpha: Float)
/** Sets the current amount of alpha that should be used for rendering the keyguard. */
fun setKeyguardAlpha(alpha: Float)
fun setKeyguardVisibility(statusBarState: Int, goingToFullShade: Boolean, occlusionTransitionRunning: Boolean)
/**
* Sets the relative offset of the lock-screen clock from its natural position on the screen.
*/
@@ -244,6 +256,9 @@ constructor(
private val _bottomAreaAlpha = MutableStateFlow(1f)
override val bottomAreaAlpha = _bottomAreaAlpha.asStateFlow()
private val _keyguardAlpha = MutableStateFlow(1f)
override val keyguardAlpha = _keyguardAlpha.asStateFlow()
private val _clockPosition = MutableStateFlow(Position(0, 0))
override val clockPosition = _clockPosition.asStateFlow()
@@ -686,6 +701,17 @@ constructor(
private val _isActiveDreamLockscreenHosted = MutableStateFlow(false)
override val isActiveDreamLockscreenHosted = _isActiveDreamLockscreenHosted.asStateFlow()
private val _keyguardRootViewVisibility =
MutableStateFlow(
KeyguardRootViewVisibilityState(
com.android.systemui.statusbar.StatusBarState.SHADE,
goingToFullShade = false,
occlusionTransitionRunning = false,
)
)
override val keyguardRootViewVisibility: Flow<KeyguardRootViewVisibilityState> =
_keyguardRootViewVisibility.asStateFlow()
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.value = animate
}
@@ -694,6 +720,23 @@ constructor(
_bottomAreaAlpha.value = alpha
}
override fun setKeyguardAlpha(alpha: Float) {
_keyguardAlpha.value = alpha
}
override fun setKeyguardVisibility(
statusBarState: Int,
goingToFullShade: Boolean,
occlusionTransitionRunning: Boolean
) {
_keyguardRootViewVisibility.value =
KeyguardRootViewVisibilityState(
statusBarState,
goingToFullShade,
occlusionTransitionRunning
)
}
override fun setClockPosition(x: Int, y: Int) {
_clockPosition.value = Position(x, y)
}

View File

@@ -22,6 +22,7 @@ import android.graphics.Point
import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.Position
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
@@ -32,14 +33,17 @@ import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardRootViewVisibilityState
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -63,6 +67,18 @@ constructor(
bouncerRepository: KeyguardBouncerRepository,
configurationRepository: ConfigurationRepository,
) {
data class PreviewMode(
val isInPreviewMode: Boolean = false,
val shouldHighlightSelectedAffordance: Boolean = false,
)
/**
* Whether this view-model instance is powering the preview experience that renders exclusively
* in the wallpaper picker application. This should _always_ be `false` for the real lock screen
* experience.
*/
val previewMode = MutableStateFlow(PreviewMode())
/**
* The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
* all.
@@ -142,8 +158,6 @@ constructor(
val alternateBouncerShowing: Flow<Boolean> = bouncerRepository.alternateBouncerVisible
/** Observable for the [StatusBarState] */
val statusBarState: Flow<StatusBarState> = repository.statusBarState
/** Whether or not quick settings or quick quick settings are showing. */
val isQuickSettingsVisible: Flow<Boolean> = repository.isQuickSettingsVisible
/**
* Observable for [BiometricUnlockModel] when biometrics like face or any fingerprint (rear,
* side, under display) is used to unlock the device.
@@ -182,6 +196,18 @@ constructor(
/** Notifies when a new configuration is set */
val configurationChange: Flow<Unit> = configurationRepository.onAnyConfigurationChange
/** Represents the current state of the KeyguardRootView visibility */
val keyguardRootViewVisibilityState: Flow<KeyguardRootViewVisibilityState> =
repository.keyguardRootViewVisibility
/** The position of the keyguard clock. */
val clockPosition: Flow<Position> = repository.clockPosition
val keyguardAlpha: Flow<Float> = repository.keyguardAlpha
/** Whether to animate the next doze mode transition. */
val animateDozingTransitions: Flow<Boolean> = repository.animateBottomAreaDozingTransitions
fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> {
return dozeTransitionModel.filter { states.contains(it.to) }
}
@@ -211,6 +237,22 @@ constructor(
repository.setQuickSettingsVisible(isVisible)
}
fun setKeyguardRootVisibility(statusBarState: Int, goingToFullShade: Boolean, isOcclusionTransitionRunning: Boolean) {
repository.setKeyguardVisibility(statusBarState, goingToFullShade, isOcclusionTransitionRunning)
}
fun setClockPosition(x: Int, y: Int) {
repository.setClockPosition(x, y)
}
fun setAlpha(alpha: Float) {
repository.setKeyguardAlpha(alpha)
}
fun setAnimateDozingTransitions(animate: Boolean) {
repository.setAnimateDozingTransitions(animate)
}
companion object {
private const val TAG = "KeyguardInteractor"
}

View File

@@ -101,10 +101,9 @@ constructor(
quickAffordanceAlwaysVisible(position),
keyguardInteractor.isDozing,
keyguardInteractor.isKeyguardShowing,
keyguardInteractor.isQuickSettingsVisible,
biometricSettingsRepository.isCurrentUserInLockdown,
) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown ->
if (!isDozing && isKeyguardShowing && !isQuickSettingsVisible && !isUserInLockdown) {
) { affordance, isDozing, isKeyguardShowing, isUserInLockdown ->
if (!isDozing && isKeyguardShowing && !isUserInLockdown) {
affordance
} else {
KeyguardQuickAffordanceModel.Hidden

View File

@@ -0,0 +1,31 @@
/*
* 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.shared.model
/**
* Provides a stateful representation of the visibility of the KeyguardRootView
*
* @param statusBarState State of the status bar represented by [StatusBarState]
* @param goingToFullShade Whether status bar is going to full shade
* @param occlusionTransitionRunning Whether the occlusion transition is running in this instant
*/
data class KeyguardRootViewVisibilityState(
val statusBarState: Int,
val goingToFullShade: Boolean,
val occlusionTransitionRunning: Boolean,
)

View File

@@ -0,0 +1,127 @@
/*
* 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.binder
import android.view.View
import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.R
import com.android.systemui.keyguard.ui.viewmodel.KeyguardAmbientIndicationViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
object KeyguardAmbientIndicationAreaViewBinder {
/**
* Defines interface for an object that acts as the binding between the view and its view-model.
*
* Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after
* it is bound.
*/
interface Binding {
/**
* Returns a collection of [ViewPropertyAnimator] instances that can be used to animate the
* indication areas.
*/
fun getIndicationAreaAnimators(): List<ViewPropertyAnimator>
/** Notifies that device configuration has changed. */
fun onConfigurationChanged()
/** Destroys this binding, releases resources, and cancels any coroutines. */
fun destroy()
}
@OptIn(ExperimentalCoroutinesApi::class)
fun bind(
view: ViewGroup,
viewModel: KeyguardAmbientIndicationViewModel,
keyguardRootViewModel: KeyguardRootViewModel,
): Binding {
val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container)
val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
val disposableHandle =
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
keyguardRootViewModel.alpha.collect { alpha ->
view.importantForAccessibility =
if (alpha == 0f) {
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
} else {
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
}
ambientIndicationArea?.alpha = alpha
}
}
launch {
viewModel.indicationAreaTranslationX.collect { translationX ->
ambientIndicationArea?.translationX = translationX
}
}
launch {
configurationBasedDimensions
.map { it.defaultBurnInPreventionYOffsetPx }
.flatMapLatest { defaultBurnInOffsetY ->
viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
}
.collect { translationY ->
ambientIndicationArea?.translationY = translationY
}
}
}
}
return object : Binding {
override fun getIndicationAreaAnimators(): List<ViewPropertyAnimator> {
return listOf(ambientIndicationArea).mapNotNull { it?.animate() }
}
override fun onConfigurationChanged() {
configurationBasedDimensions.value = loadFromResources(view)
}
override fun destroy() {
disposableHandle.dispose()
}
}
}
private fun loadFromResources(view: View): ConfigurationBasedDimensions {
return ConfigurationBasedDimensions(
defaultBurnInPreventionYOffsetPx =
view.resources.getDimensionPixelOffset(R.dimen.default_burn_in_prevention_offset),
)
}
private data class ConfigurationBasedDimensions(
val defaultBurnInPreventionYOffsetPx: Int,
)
}

View File

@@ -63,6 +63,7 @@ import kotlinx.coroutines.launch
* view-model to be reused for multiple view/view-binder bindings.
*/
@OptIn(ExperimentalCoroutinesApi::class)
@Deprecated("Deprecated as part of b/278057014")
object KeyguardBottomAreaViewBinder {
private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
@@ -75,6 +76,8 @@ object KeyguardBottomAreaViewBinder {
* Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after
* it is bound.
*/
//If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
@Deprecated("Deprecated as part of b/278057014")
interface Binding {
/**
* Returns a collection of [ViewPropertyAnimator] instances that can be used to animate the
@@ -96,6 +99,7 @@ object KeyguardBottomAreaViewBinder {
}
/** Binds the view to the view-model, continuing to update the former based on the latter. */
@Deprecated("Deprecated as part of b/278057014")
@SuppressLint("ClickableViewAccessibility")
@JvmStatic
fun bind(
@@ -132,6 +136,8 @@ object KeyguardBottomAreaViewBinder {
val disposableHandle =
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
//If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
viewModel.startButton.collect { buttonModel ->
updateButton(
@@ -144,6 +150,7 @@ object KeyguardBottomAreaViewBinder {
}
}
//If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
viewModel.endButton.collect { buttonModel ->
updateButton(
@@ -180,6 +187,7 @@ object KeyguardBottomAreaViewBinder {
}
}
//If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
updateButtonAlpha(
view = startButton,
@@ -188,6 +196,7 @@ object KeyguardBottomAreaViewBinder {
)
}
//If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
updateButtonAlpha(
view = endButton,
@@ -213,6 +222,7 @@ object KeyguardBottomAreaViewBinder {
}
}
//If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
configurationBasedDimensions.collect { dimensions ->
startButton.updateLayoutParams<ViewGroup.LayoutParams> {
@@ -287,6 +297,8 @@ object KeyguardBottomAreaViewBinder {
}
}
@Deprecated("Deprecated as part of b/278057014")
// If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
@SuppressLint("ClickableViewAccessibility")
private fun updateButton(
view: ImageView,
@@ -394,6 +406,8 @@ object KeyguardBottomAreaViewBinder {
view.isSelected = viewModel.isSelected
}
@Deprecated("Deprecated as part of b/278057014")
//If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
private suspend fun updateButtonAlpha(
view: View,
viewModel: Flow<KeyguardQuickAffordanceViewModel>,
@@ -405,6 +419,7 @@ object KeyguardBottomAreaViewBinder {
.collect { view.alpha = it }
}
@Deprecated("Deprecated as part of b/278057014")
private fun View.animateVisibility(visible: Boolean) {
animate()
.withStartAction {
@@ -422,6 +437,8 @@ object KeyguardBottomAreaViewBinder {
.start()
}
@Deprecated("Deprecated as part of b/278057014")
//If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
private class OnLongClickListener(
private val falsingManager: FalsingManager?,
private val viewModel: KeyguardQuickAffordanceViewModel,
@@ -455,9 +472,10 @@ object KeyguardBottomAreaViewBinder {
}
override fun onLongClickUseDefaultHapticFeedback(view: View?) = false
}
@Deprecated("Deprecated as part of b/278057014")
//If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
private class OnClickListener(
private val viewModel: KeyguardQuickAffordanceViewModel,
private val falsingManager: FalsingManager,
@@ -479,6 +497,7 @@ object KeyguardBottomAreaViewBinder {
}
}
@Deprecated("Deprecated as part of b/278057014")
private fun loadFromResources(view: View): ConfigurationBasedDimensions {
return ConfigurationBasedDimensions(
defaultBurnInPreventionYOffsetPx =
@@ -491,6 +510,7 @@ object KeyguardBottomAreaViewBinder {
)
}
@Deprecated("Deprecated as part of b/278057014")
/** Opens the wallpaper picker screen after the device is unlocked by the user. */
private fun navigateToLockScreenSettings(
activityStarter: ActivityStarter,
@@ -510,6 +530,8 @@ object KeyguardBottomAreaViewBinder {
)
}
@Deprecated("Deprecated as part of b/278057014")
//If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
private data class ConfigurationBasedDimensions(
val defaultBurnInPreventionYOffsetPx: Int,
val buttonSizePx: Size,

View File

@@ -23,7 +23,10 @@ import android.widget.TextView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.R
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.KeyguardIndicationController
import kotlinx.coroutines.DisposableHandle
@@ -49,7 +52,9 @@ object KeyguardIndicationAreaBinder {
fun bind(
view: ViewGroup,
viewModel: KeyguardIndicationAreaViewModel,
keyguardRootViewModel: KeyguardRootViewModel,
indicationController: KeyguardIndicationController,
featureFlags: FeatureFlags,
): DisposableHandle {
val indicationArea: ViewGroup = view.requireViewById(R.id.keyguard_indication_area)
indicationController.setIndicationArea(indicationArea)
@@ -66,15 +71,28 @@ object KeyguardIndicationAreaBinder {
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.alpha.collect { alpha ->
view.importantForAccessibility =
if (alpha == 0f) {
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
} else {
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
}
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
keyguardRootViewModel.alpha.collect { alpha ->
view.importantForAccessibility =
if (alpha == 0f) {
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
} else {
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
}
indicationArea.alpha = alpha
indicationArea.alpha = alpha
}
} else {
viewModel.alpha.collect { alpha ->
view.importantForAccessibility =
if (alpha == 0f) {
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
} else {
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
}
indicationArea.alpha = alpha
}
}
}

View File

@@ -0,0 +1,315 @@
/*
* 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.binder
import android.annotation.SuppressLint
import android.graphics.drawable.Animatable2
import android.util.Size
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.animation.Interpolators
import com.android.settingslib.Utils
import com.android.systemui.R
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
/**
* This is only for a SINGLE Quick affordance
*/
object KeyguardQuickAffordanceViewBinder {
private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
private const val SCALE_SELECTED_BUTTON = 1.23f
private const val DIM_ALPHA = 0.3f
/**
* Defines interface for an object that acts as the binding between the view and its view-model.
*
* Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after
* it is bound.
*/
interface Binding {
/** Notifies that device configuration has changed. */
fun onConfigurationChanged()
/** Destroys this binding, releases resources, and cancels any coroutines. */
fun destroy()
}
fun bind(
view: LaunchableImageView,
viewModel: Flow<KeyguardQuickAffordanceViewModel>,
alpha: Flow<Float>,
falsingManager: FalsingManager?,
vibratorHelper: VibratorHelper?,
messageDisplayer: (Int) -> Unit,
): Binding {
val button = view as ImageView
val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
val disposableHandle =
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.collect { buttonModel ->
updateButton(
view = button,
viewModel = buttonModel,
falsingManager = falsingManager,
messageDisplayer = messageDisplayer,
vibratorHelper = vibratorHelper,
)
}
}
launch {
updateButtonAlpha(
view = button,
viewModel = viewModel,
alphaFlow = alpha,
)
}
launch {
configurationBasedDimensions.collect { dimensions ->
button.updateLayoutParams<ViewGroup.LayoutParams> {
width = dimensions.buttonSizePx.width
height = dimensions.buttonSizePx.height
}
}
}
}
}
return object : Binding {
override fun onConfigurationChanged() {
configurationBasedDimensions.value = loadFromResources(view)
}
override fun destroy() {
disposableHandle.dispose()
}
}
}
@SuppressLint("ClickableViewAccessibility")
private fun updateButton(
view: ImageView,
viewModel: KeyguardQuickAffordanceViewModel,
falsingManager: FalsingManager?,
messageDisplayer: (Int) -> Unit,
vibratorHelper: VibratorHelper?,
) {
if (!viewModel.isVisible) {
view.isInvisible = true
return
}
if (!view.isVisible) {
view.isVisible = true
if (viewModel.animateReveal) {
view.alpha = 0f
view.translationY = view.height / 2f
view
.animate()
.alpha(1f)
.translationY(0f)
.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
.setDuration(EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS)
.start()
}
}
IconViewBinder.bind(viewModel.icon, view)
(view.drawable as? Animatable2)?.let { animatable ->
(viewModel.icon as? Icon.Resource)?.res?.let { iconResourceId ->
// Always start the animation (we do call stop() below, if we need to skip it).
animatable.start()
if (view.tag != iconResourceId) {
// Here when we haven't run the animation on a previous update.
//
// Save the resource ID for next time, so we know not to re-animate the same
// animation again.
view.tag = iconResourceId
} else {
// Here when we've already done this animation on a previous update and want to
// skip directly to the final frame of the animation to avoid running it.
//
// By calling stop after start, we go to the final frame of the animation.
animatable.stop()
}
}
}
view.isActivated = viewModel.isActivated
view.drawable.setTint(
Utils.getColorAttrDefaultColor(
view.context,
if (viewModel.isActivated) {
com.android.internal.R.attr.materialColorOnPrimaryFixed
} else {
com.android.internal.R.attr.materialColorOnSurface
},
)
)
view.backgroundTintList =
if (!viewModel.isSelected) {
Utils.getColorAttr(
view.context,
if (viewModel.isActivated) {
com.android.internal.R.attr.materialColorPrimaryFixed
} else {
com.android.internal.R.attr.materialColorSurfaceContainerHigh
}
)
} else {
null
}
view
.animate()
.scaleX(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f)
.scaleY(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f)
.start()
view.isClickable = viewModel.isClickable
if (viewModel.isClickable) {
if (viewModel.useLongPress) {
val onTouchListener = KeyguardQuickAffordanceOnTouchListener(
view,
viewModel,
messageDisplayer,
vibratorHelper,
falsingManager,
)
view.setOnTouchListener(onTouchListener)
view.onLongClickListener =
OnLongClickListener(falsingManager, viewModel, vibratorHelper, onTouchListener)
} else {
view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
}
} else {
view.onLongClickListener = null
view.setOnClickListener(null)
view.setOnTouchListener(null)
}
view.isSelected = viewModel.isSelected
}
private suspend fun updateButtonAlpha(
view: View,
viewModel: Flow<KeyguardQuickAffordanceViewModel>,
alphaFlow: Flow<Float>,
) {
combine(viewModel.map { it.isDimmed }, alphaFlow) { isDimmed, alpha ->
if (isDimmed) DIM_ALPHA else alpha
}
.collect { view.alpha = it }
}
private fun loadFromResources(view: View): ConfigurationBasedDimensions {
return ConfigurationBasedDimensions(
buttonSizePx =
Size(
view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width),
view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height),
),
)
}
private class OnClickListener(
private val viewModel: KeyguardQuickAffordanceViewModel,
private val falsingManager: FalsingManager,
) : View.OnClickListener {
override fun onClick(view: View) {
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
return
}
if (viewModel.configKey != null) {
viewModel.onClicked(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = viewModel.configKey,
expandable = Expandable.fromView(view),
slotId = viewModel.slotId,
)
)
}
}
}
private class OnLongClickListener(
private val falsingManager: FalsingManager?,
private val viewModel: KeyguardQuickAffordanceViewModel,
private val vibratorHelper: VibratorHelper?,
private val onTouchListener: KeyguardQuickAffordanceOnTouchListener
) : View.OnLongClickListener {
override fun onLongClick(view: View): Boolean {
if (falsingManager?.isFalseLongTap(FalsingManager.MODERATE_PENALTY) == true) {
return true
}
if (viewModel.configKey != null) {
viewModel.onClicked(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = viewModel.configKey,
expandable = Expandable.fromView(view),
slotId = viewModel.slotId,
)
)
vibratorHelper?.vibrate(
if (viewModel.isActivated) {
KeyguardBottomAreaVibrations.Activated
} else {
KeyguardBottomAreaVibrations.Deactivated
}
)
}
onTouchListener.cancel()
return true
}
override fun onLongClickUseDefaultHapticFeedback(view: View?) = false
}
private data class ConfigurationBasedDimensions(
val buttonSizePx: Size,
)
}

View File

@@ -17,20 +17,26 @@
package com.android.systemui.keyguard.ui.binder
import android.annotation.DrawableRes
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.animation.Interpolators
import com.android.systemui.R
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.Text
import com.android.systemui.common.shared.model.TintedIcon
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.temporarydisplay.ViewPriority
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
@@ -40,31 +46,74 @@ object KeyguardRootViewBinder {
@JvmStatic
fun bind(
view: ViewGroup,
viewModel: KeyguardRootViewModel,
featureFlags: FeatureFlags,
occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
chipbarCoordinator: ChipbarCoordinator,
) {
if (featureFlags.isEnabled(Flags.FP_LISTEN_OCCLUDING_APPS)) {
keyguardStateController: KeyguardStateController,
): DisposableHandle {
val disposableHandle =
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
occludingAppDeviceEntryMessageViewModel.message.collect { biometricMessage
->
if (biometricMessage?.message != null) {
chipbarCoordinator.displayView(
createChipbarInfo(
biometricMessage.message,
R.drawable.ic_lock,
if (featureFlags.isEnabled(Flags.FP_LISTEN_OCCLUDING_APPS)) {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
occludingAppDeviceEntryMessageViewModel.message.collect {
biometricMessage ->
if (biometricMessage?.message != null) {
chipbarCoordinator.displayView(
createChipbarInfo(
biometricMessage.message,
R.drawable.ic_lock,
)
)
)
} else {
chipbarCoordinator.removeView(ID, "occludingAppMsgNull")
} else {
chipbarCoordinator.removeView(ID, "occludingAppMsgNull")
}
}
}
}
}
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.keyguardRootViewVisibilityState.collect { visibilityState ->
view.animate().cancel()
val goingToFullShade = visibilityState.goingToFullShade
val statusBarState = visibilityState.statusBarState
val isOcclusionTransitionRunning =
visibilityState.occlusionTransitionRunning
if (goingToFullShade) {
view.animate().alpha(0f).setStartDelay(
keyguardStateController.keyguardFadingAwayDelay
).setDuration(
keyguardStateController.shortenedFadingAwayDuration
).setInterpolator(
Interpolators.ALPHA_OUT
).withEndAction { view.visibility = View.GONE }.start()
} else if (
statusBarState == StatusBarState.KEYGUARD ||
statusBarState == StatusBarState.SHADE_LOCKED
) {
view.visibility = View.VISIBLE
if (!isOcclusionTransitionRunning) {
view.alpha = 1f
}
} else {
view.visibility = View.GONE
}
}
}
launch {
viewModel.alpha.collect { alpha ->
view.alpha = alpha
}
}
}
}
}
}
return disposableHandle
}
/**
@@ -73,10 +122,10 @@ object KeyguardRootViewBinder {
private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo {
return ChipbarInfo(
startIcon =
TintedIcon(
Icon.Resource(icon, null),
ChipbarInfo.DEFAULT_ICON_TINT,
),
TintedIcon(
Icon.Resource(icon, null),
ChipbarInfo.DEFAULT_ICON_TINT,
),
text = Text.Loaded(message),
endItem = null,
vibrationEffect = null,

View File

@@ -0,0 +1,131 @@
/*
* 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.binder
import android.content.Intent
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.R
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.view.LaunchableLinearLayout
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.common.ui.binder.TextViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.VibratorHelper
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
object KeyguardSettingsViewBinder {
fun bind(
parentView: View,
viewModel: KeyguardSettingsMenuViewModel,
vibratorHelper: VibratorHelper,
activityStarter: ActivityStarter
): DisposableHandle {
val view = parentView.findViewById<LaunchableLinearLayout>(R.id.keyguard_settings_button)
val disposableHandle =
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.isVisible.distinctUntilChanged().collect { isVisible ->
view.animateVisibility(visible = isVisible)
if (isVisible) {
vibratorHelper.vibrate(KeyguardBottomAreaVibrations.Activated)
view.setOnTouchListener(
KeyguardSettingsButtonOnTouchListener(
view = view,
viewModel = viewModel,
)
)
IconViewBinder.bind(
icon = viewModel.icon,
view = view.requireViewById(R.id.icon),
)
TextViewBinder.bind(
view = view.requireViewById(R.id.text),
viewModel = viewModel.text,
)
}
}
}
// activityStarter will only be null when rendering the preview that
// shows up in the Wallpaper Picker app. If we do that, then the
// settings menu should never be visible.
if (activityStarter != null) {
launch {
viewModel.shouldOpenSettings
.filter { it }
.collect {
navigateToLockScreenSettings(
activityStarter = activityStarter,
view = view,
)
viewModel.onSettingsShown()
}
}
}
}
}
return disposableHandle
}
/** Opens the wallpaper picker screen after the device is unlocked by the user. */
private fun navigateToLockScreenSettings(
activityStarter: ActivityStarter,
view: View,
) {
activityStarter.postStartActivityDismissingKeyguard(
Intent(Intent.ACTION_SET_WALLPAPER).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
view.context
.getString(R.string.config_wallpaperPickerPackage)
.takeIf { it.isNotEmpty() }
?.let { packageName -> setPackage(packageName) }
},
/* delay= */ 0,
/* animationController= */ ActivityLaunchAnimator.Controller.fromView(view),
/* customMessage= */ view.context.getString(R.string.keyguard_unlock_to_customize_ls)
)
}
private fun View.animateVisibility(visible: Boolean) {
animate()
.withStartAction {
if (visible) {
alpha = 0f
isVisible = true
}
}
.alpha(if (visible) 1f else 0f)
.withEndAction {
if (!visible) {
isVisible = false
}
}
.start()
}
}

View File

@@ -42,17 +42,27 @@ import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
import com.android.systemui.keyguard.ui.view.KeyguardRootView
import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManager
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewSmartspaceViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.monet.ColorScheme
import com.android.systemui.plugins.ClockController
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.shared.clocks.ClockRegistry
import com.android.systemui.shared.clocks.DefaultClockController
import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants
import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants
import com.android.systemui.statusbar.KeyguardIndicationController
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
import dagger.assisted.Assisted
@@ -71,6 +81,7 @@ constructor(
private val clockViewModel: KeyguardPreviewClockViewModel,
private val smartspaceViewModel: KeyguardPreviewSmartspaceViewModel,
private val bottomAreaViewModel: KeyguardBottomAreaViewModel,
private val quickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel,
displayManager: DisplayManager,
private val windowManager: WindowManager,
private val clockController: ClockEventController,
@@ -78,6 +89,12 @@ constructor(
private val broadcastDispatcher: BroadcastDispatcher,
private val lockscreenSmartspaceController: LockscreenSmartspaceController,
private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
private val featureFlags: FeatureFlags,
private val keyguardLayoutManager: KeyguardLayoutManager,
private val falsingManager: FalsingManager,
private val vibratorHelper: VibratorHelper,
private val indicationController: KeyguardIndicationController,
private val keyguardRootViewModel: KeyguardRootViewModel,
@Assisted bundle: Bundle,
) {
@@ -106,14 +123,26 @@ constructor(
private val disposables = mutableSetOf<DisposableHandle>()
private var isDestroyed = false
private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>()
init {
bottomAreaViewModel.enablePreviewMode(
initiallySelectedSlotId =
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
keyguardRootViewModel.enablePreviewMode(
initiallySelectedSlotId =
bundle.getString(
KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
),
shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
)
shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
)
} else {
bottomAreaViewModel.enablePreviewMode(
initiallySelectedSlotId =
bundle.getString(
KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
),
shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
)
}
runBlocking(mainDispatcher) {
host =
SurfaceControlViewHost(
@@ -130,7 +159,20 @@ constructor(
mainHandler.post {
val rootView = FrameLayout(context)
setUpBottomArea(rootView)
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
val keyguardRootView = KeyguardRootView(context, null)
rootView.addView(
keyguardRootView,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT,
),
)
setupShortcuts(keyguardRootView)
keyguardLayoutManager.layoutViews(keyguardRootView)
} else {
setUpBottomArea(rootView)
}
setUpSmartspace(rootView)
smartSpaceView?.let {
@@ -182,13 +224,20 @@ constructor(
}
fun onSlotSelected(slotId: String) {
bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId)
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
quickAffordancesCombinedViewModel.onPreviewSlotSelected(slotId = slotId)
} else {
bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId)
}
}
fun destroy() {
isDestroyed = true
lockscreenSmartspaceController.disconnect()
disposables.forEach { it.dispose() }
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
shortcutsBindings.forEach { it.destroy() }
}
}
/**
@@ -250,6 +299,7 @@ constructor(
smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f
}
@Deprecated("Deprecated as part of b/278057014")
private fun setUpBottomArea(parentView: ViewGroup) {
val bottomAreaView =
LayoutInflater.from(context)
@@ -259,7 +309,7 @@ constructor(
false,
) as KeyguardBottomAreaView
bottomAreaView.init(
viewModel = bottomAreaViewModel,
viewModel = bottomAreaViewModel,
)
parentView.addView(
bottomAreaView,
@@ -270,6 +320,32 @@ constructor(
)
}
private fun setupShortcuts(keyguardRootView: KeyguardRootView) {
shortcutsBindings.add(
KeyguardQuickAffordanceViewBinder.bind(
keyguardRootView.requireViewById(R.id.start_button),
quickAffordancesCombinedViewModel.startButton,
keyguardRootViewModel.alpha,
falsingManager,
vibratorHelper,
) {
indicationController.showTransientIndication(it)
}
)
shortcutsBindings.add(
KeyguardQuickAffordanceViewBinder.bind(
keyguardRootView.requireViewById(R.id.end_button),
quickAffordancesCombinedViewModel.endButton,
keyguardRootViewModel.alpha,
falsingManager,
vibratorHelper,
) {
indicationController.showTransientIndication(it)
}
)
}
private fun setUpUdfps(parentView: ViewGroup) {
val sensorBounds = udfpsOverlayInteractor.udfpsOverlayParams.value.sensorBounds

View File

@@ -20,8 +20,13 @@ package com.android.systemui.keyguard.ui.view
import android.content.Context
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import androidx.core.content.res.ResourcesCompat
import com.android.keyguard.LockIconView
import com.android.systemui.R
import com.android.systemui.animation.view.LaunchableImageView
/** Provides a container for all keyguard ui content. */
class KeyguardRootView(
@@ -36,6 +41,10 @@ class KeyguardRootView(
init {
addIndicationTextArea()
addLockIconView()
addAmbientIndicationArea()
addLeftShortcut()
addRightShortcut()
addSettingsPopupMenu()
}
private fun addIndicationTextArea() {
@@ -47,4 +56,65 @@ class KeyguardRootView(
val view = LockIconView(context, attrs).apply { id = R.id.lock_icon_view }
addView(view)
}
private fun addAmbientIndicationArea() {
LayoutInflater.from(context).inflate(R.layout.ambient_indication, this)
}
private fun addLeftShortcut() {
val view = LaunchableImageView(context, attrs)
.apply {
id = R.id.start_button
scaleType = ImageView.ScaleType.FIT_CENTER
background =
ResourcesCompat.getDrawable(
context.resources,
R.drawable.keyguard_bottom_affordance_bg,
context.theme
)
foreground =
ResourcesCompat.getDrawable(
context.resources,
R.drawable.keyguard_bottom_affordance_selected_border,
context.theme
)
visibility = View.INVISIBLE
}
addView(view)
}
private fun addRightShortcut() {
val view = LaunchableImageView(context, attrs)
.apply {
id = R.id.end_button
scaleType = ImageView.ScaleType.FIT_CENTER
background =
ResourcesCompat.getDrawable(
context.resources,
R.drawable.keyguard_bottom_affordance_bg,
context.theme
)
foreground =
ResourcesCompat.getDrawable(
context.resources,
R.drawable.keyguard_bottom_affordance_selected_border,
context.theme
)
visibility = View.INVISIBLE
}
addView(view)
}
private fun addSettingsPopupMenu() {
val view = LayoutInflater.from(context).inflate(
R.layout.keyguard_settings_popup_menu,
this,
false
)
.apply {
id = R.id.keyguard_settings_button
visibility = GONE
}
addView(view)
}
}

View File

@@ -24,17 +24,27 @@ import android.util.DisplayMetrics
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
import androidx.constraintlayout.widget.ConstraintSet.END
import androidx.constraintlayout.widget.ConstraintSet.LEFT
import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.RIGHT
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R
import com.android.systemui.animation.view.LaunchableLinearLayout
import com.android.systemui.biometrics.AuthController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.ui.view.KeyguardRootView
@@ -136,15 +146,139 @@ constructor(
}
}
override fun layoutShortcuts(rootView: KeyguardRootView) {
val leftShortcut = rootView.findViewById<View>(R.id.start_button) ?: return
val rightShortcut = rootView.findViewById<View>(R.id.end_button) ?: return
val width =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width)
val height =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height)
val horizontalOffsetMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_horizontal_offset)
val verticalOffsetMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_vertical_offset)
val padding =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_padding)
leftShortcut.apply {
updateLayoutParams<MarginLayoutParams> {
marginStart = horizontalOffsetMargin
bottomMargin = verticalOffsetMargin
}
setPadding(padding)
}
rightShortcut.apply {
updateLayoutParams<MarginLayoutParams> {
marginEnd = horizontalOffsetMargin
bottomMargin = verticalOffsetMargin
}
setPadding(padding)
}
rootView.getConstraintSet().apply {
constrainWidth(leftShortcut.id, width)
constrainHeight(leftShortcut.id, height)
connect(leftShortcut.id, LEFT, PARENT_ID, LEFT)
connect(leftShortcut.id, BOTTOM, PARENT_ID, BOTTOM)
constrainWidth(rightShortcut.id, width)
constrainHeight(rightShortcut.id, height)
connect(rightShortcut.id, RIGHT, PARENT_ID, RIGHT)
connect(rightShortcut.id, BOTTOM, PARENT_ID, BOTTOM)
applyTo(rootView)
}
}
override fun layoutAmbientIndicationArea(rootView: KeyguardRootView) {
val ambientIndicationContainer =
rootView.findViewById<View>(R.id.ambient_indication_container) ?: return
val lockIconView = rootView.findViewById<View>(R.id.lock_icon_view) ?: return
val indicationArea = rootView.findViewById<View>(R.id.keyguard_indication_area) ?: return
rootView.getConstraintSet().apply {
constrainWidth(ambientIndicationContainer.id, MATCH_PARENT)
if (keyguardUpdateMonitor.isUdfpsSupported) {
//constrain below udfps and above indication area
constrainHeight(ambientIndicationContainer.id, MATCH_CONSTRAINT)
connect(ambientIndicationContainer.id, TOP, lockIconView.id, BOTTOM)
connect(ambientIndicationContainer.id, BOTTOM, indicationArea.id, TOP)
connect(ambientIndicationContainer.id, LEFT, PARENT_ID, LEFT)
connect(ambientIndicationContainer.id, RIGHT, PARENT_ID, RIGHT)
} else {
//constrain above lock icon
constrainHeight(ambientIndicationContainer.id, WRAP_CONTENT)
connect(ambientIndicationContainer.id, BOTTOM, lockIconView.id, TOP)
connect(ambientIndicationContainer.id, LEFT, PARENT_ID, LEFT)
connect(ambientIndicationContainer.id, RIGHT, PARENT_ID, RIGHT)
}
applyTo(rootView)
}
}
override fun layoutSettingsPopupMenu(rootView: KeyguardRootView) {
val popupMenu =
rootView.findViewById<LaunchableLinearLayout>(R.id.keyguard_settings_button) ?: return
val icon = popupMenu.findViewById<ImageView>(R.id.icon) ?: return
val textView = popupMenu.findViewById<TextView>(R.id.text) ?: return
val horizontalOffsetMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_horizontal_offset)
icon.updateLayoutParams<LinearLayout.LayoutParams> {
height =
context
.resources
.getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_height)
width =
context
.resources
.getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_width)
marginEnd =
context
.resources
.getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_end_margin)
}
textView.updateLayoutParams<LinearLayout.LayoutParams> {
height = WRAP_CONTENT
width = WRAP_CONTENT
}
popupMenu.updateLayoutParams<MarginLayoutParams> {
bottomMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_vertical_offset)
marginStart = horizontalOffsetMargin
marginEnd = horizontalOffsetMargin
}
popupMenu.setPadding(
context.resources.getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_padding)
)
rootView.getConstraintSet().apply {
constrainWidth(popupMenu.id, WRAP_CONTENT)
constrainHeight(popupMenu.id, WRAP_CONTENT)
constrainMinHeight(
popupMenu.id,
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height)
)
connect(popupMenu.id, LEFT, PARENT_ID, LEFT)
connect(popupMenu.id, RIGHT, PARENT_ID, RIGHT)
connect(popupMenu.id, BOTTOM, PARENT_ID, BOTTOM)
applyTo(rootView)
}
}
private fun Int.dp(): Int {
return context.resources.getDimensionPixelSize(this)
}
private fun ConstraintLayout.getConstraintSet(): ConstraintSet {
val cs = ConstraintSet()
cs.clone(this)
return cs
}
private fun ConstraintLayout.getConstraintSet(): ConstraintSet =
ConstraintSet().also {
it.clone(this)
}
companion object {
const val DEFAULT = "default"

View File

@@ -63,8 +63,8 @@ constructor(
return true
}
fun layoutViews() {
layout?.layoutViews(keyguardRootView)
fun layoutViews(rootView: KeyguardRootView = keyguardRootView) {
layout?.layoutViews(rootView)
}
companion object {
@@ -85,7 +85,13 @@ interface LockscreenLayout {
.applyTo(rootView)
layoutIndicationArea(rootView)
layoutLockIcon(rootView)
layoutShortcuts(rootView)
layoutAmbientIndicationArea(rootView)
layoutSettingsPopupMenu(rootView)
}
fun layoutIndicationArea(rootView: KeyguardRootView)
fun layoutLockIcon(rootView: KeyguardRootView)
fun layoutShortcuts(rootView: KeyguardRootView)
fun layoutAmbientIndicationArea(rootView: KeyguardRootView)
fun layoutSettingsPopupMenu(rootView: KeyguardRootView)
}

View File

@@ -28,4 +28,10 @@ abstract class LockscreenLayoutModule {
abstract fun bindDefaultLayout(
defaultLockscreenLayout: DefaultLockscreenLayout
): LockscreenLayout
@Binds
@IntoSet
abstract fun bindShortcutsBesideUdfpsLockscreenLayout(
shortcutsBesideUdfpsLockscreenLayout: ShortcutsBesideUdfpsLockscreenLayout
): LockscreenLayout
}

View File

@@ -0,0 +1,305 @@
/*
* 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.view.layout
import android.content.Context
import android.graphics.Point
import android.graphics.Rect
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
import androidx.constraintlayout.widget.ConstraintSet.END
import androidx.constraintlayout.widget.ConstraintSet.LEFT
import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.RIGHT
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R
import com.android.systemui.animation.view.LaunchableLinearLayout
import com.android.systemui.biometrics.AuthController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.ui.view.KeyguardRootView
import javax.inject.Inject
/**
* Positions elements of the lockscreen to the default position.
*
* This will be the most common use case for phones in portrait mode.
*/
@SysUISingleton
class ShortcutsBesideUdfpsLockscreenLayout
@Inject
constructor(
private val authController: AuthController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val windowManager: WindowManager,
private val context: Context,
) : LockscreenLayout {
override val id: String = SHORTCUTS_BESIDE_UDFPS
override fun layoutIndicationArea(rootView: KeyguardRootView) {
val indicationArea = rootView.findViewById<View>(R.id.keyguard_indication_area) ?: return
rootView.getConstraintSet().apply {
constrainWidth(indicationArea.id, MATCH_PARENT)
constrainHeight(indicationArea.id, WRAP_CONTENT)
connect(
indicationArea.id,
BOTTOM,
PARENT_ID,
BOTTOM,
R.dimen.keyguard_indication_margin_bottom.dp()
)
connect(indicationArea.id, START, PARENT_ID, START)
connect(indicationArea.id, END, PARENT_ID, END)
applyTo(rootView)
}
}
override fun layoutLockIcon(rootView: KeyguardRootView) {
val isUdfpsSupported = keyguardUpdateMonitor.isUdfpsSupported
val scaleFactor: Float = authController.scaleFactor
val mBottomPaddingPx = R.dimen.lock_icon_margin_bottom.dp()
val mDefaultPaddingPx = R.dimen.lock_icon_padding.dp()
val scaledPadding: Int = (mDefaultPaddingPx * scaleFactor).toInt()
val bounds = windowManager.currentWindowMetrics.bounds
val widthPixels = bounds.right.toFloat()
val heightPixels = bounds.bottom.toFloat()
val defaultDensity =
DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
DisplayMetrics.DENSITY_DEFAULT.toFloat()
val lockIconRadiusPx = (defaultDensity * 36).toInt()
if (isUdfpsSupported) {
authController.udfpsLocation?.let { udfpsLocation ->
centerLockIcon(udfpsLocation, authController.udfpsRadius, scaledPadding, rootView)
}
} else {
centerLockIcon(
Point(
(widthPixels / 2).toInt(),
(heightPixels - ((mBottomPaddingPx + lockIconRadiusPx) * scaleFactor)).toInt()
),
lockIconRadiusPx * scaleFactor,
scaledPadding,
rootView
)
}
}
@VisibleForTesting
internal fun centerLockIcon(
center: Point,
radius: Float,
drawablePadding: Int,
rootView: KeyguardRootView,
) {
val lockIconView = rootView.findViewById<View>(R.id.lock_icon_view) ?: return
val lockIcon = lockIconView.findViewById<View>(R.id.lock_icon) ?: return
lockIcon.setPadding(drawablePadding, drawablePadding, drawablePadding, drawablePadding)
val sensorRect =
Rect().apply {
set(
center.x - radius.toInt(),
center.y - radius.toInt(),
center.x + radius.toInt(),
center.y + radius.toInt(),
)
}
rootView.getConstraintSet().apply {
constrainWidth(lockIconView.id, sensorRect.right - sensorRect.left)
constrainHeight(lockIconView.id, sensorRect.bottom - sensorRect.top)
connect(lockIconView.id, TOP, PARENT_ID, TOP, sensorRect.top)
connect(lockIconView.id, START, PARENT_ID, START, sensorRect.left)
applyTo(rootView)
}
}
override fun layoutShortcuts(rootView: KeyguardRootView) {
val leftShortcut = rootView.findViewById<View>(R.id.start_button) ?: return
val rightShortcut = rootView.findViewById<View>(R.id.end_button) ?: return
val lockIconView = rootView.findViewById<View>(R.id.lock_icon_view) ?: return
val udfpsSupported = keyguardUpdateMonitor.isUdfpsSupported
val width =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width)
val height =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height)
val horizontalOffsetMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_horizontal_offset)
val verticalOffsetMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_vertical_offset)
val padding =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_padding)
if (!udfpsSupported) {
leftShortcut.apply {
updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginStart = horizontalOffsetMargin
bottomMargin = verticalOffsetMargin
}
setPadding(padding)
}
rightShortcut.apply {
updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginEnd = horizontalOffsetMargin
bottomMargin = verticalOffsetMargin
}
setPadding(padding)
}
}
rootView.getConstraintSet().apply {
if (udfpsSupported) {
constrainWidth(leftShortcut.id, width)
constrainHeight(leftShortcut.id, height)
connect(leftShortcut.id, LEFT, PARENT_ID, LEFT)
connect(leftShortcut.id, RIGHT, lockIconView.id, LEFT)
connect(leftShortcut.id, TOP, lockIconView.id, TOP)
connect(leftShortcut.id, BOTTOM, lockIconView.id, BOTTOM)
constrainWidth(rightShortcut.id, width)
constrainHeight(rightShortcut.id, height)
connect(rightShortcut.id, RIGHT, PARENT_ID, RIGHT)
connect(rightShortcut.id, LEFT, lockIconView.id, RIGHT)
connect(rightShortcut.id, TOP, lockIconView.id, TOP)
connect(rightShortcut.id, BOTTOM, lockIconView.id, BOTTOM)
} else {
constrainWidth(leftShortcut.id, width)
constrainHeight(leftShortcut.id, height)
connect(leftShortcut.id, LEFT, PARENT_ID, LEFT)
connect(leftShortcut.id, BOTTOM, PARENT_ID, BOTTOM)
constrainWidth(rightShortcut.id, width)
constrainHeight(rightShortcut.id, height)
connect(rightShortcut.id, RIGHT, PARENT_ID, RIGHT)
connect(rightShortcut.id, BOTTOM, PARENT_ID, BOTTOM)
}
applyTo(rootView)
}
}
override fun layoutAmbientIndicationArea(rootView: KeyguardRootView) {
val ambientIndicationContainer =
rootView.findViewById<View>(R.id.ambient_indication_container) ?: return
val lockIconView = rootView.findViewById<View>(R.id.lock_icon_view) ?: return
val indicationArea = rootView.findViewById<View>(R.id.keyguard_indication_area) ?: return
rootView.getConstraintSet().apply {
constrainWidth(ambientIndicationContainer.id, MATCH_PARENT)
if (keyguardUpdateMonitor.isUdfpsSupported) {
//constrain below udfps and above indication area
constrainHeight(ambientIndicationContainer.id, MATCH_CONSTRAINT)
connect(ambientIndicationContainer.id, TOP, lockIconView.id, BOTTOM)
connect(ambientIndicationContainer.id, BOTTOM, indicationArea.id, TOP)
connect(ambientIndicationContainer.id, LEFT, PARENT_ID, LEFT)
connect(ambientIndicationContainer.id, RIGHT, PARENT_ID, RIGHT)
} else {
//constrain above lock icon
constrainHeight(ambientIndicationContainer.id, WRAP_CONTENT)
connect(ambientIndicationContainer.id, BOTTOM, lockIconView.id, TOP)
connect(ambientIndicationContainer.id, LEFT, PARENT_ID, LEFT)
connect(ambientIndicationContainer.id, RIGHT, PARENT_ID, RIGHT)
}
applyTo(rootView)
}
}
override fun layoutSettingsPopupMenu(rootView: KeyguardRootView) {
val popupMenu =
rootView.findViewById<LaunchableLinearLayout>(R.id.keyguard_settings_button) ?: return
val icon = popupMenu.findViewById<ImageView>(R.id.icon) ?: return
val textView = popupMenu.findViewById<TextView>(R.id.text) ?: return
val horizontalOffsetMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_horizontal_offset)
icon.updateLayoutParams<LinearLayout.LayoutParams> {
height =
context
.resources
.getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_height)
width =
context
.resources
.getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_width)
marginEnd =
context
.resources
.getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_end_margin)
}
textView.updateLayoutParams<LinearLayout.LayoutParams> {
height = WRAP_CONTENT
width = WRAP_CONTENT
}
popupMenu.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_vertical_offset)
marginStart = horizontalOffsetMargin
marginEnd = horizontalOffsetMargin
}
popupMenu.setPadding(
context.resources.getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_padding)
)
rootView.getConstraintSet().apply {
constrainWidth(popupMenu.id, WRAP_CONTENT)
constrainHeight(popupMenu.id, WRAP_CONTENT)
constrainMinHeight(
popupMenu.id,
context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height)
)
connect(popupMenu.id, LEFT, PARENT_ID, LEFT)
connect(popupMenu.id, RIGHT, PARENT_ID, RIGHT)
connect(popupMenu.id, BOTTOM, PARENT_ID, BOTTOM)
applyTo(rootView)
}
}
private fun Int.dp(): Int {
return context.resources.getDimensionPixelSize(this)
}
private fun ConstraintLayout.getConstraintSet(): ConstraintSet =
ConstraintSet().also {
it.clone(this)
}
companion object {
const val SHORTCUTS_BESIDE_UDFPS = "shortcutsBesideUdfps"
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.doze.util.BurnInHelperWrapper
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import javax.inject.Inject
class KeyguardAmbientIndicationViewModel
@Inject
constructor(
private val keyguardInteractor: KeyguardInteractor,
private val burnInHelperWrapper: BurnInHelperWrapper,
) {
/** An observable for the x-offset by which the indication area should be translated. */
val indicationAreaTranslationX: Flow<Float> =
keyguardInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
/** Returns an observable for the y-offset by which the indication area should be translated. */
fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> {
return keyguardInteractor.dozeAmount
.map { dozeAmount ->
dozeAmount *
(burnInHelperWrapper.burnInOffset(
/* amplitude = */ defaultBurnInOffset * 2,
/* xAxis= */ false,
) - defaultBurnInOffset)
}
.distinctUntilChanged()
}
}

View File

@@ -17,6 +17,8 @@
package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import javax.inject.Inject
@@ -33,6 +35,8 @@ constructor(
bottomAreaInteractor: KeyguardBottomAreaInteractor,
keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
private val burnInHelperWrapper: BurnInHelperWrapper,
private val shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel,
private val featureFlags: FeatureFlags,
) {
/** Notifies when a new configuration is set */
@@ -43,15 +47,28 @@ constructor(
/** An observable for whether the indication area should be padded. */
val isIndicationAreaPadded: Flow<Boolean> =
combine(keyguardBottomAreaViewModel.startButton, keyguardBottomAreaViewModel.endButton) {
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
combine(shortcutsCombinedViewModel.startButton, shortcutsCombinedViewModel.endButton) {
startButtonModel,
endButtonModel ->
startButtonModel.isVisible || endButtonModel.isVisible
}
.distinctUntilChanged()
.distinctUntilChanged()
} else {
combine(keyguardBottomAreaViewModel.startButton, keyguardBottomAreaViewModel.endButton) {
startButtonModel,
endButtonModel ->
startButtonModel.isVisible || endButtonModel.isVisible
}
.distinctUntilChanged()
}
/** An observable for the x-offset by which the indication area should be translated. */
val indicationAreaTranslationX: Flow<Float> =
bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
keyguardInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
} else {
bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
}
/** Returns an observable for the y-offset by which the indication area should be translated. */
fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> {

View File

@@ -0,0 +1,174 @@
/*
* 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.annotation.VisibleForTesting
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
class KeyguardQuickAffordancesCombinedViewModel
@Inject
constructor(
private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor,
private val keyguardInteractor: KeyguardInteractor,
) {
/**
* ID of the slot that's currently selected in the preview that renders exclusively in the
* wallpaper picker application. This is ignored for the actual, real lock screen experience.
*/
private val selectedPreviewSlotId =
MutableStateFlow(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START)
/**
* Whether quick affordances are "opaque enough" to be considered visible to and interactive by
* the user. If they are not interactive, user input should not be allowed on them.
*
* Note that there is a margin of error, where we allow very, very slightly transparent views to
* be considered "fully opaque" for the purpose of being interactive. This is to accommodate the
* error margin of floating point arithmetic.
*
* A view that is visible but with an alpha of less than our threshold either means it's not
* fully done fading in or is fading/faded out. Either way, it should not be
* interactive/clickable unless "fully opaque" to avoid issues like in b/241830987.
*/
private val areQuickAffordancesFullyOpaque: Flow<Boolean> =
keyguardInteractor.keyguardAlpha
.map { alpha -> alpha >= AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD }
.distinctUntilChanged()
/** An observable for the view-model of the "start button" quick affordance. */
val startButton: Flow<KeyguardQuickAffordanceViewModel> =
button(KeyguardQuickAffordancePosition.BOTTOM_START)
/** An observable for the view-model of the "end button" quick affordance. */
val endButton: Flow<KeyguardQuickAffordanceViewModel> =
button(KeyguardQuickAffordancePosition.BOTTOM_END)
/**
* Notifies that a slot with the given ID has been selected in the preview experience that is
* rendering in the wallpaper picker. This is ignored for the real lock screen experience.
*
* @see [KeyguardRootViewModel.enablePreviewMode]
*/
fun onPreviewSlotSelected(slotId: String) {
selectedPreviewSlotId.value = slotId
}
private fun button(
position: KeyguardQuickAffordancePosition
): Flow<KeyguardQuickAffordanceViewModel> {
return keyguardInteractor.previewMode.flatMapLatest { previewMode ->
combine(
if (previewMode.isInPreviewMode) {
quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position)
} else {
quickAffordanceInteractor.quickAffordance(position = position)
},
keyguardInteractor.animateDozingTransitions.distinctUntilChanged(),
areQuickAffordancesFullyOpaque,
selectedPreviewSlotId,
quickAffordanceInteractor.useLongPress(),
) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress ->
val slotId = position.toSlotId()
val isSelected = selectedPreviewSlotId == slotId
model.toViewModel(
animateReveal = !previewMode.isInPreviewMode && animateReveal,
isClickable = isFullyOpaque && !previewMode.isInPreviewMode,
isSelected =
previewMode.isInPreviewMode &&
previewMode.shouldHighlightSelectedAffordance &&
isSelected,
isDimmed =
previewMode.isInPreviewMode &&
previewMode.shouldHighlightSelectedAffordance &&
!isSelected,
forceInactive = previewMode.isInPreviewMode,
slotId = slotId,
useLongPress = useLongPress,
)
}
.distinctUntilChanged()
}
}
private fun KeyguardQuickAffordanceModel.toViewModel(
animateReveal: Boolean,
isClickable: Boolean,
isSelected: Boolean,
isDimmed: Boolean,
forceInactive: Boolean,
slotId: String,
useLongPress: Boolean,
): KeyguardQuickAffordanceViewModel {
return when (this) {
is KeyguardQuickAffordanceModel.Visible ->
KeyguardQuickAffordanceViewModel(
configKey = configKey,
isVisible = true,
animateReveal = animateReveal,
icon = icon,
onClicked = { parameters ->
quickAffordanceInteractor.onQuickAffordanceTriggered(
configKey = parameters.configKey,
expandable = parameters.expandable,
slotId = parameters.slotId,
)
},
isClickable = isClickable,
isActivated = !forceInactive && activationState is ActivationState.Active,
isSelected = isSelected,
useLongPress = useLongPress,
isDimmed = isDimmed,
slotId = slotId,
)
is KeyguardQuickAffordanceModel.Hidden ->
KeyguardQuickAffordanceViewModel(
slotId = slotId,
)
}
}
companion object {
// We select a value that's less than 1.0 because we want floating point math precision to
// not be a factor in determining whether the affordance UI is fully opaque. The number we
// choose needs to be close enough 1.0 such that the user can't easily tell the difference
// between the UI with an alpha at the threshold and when the alpha is 1.0. At the same
// time, we don't want the number to be too close to 1.0 such that there is a chance that we
// never treat the affordance UI as "fully opaque" as that would risk making it forever not
// clickable.
@VisibleForTesting
const val AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD = 0.95f
}
}

View File

@@ -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.keyguard.ui.viewmodel
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.shared.model.KeyguardRootViewVisibilityState
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
class KeyguardRootViewModel
@Inject
constructor(
private val keyguardInteractor: KeyguardInteractor,
private val keyguardQuickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel
)
{
/** Represents the current state of the KeyguardRootView visibility */
val keyguardRootViewVisibilityState: Flow<KeyguardRootViewVisibilityState> =
keyguardInteractor.keyguardRootViewVisibilityState
/** An observable for the alpha level for the entire keyguard root view. */
val alpha: Flow<Float> =
keyguardInteractor.previewMode.flatMapLatest {
if (it.isInPreviewMode) {
flowOf(1f)
} else {
keyguardInteractor.keyguardAlpha.distinctUntilChanged()
}
}
/**
* Puts this view-model in "preview mode", which means it's being used for UI that is rendering
* the lock screen preview in wallpaper picker / settings and not the real experience on the
* lock screen.
*
* @param initiallySelectedSlotId The ID of the initial slot to render as the selected one.
* @param shouldHighlightSelectedAffordance Whether the selected quick affordance should be
* highlighted (while all others are dimmed to make the selected one stand out).
*/
fun enablePreviewMode(
initiallySelectedSlotId: String?,
shouldHighlightSelectedAffordance: Boolean,
) {
keyguardInteractor.previewMode.value =
KeyguardInteractor.PreviewMode(
isInPreviewMode = true,
shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
)
keyguardQuickAffordancesCombinedViewModel.onPreviewSlotSelected(
initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
)
}
}

View File

@@ -138,6 +138,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.shared.model.WakefulnessModel;
import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder;
import com.android.systemui.keyguard.ui.view.KeyguardRootView;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
@@ -348,6 +349,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private final AccessibilityDelegate mAccessibilityDelegate = new ShadeAccessibilityDelegate();
private final NotificationGutsManager mGutsManager;
private final AlternateBouncerInteractor mAlternateBouncerInteractor;
private final KeyguardRootView mKeyguardRootView;
private final QuickSettingsController mQsController;
private final TouchHandler mTouchHandler = new TouchHandler();
@@ -363,6 +365,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private boolean mTracking;
private boolean mIsTrackingExpansionFromStatusBar;
private boolean mHintAnimationRunning;
@Deprecated
private KeyguardBottomAreaView mKeyguardBottomArea;
private boolean mExpanding;
private boolean mSplitShadeEnabled;
@@ -739,7 +742,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
KeyguardInteractor keyguardInteractor,
ActivityStarter activityStarter,
KeyguardViewConfigurator keyguardViewConfigurator,
KeyguardFaceAuthInteractor keyguardFaceAuthInteractor) {
KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
KeyguardRootView keyguardRootView) {
keyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
public void onKeyguardFadingAwayChanged() {
@@ -939,6 +943,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
}
});
mAlternateBouncerInteractor = alternateBouncerInteractor;
mKeyguardRootView = keyguardRootView;
dumpManager.registerDumpable(this);
}
@@ -1029,7 +1034,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mShadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged);
mShadeHeadsUpTracker.addTrackingHeadsUpListener(
mNotificationStackScrollLayoutController::setTrackingHeadsUp);
setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area));
if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area));
}
initBottomArea();
@@ -1311,14 +1318,17 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
updateViewControllers(mView.findViewById(R.id.keyguard_status_view), userAvatarView,
keyguardUserSwitcherView);
// Update keyguard bottom area
int index = mView.indexOfChild(mKeyguardBottomArea);
mView.removeView(mKeyguardBottomArea);
KeyguardBottomAreaView oldBottomArea = mKeyguardBottomArea;
setKeyguardBottomArea(mKeyguardBottomAreaViewControllerProvider.get().getView());
mKeyguardBottomArea.initFrom(oldBottomArea);
mView.addView(mKeyguardBottomArea, index);
initBottomArea();
if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
// Update keyguard bottom area
int index = mView.indexOfChild(mKeyguardBottomArea);
mView.removeView(mKeyguardBottomArea);
KeyguardBottomAreaView oldBottomArea = mKeyguardBottomArea;
setKeyguardBottomArea(mKeyguardBottomAreaViewControllerProvider.get().getView());
mKeyguardBottomArea.initFrom(oldBottomArea);
mView.addView(mKeyguardBottomArea, index);
initBottomArea();
}
mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(),
mStatusBarStateController.getInterpolatedDozeAmount());
@@ -1341,7 +1351,12 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
false,
mBarState);
}
setKeyguardBottomAreaVisibility(mBarState, false);
if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
setKeyguardVisibility(mBarState, false);
} else {
setKeyguardBottomAreaVisibility(mBarState, false);
}
mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
mNotificationPanelUnfoldAnimationController.ifPresent(u -> u.setup(mView));
@@ -1352,7 +1367,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
}
private void initBottomArea() {
mKeyguardBottomArea.init(
if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
mKeyguardBottomArea.init(
mKeyguardBottomAreaViewModel,
mFalsingManager,
mLockIconViewController,
@@ -1361,8 +1377,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mVibratorHelper,
mActivityStarter);
// Rebind (for now), as a new bottom area and indication area may have been created
mKeyguardViewConfigurator.bindIndicationArea(mKeyguardBottomArea);
// Rebind (for now), as a new bottom area and indication area may have been created
mKeyguardViewConfigurator.bindIndicationArea();
}
}
@VisibleForTesting
@@ -1398,6 +1415,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
return mHintAnimationRunning || mUnlockedScreenOffAnimationController.isAnimationPlaying();
}
@Deprecated
private void setKeyguardBottomArea(KeyguardBottomAreaView keyguardBottomArea) {
mKeyguardBottomArea = keyguardBottomArea;
}
@@ -1521,8 +1539,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mClockPositionAlgorithm.run(mClockPositionResult);
mKeyguardStatusViewController.setLockscreenClockY(
mClockPositionAlgorithm.getExpandedPreferredClockY());
mKeyguardBottomAreaInteractor.setClockPosition(
if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
mKeyguardInteractor.setClockPosition(
mClockPositionResult.clockX, mClockPositionResult.clockY);
} else {
mKeyguardBottomAreaInteractor.setClockPosition(
mClockPositionResult.clockX, mClockPositionResult.clockY);
}
boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange;
mKeyguardStatusViewController.updatePosition(
@@ -2169,6 +2192,15 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
}
}
private void setKeyguardVisibility(int statusBarState, boolean goingToFullShade) {
mKeyguardInteractor.setKeyguardRootVisibility(
statusBarState,
goingToFullShade,
mIsOcclusionTransitionRunning
);
}
@Deprecated
private void setKeyguardBottomAreaVisibility(int statusBarState, boolean goingToFullShade) {
mKeyguardBottomArea.animate().cancel();
if (goingToFullShade) {
@@ -2177,8 +2209,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mKeyguardStateController.getShortenedFadingAwayDuration()).setInterpolator(
Interpolators.ALPHA_OUT).withEndAction(
mAnimateKeyguardBottomAreaInvisibleEndRunnable).start();
} else if (statusBarState == KEYGUARD
|| statusBarState == StatusBarState.SHADE_LOCKED) {
} else if (statusBarState == KEYGUARD || statusBarState == StatusBarState.SHADE_LOCKED) {
mKeyguardBottomArea.setVisibility(View.VISIBLE);
if (!mIsOcclusionTransitionRunning) {
mKeyguardBottomArea.setAlpha(1f);
@@ -2514,7 +2545,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
getExpandedFraction());
float alpha = Math.min(expansionAlpha, 1 - mQsController.computeExpansionFraction());
alpha *= mBottomAreaShadeAlpha;
mKeyguardBottomAreaInteractor.setAlpha(alpha);
if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
mKeyguardInteractor.setAlpha(alpha);
} else {
mKeyguardBottomAreaInteractor.setAlpha(alpha);
}
mLockIconViewController.setAlpha(alpha);
}
@@ -2773,7 +2808,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
}
private void updateDozingVisibilities(boolean animate) {
mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
mKeyguardInteractor.setAnimateDozingTransitions(animate);
} else {
mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
}
if (!mDozing && animate) {
mKeyguardStatusBarViewController.animateKeyguardStatusBarIn();
}
@@ -2977,7 +3016,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mDozing = dozing;
// TODO (b/) make listeners for this
mNotificationStackScrollLayoutController.setDozing(mDozing, animate);
mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
mKeyguardInteractor.setAnimateDozingTransitions(animate);
} else {
mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
}
mKeyguardStatusBarViewController.setDozing(mDozing);
mQsController.setDozing(mDozing);
@@ -3898,20 +3941,37 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
animator.start();
setAnimator(animator);
final List<ViewPropertyAnimator> indicationAnimators =
mKeyguardBottomArea.getIndicationAreaAnimators();
for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) {
indicationAreaAnimator
if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
final ViewPropertyAnimator mKeyguardRootViewAnimator = mKeyguardRootView.animate();
mKeyguardRootViewAnimator
.translationY(-mHintDistance)
.setDuration(250)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.withEndAction(() -> indicationAreaAnimator
.withEndAction(() -> mKeyguardRootViewAnimator
.translationY(0)
.setDuration(450)
.setInterpolator(mBounceInterpolator)
.start())
.start();
} else {
final List<ViewPropertyAnimator> indicationAnimators =
mKeyguardBottomArea.getIndicationAreaAnimators();
for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) {
indicationAreaAnimator
.translationY(-mHintDistance)
.setDuration(250)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.withEndAction(() -> indicationAreaAnimator
.translationY(0)
.setDuration(450)
.setInterpolator(mBounceInterpolator)
.start())
.start();
}
}
}
private void setAnimator(ValueAnimator animator) {
@@ -4352,7 +4412,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
goingToFullShade,
mBarState);
setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
setKeyguardVisibility(statusBarState, goingToFullShade);
} else {
setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
}
// TODO: maybe add a listener for barstate
mBarState = statusBarState;
@@ -4589,7 +4653,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mKeyguardStatusViewController.setAlpha(alpha);
stackScroller.setAlpha(alpha);
mKeyguardBottomAreaInteractor.setAlpha(alpha);
if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
mKeyguardInteractor.setAlpha(alpha);
} else {
mKeyguardBottomAreaInteractor.setAlpha(alpha);
}
mLockIconViewController.setAlpha(alpha);
if (mKeyguardQsUserSwitchController != null) {

View File

@@ -38,6 +38,7 @@ import com.android.systemui.statusbar.VibratorHelper
* elements. A secondary concern is the interaction of the quick affordance elements with the
* indication area between them, though the indication area is primarily controlled elsewhere.
*/
@Deprecated("Deprecated as part of b/278057014")
class KeyguardBottomAreaView
@JvmOverloads
constructor(
@@ -53,6 +54,7 @@ constructor(
defStyleRes,
) {
@Deprecated("Deprecated as part of b/278057014")
interface MessageDisplayer {
fun display(@StringRes stringResourceId: Int)
}
@@ -62,6 +64,7 @@ constructor(
private var lockIconViewController: LockIconViewController? = null
/** Initializes the view. */
@Deprecated("Deprecated as part of b/278057014")
fun init(
viewModel: KeyguardBottomAreaViewModel,
falsingManager: FalsingManager? = null,
@@ -88,6 +91,7 @@ constructor(
* Initializes this instance of [KeyguardBottomAreaView] based on the given instance of another
* [KeyguardBottomAreaView]
*/
@Deprecated("Deprecated as part of b/278057014")
fun initFrom(oldBottomArea: KeyguardBottomAreaView) {
// if it exists, continue to use the original ambient indication container
// instead of the newly inflated one
@@ -122,9 +126,11 @@ constructor(
}
/** Returns a list of animators to use to animate the indication areas. */
@Deprecated("Deprecated as part of b/278057014")
val indicationAreaAnimators: List<ViewPropertyAnimator>
get() = checkNotNull(binding).getIndicationAreaAnimators()
override fun hasOverlappingRendering(): Boolean {
return false
}

View File

@@ -292,24 +292,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
assertThat(collectedValue).isInstanceOf(KeyguardQuickAffordanceModel.Hidden::class.java)
}
@Test
fun quickAffordance_hiddenWhenQuickSettingsIsVisible() =
testScope.runTest {
repository.setQuickSettingsVisible(true)
quickAccessWallet.setState(
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
icon = ICON,
)
)
val collectedValue =
collectLastValue(
underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
)
assertThat(collectedValue()).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
}
@Test
fun quickAffordance_hiddenWhenUserIsInLockdownMode() =
testScope.runTest {

View File

@@ -26,6 +26,8 @@ import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.AuthController
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.ui.view.KeyguardRootView
import com.android.systemui.monet.utils.ArgbSubject.assertThat
import org.junit.Before
@@ -42,9 +44,10 @@ class DefaultLockscreenLayoutTest : SysuiTestCase() {
private lateinit var defaultLockscreenLayout: DefaultLockscreenLayout
private lateinit var rootView: KeyguardRootView
@Mock private lateinit var authController: AuthController
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var windowManager: WindowManager
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Before
fun setup() {
MockitoAnnotations.initMocks(this)

View File

@@ -20,6 +20,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
@@ -45,6 +46,8 @@ import org.mockito.MockitoAnnotations
class KeyguardIndicationAreaViewModelTest : SysuiTestCase() {
@Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
@Mock private lateinit var shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel
@Mock private lateinit var featureFlags: FeatureFlags
private lateinit var underTest: KeyguardIndicationAreaViewModel
private lateinit var repository: FakeKeyguardRepository
@@ -83,6 +86,8 @@ class KeyguardIndicationAreaViewModelTest : SysuiTestCase() {
bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository),
keyguardBottomAreaViewModel = bottomAreaViewModel,
burnInHelperWrapper = burnInHelperWrapper,
shortcutsCombinedViewModel = shortcutsCombinedViewModel,
featureFlags = featureFlags,
)
}

View File

@@ -0,0 +1,681 @@
/*
* 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 android.app.admin.DevicePolicyManager
import android.content.Intent
import android.os.UserHandle
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dock.DockManagerFake
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import kotlin.math.max
import kotlin.math.min
@SmallTest
@RunWith(JUnit4::class)
class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() {
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
@Mock private lateinit var expandable: Expandable
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var lockPatternUtils: LockPatternUtils
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var launchAnimator: DialogLaunchAnimator
@Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger
private lateinit var underTest: KeyguardQuickAffordancesCombinedViewModel
private lateinit var testScope: TestScope
private lateinit var repository: FakeKeyguardRepository
private lateinit var homeControlsQuickAffordanceConfig: FakeKeyguardQuickAffordanceConfig
private lateinit var quickAccessWalletAffordanceConfig: FakeKeyguardQuickAffordanceConfig
private lateinit var qrCodeScannerAffordanceConfig: FakeKeyguardQuickAffordanceConfig
private lateinit var dockManager: DockManagerFake
private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
private lateinit var keyguardInteractor: KeyguardInteractor
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
overrideResource(R.bool.custom_lockscreen_shortcuts_enabled, true)
overrideResource(
R.array.config_keyguardQuickAffordanceDefaults,
arrayOf(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + ":" +
BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END + ":" +
BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
)
)
homeControlsQuickAffordanceConfig =
FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
quickAccessWalletAffordanceConfig =
FakeKeyguardQuickAffordanceConfig(
BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
)
qrCodeScannerAffordanceConfig =
FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
dockManager = DockManagerFake()
biometricSettingsRepository = FakeBiometricSettingsRepository()
val featureFlags =
FakeFeatureFlags().apply {
set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true)
set(Flags.FACE_AUTH_REFACTOR, true)
set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false)
set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false)
}
val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags)
keyguardInteractor = withDeps.keyguardInteractor
repository = withDeps.repository
whenever(userTracker.userHandle).thenReturn(mock())
whenever(lockPatternUtils.getStrongAuthForUser(ArgumentMatchers.anyInt()))
.thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
val testDispatcher = StandardTestDispatcher()
testScope = TestScope(testDispatcher)
val localUserSelectionManager =
KeyguardQuickAffordanceLocalUserSelectionManager(
context = context,
userFileManager =
mock<UserFileManager>().apply {
whenever(
getSharedPreferences(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyInt(),
ArgumentMatchers.anyInt(),
)
)
.thenReturn(FakeSharedPreferences())
},
userTracker = userTracker,
broadcastDispatcher = fakeBroadcastDispatcher,
)
val remoteUserSelectionManager =
KeyguardQuickAffordanceRemoteUserSelectionManager(
scope = testScope.backgroundScope,
userTracker = userTracker,
clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
userHandle = UserHandle.SYSTEM,
)
val quickAffordanceRepository =
KeyguardQuickAffordanceRepository(
appContext = context,
scope = testScope.backgroundScope,
localUserSelectionManager = localUserSelectionManager,
remoteUserSelectionManager = remoteUserSelectionManager,
userTracker = userTracker,
legacySettingSyncer =
KeyguardQuickAffordanceLegacySettingSyncer(
scope = testScope.backgroundScope,
backgroundDispatcher = testDispatcher,
secureSettings = FakeSettings(),
selectionsManager = localUserSelectionManager,
),
configs =
setOf(
homeControlsQuickAffordanceConfig,
quickAccessWalletAffordanceConfig,
qrCodeScannerAffordanceConfig,
),
dumpManager = mock(),
userHandle = UserHandle.SYSTEM,
)
underTest = KeyguardQuickAffordancesCombinedViewModel(
quickAffordanceInteractor =
KeyguardQuickAffordanceInteractor(
keyguardInteractor = keyguardInteractor,
lockPatternUtils = lockPatternUtils,
keyguardStateController = keyguardStateController,
userTracker = userTracker,
activityStarter = activityStarter,
featureFlags = featureFlags,
repository = { quickAffordanceRepository },
launchAnimator = launchAnimator,
logger = logger,
devicePolicyManager = devicePolicyManager,
dockManager = dockManager,
biometricSettingsRepository = biometricSettingsRepository,
backgroundDispatcher = testDispatcher,
appContext = mContext,
),
keyguardInteractor = keyguardInteractor
)
}
@Test
fun startButton_present_visibleModel_startsActivityOnClick() =
testScope.runTest {
repository.setKeyguardShowing(true)
val latest = collectLastValue(underTest.startButton)
val testConfig =
TestConfig(
isVisible = true,
isClickable = true,
isActivated = true,
icon = mock(),
canShowWhileLocked = false,
intent = Intent("action"),
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
)
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig = testConfig,
)
assertQuickAffordanceViewModel(
viewModel = latest(),
testConfig = testConfig,
configKey = configKey,
)
}
@Test
fun startButton_hiddenWhenDevicePolicyDisablesAllKeyguardFeatures() =
testScope.runTest {
whenever(
devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId)
).thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL)
repository.setKeyguardShowing(true)
val latest by collectLastValue(underTest.startButton)
val testConfig =
TestConfig(
isVisible = true,
isClickable = true,
isActivated = true,
icon = mock(),
canShowWhileLocked = false,
intent = Intent("action"),
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
)
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig = testConfig,
)
assertQuickAffordanceViewModel(
viewModel = latest,
testConfig =
TestConfig(
isVisible = false,
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
),
configKey = configKey,
)
}
@Test
fun startButton_inPreviewMode_visibleEvenWhenKeyguardNotShowing() =
testScope.runTest {
underTest.onPreviewSlotSelected(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
)
keyguardInteractor.previewMode.value =
KeyguardInteractor.PreviewMode(
isInPreviewMode = true,
shouldHighlightSelectedAffordance = true,
)
repository.setKeyguardShowing(false)
val latest = collectLastValue(underTest.startButton)
val icon: Icon = mock()
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig =
TestConfig(
isVisible = true,
isClickable = true,
isActivated = true,
icon = icon,
canShowWhileLocked = false,
intent = Intent("action"),
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
),
)
assertQuickAffordanceViewModel(
viewModel = latest(),
testConfig =
TestConfig(
isVisible = true,
isClickable = false,
isActivated = false,
icon = icon,
canShowWhileLocked = false,
intent = Intent("action"),
isSelected = true,
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
),
configKey = configKey,
)
Truth.assertThat(latest()?.isSelected).isTrue()
}
@Test
fun endButton_inHiglightedPreviewMode_dimmedWhenOtherIsSelected() =
testScope.runTest {
underTest.onPreviewSlotSelected(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
)
keyguardInteractor.previewMode.value =
KeyguardInteractor.PreviewMode(
isInPreviewMode = true,
shouldHighlightSelectedAffordance = true,
)
repository.setKeyguardShowing(false)
val endButton = collectLastValue(underTest.endButton)
val icon: Icon = mock()
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig =
TestConfig(
isVisible = true,
isClickable = true,
isActivated = true,
icon = icon,
canShowWhileLocked = false,
intent = Intent("action"),
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
),
)
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_END,
testConfig =
TestConfig(
isVisible = true,
isClickable = true,
isActivated = true,
icon = icon,
canShowWhileLocked = false,
intent = Intent("action"),
slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(),
),
)
assertQuickAffordanceViewModel(
viewModel = endButton(),
testConfig =
TestConfig(
isVisible = true,
isClickable = false,
isActivated = false,
icon = icon,
canShowWhileLocked = false,
intent = Intent("action"),
isDimmed = true,
slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(),
),
configKey = configKey,
)
}
@Test
fun endButton_present_visibleModel_doNothingOnClick() =
testScope.runTest {
repository.setKeyguardShowing(true)
val latest = collectLastValue(underTest.endButton)
val config =
TestConfig(
isVisible = true,
isClickable = true,
icon = mock(),
canShowWhileLocked = false,
intent =
null, // This will cause it to tell the system that the click was handled.
slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(),
)
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_END,
testConfig = config,
)
assertQuickAffordanceViewModel(
viewModel = latest(),
testConfig = config,
configKey = configKey,
)
}
@Test
fun startButton_notPresent_modelIsHidden() =
testScope.runTest {
val latest = collectLastValue(underTest.startButton)
val config =
TestConfig(
isVisible = false,
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
)
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig = config,
)
assertQuickAffordanceViewModel(
viewModel = latest(),
testConfig = config,
configKey = configKey,
)
}
@Test
fun animateButtonReveal() =
testScope.runTest {
repository.setKeyguardShowing(true)
val testConfig =
TestConfig(
isVisible = true,
isClickable = true,
icon = mock(),
canShowWhileLocked = false,
intent = Intent("action"),
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
)
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig = testConfig,
)
val value = collectLastValue(underTest.startButton.map { it.animateReveal })
Truth.assertThat(value()).isFalse()
repository.setAnimateDozingTransitions(true)
Truth.assertThat(value()).isTrue()
repository.setAnimateDozingTransitions(false)
Truth.assertThat(value()).isFalse()
}
@Test
fun isClickable_trueWhenAlphaAtThreshold() =
testScope.runTest {
repository.setKeyguardShowing(true)
repository.setKeyguardAlpha(
KeyguardQuickAffordancesCombinedViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD
)
val testConfig =
TestConfig(
isVisible = true,
isClickable = true,
icon = mock(),
canShowWhileLocked = false,
intent = Intent("action"),
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
)
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig = testConfig,
)
val latest = collectLastValue(underTest.startButton)
assertQuickAffordanceViewModel(
viewModel = latest(),
testConfig = testConfig,
configKey = configKey,
)
}
@Test
fun isClickable_trueWhenAlphaAboveThreshold() =
testScope.runTest {
repository.setKeyguardShowing(true)
val latest = collectLastValue(underTest.startButton)
repository.setKeyguardAlpha(
min(
1f,
KeyguardQuickAffordancesCombinedViewModel
.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f
),
)
val testConfig =
TestConfig(
isVisible = true,
isClickable = true,
icon = mock(),
canShowWhileLocked = false,
intent = Intent("action"),
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
)
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig = testConfig,
)
assertQuickAffordanceViewModel(
viewModel = latest(),
testConfig = testConfig,
configKey = configKey,
)
}
@Test
fun isClickable_falseWhenAlphaBelowThreshold() =
testScope.runTest {
repository.setKeyguardShowing(true)
val latest = collectLastValue(underTest.startButton)
repository.setKeyguardAlpha(
max(
0f,
KeyguardQuickAffordancesCombinedViewModel
.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f
),
)
val testConfig =
TestConfig(
isVisible = true,
isClickable = false,
icon = mock(),
canShowWhileLocked = false,
intent = Intent("action"),
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
)
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig = testConfig,
)
assertQuickAffordanceViewModel(
viewModel = latest(),
testConfig = testConfig,
configKey = configKey,
)
}
@Test
fun isClickable_falseWhenAlphaAtZero() =
testScope.runTest {
repository.setKeyguardShowing(true)
val latest = collectLastValue(underTest.startButton)
repository.setKeyguardAlpha(0f)
val testConfig =
TestConfig(
isVisible = true,
isClickable = false,
icon = mock(),
canShowWhileLocked = false,
intent = Intent("action"),
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
)
val configKey =
setUpQuickAffordanceModel(
position = KeyguardQuickAffordancePosition.BOTTOM_START,
testConfig = testConfig,
)
assertQuickAffordanceViewModel(
viewModel = latest(),
testConfig = testConfig,
configKey = configKey,
)
}
private suspend fun setUpQuickAffordanceModel(
position: KeyguardQuickAffordancePosition,
testConfig: TestConfig,
): String {
val config =
when (position) {
KeyguardQuickAffordancePosition.BOTTOM_START -> homeControlsQuickAffordanceConfig
KeyguardQuickAffordancePosition.BOTTOM_END -> quickAccessWalletAffordanceConfig
}
val lockScreenState =
if (testConfig.isVisible) {
if (testConfig.intent != null) {
config.onTriggeredResult =
KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity(
intent = testConfig.intent,
canShowWhileLocked = testConfig.canShowWhileLocked,
)
}
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
icon = testConfig.icon ?: error("Icon is unexpectedly null!"),
activationState =
when (testConfig.isActivated) {
true -> ActivationState.Active
false -> ActivationState.Inactive
}
)
} else {
KeyguardQuickAffordanceConfig.LockScreenState.Hidden
}
config.setState(lockScreenState)
return "${position.toSlotId()}::${config.key}"
}
private fun assertQuickAffordanceViewModel(
viewModel: KeyguardQuickAffordanceViewModel?,
testConfig: TestConfig,
configKey: String,
) {
checkNotNull(viewModel)
Truth.assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
Truth.assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable)
Truth.assertThat(viewModel.isActivated).isEqualTo(testConfig.isActivated)
Truth.assertThat(viewModel.isSelected).isEqualTo(testConfig.isSelected)
Truth.assertThat(viewModel.isDimmed).isEqualTo(testConfig.isDimmed)
Truth.assertThat(viewModel.slotId).isEqualTo(testConfig.slotId)
if (testConfig.isVisible) {
Truth.assertThat(viewModel.icon).isEqualTo(testConfig.icon)
viewModel.onClicked.invoke(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = configKey,
expandable = expandable,
slotId = viewModel.slotId,
)
)
if (testConfig.intent != null) {
Truth.assertThat(
Mockito.mockingDetails(activityStarter).invocations
).hasSize(1)
} else {
Mockito.verifyZeroInteractions(activityStarter)
}
} else {
Truth.assertThat(viewModel.isVisible).isFalse()
}
}
private data class TestConfig(
val isVisible: Boolean,
val isClickable: Boolean = false,
val isActivated: Boolean = false,
val icon: Icon? = null,
val canShowWhileLocked: Boolean = false,
val intent: Intent? = null,
val isSelected: Boolean = false,
val isDimmed: Boolean = false,
val slotId: String = ""
) {
init {
check(!isVisible || icon != null) { "Must supply non-null icon if visible!" }
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.google.common.truth.Truth
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(JUnit4::class)
class KeyguardRootViewModelTest : SysuiTestCase() {
private lateinit var underTest: KeyguardRootViewModel
private lateinit var testScope: TestScope
private lateinit var repository: FakeKeyguardRepository
private lateinit var keyguardInteractor: KeyguardInteractor
@Mock private lateinit var keyguardQuickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
val testDispatcher = StandardTestDispatcher()
testScope = TestScope(testDispatcher)
val featureFlags =
FakeFeatureFlags().apply {
set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true)
set(Flags.FACE_AUTH_REFACTOR, true)
}
val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags)
keyguardInteractor = withDeps.keyguardInteractor
repository = withDeps.repository
underTest = KeyguardRootViewModel(
keyguardInteractor,
keyguardQuickAffordancesCombinedViewModel,
)
}
@Test
fun alpha() =
testScope.runTest {
val value = collectLastValue(underTest.alpha)
Truth.assertThat(value()).isEqualTo(1f)
repository.setKeyguardAlpha(0.1f)
Truth.assertThat(value()).isEqualTo(0.1f)
repository.setKeyguardAlpha(0.5f)
Truth.assertThat(value()).isEqualTo(0.5f)
repository.setKeyguardAlpha(0.2f)
Truth.assertThat(value()).isEqualTo(0.2f)
repository.setKeyguardAlpha(0f)
Truth.assertThat(value()).isEqualTo(0f)
}
@Test
fun alpha_inPreviewMode_doesNotChange() =
testScope.runTest {
val value = collectLastValue(underTest.alpha)
underTest.enablePreviewMode(
initiallySelectedSlotId = null,
shouldHighlightSelectedAffordance = false,
)
Truth.assertThat(value()).isEqualTo(1f)
repository.setKeyguardAlpha(0.1f)
Truth.assertThat(value()).isEqualTo(1f)
repository.setKeyguardAlpha(0.5f)
Truth.assertThat(value()).isEqualTo(1f)
repository.setKeyguardAlpha(0.2f)
Truth.assertThat(value()).isEqualTo(1f)
repository.setKeyguardAlpha(0f)
Truth.assertThat(value()).isEqualTo(1f)
}
}

View File

@@ -97,6 +97,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteracto
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.ui.view.KeyguardRootView;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
@@ -314,6 +315,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
@Mock private ShadeInteractor mShadeInteractor;
@Mock private JavaAdapter mJavaAdapter;
@Mock private CastController mCastController;
@Mock private KeyguardRootView mKeyguardRootView;
protected final int mMaxUdfpsBurnInOffsetY = 5;
protected KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
@@ -618,7 +620,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
mKeyguardInteractor,
mActivityStarter,
mKeyguardViewConfigurator,
mKeyguardFaceAuthInteractor);
mKeyguardFaceAuthInteractor,
mKeyguardRootView);
mNotificationPanelViewController.initDependencies(
mCentralSurfaces,
null,

View File

@@ -22,6 +22,7 @@ import com.android.systemui.common.shared.model.Position
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardRootViewVisibilityState
import com.android.systemui.keyguard.shared.model.ScreenModel
import com.android.systemui.keyguard.shared.model.ScreenState
import com.android.systemui.keyguard.shared.model.StatusBarState
@@ -115,6 +116,20 @@ class FakeKeyguardRepository : KeyguardRepository {
private val _isQuickSettingsVisible = MutableStateFlow(false)
override val isQuickSettingsVisible: Flow<Boolean> = _isQuickSettingsVisible.asStateFlow()
private val _keyguardAlpha = MutableStateFlow(1f)
override val keyguardAlpha: StateFlow<Float> = _keyguardAlpha
private val _keyguardRootViewVisibility =
MutableStateFlow(
KeyguardRootViewVisibilityState(
0,
goingToFullShade = false,
occlusionTransitionRunning = false
)
)
override val keyguardRootViewVisibility: Flow<KeyguardRootViewVisibilityState> =
_keyguardRootViewVisibility.asStateFlow()
override fun setQuickSettingsVisible(isVisible: Boolean) {
_isQuickSettingsVisible.value = isVisible
}
@@ -132,6 +147,8 @@ class FakeKeyguardRepository : KeyguardRepository {
_animateBottomAreaDozingTransitions.tryEmit(animate)
}
@Deprecated("Deprecated as part of b/278057014")
override fun setBottomAreaAlpha(alpha: Float) {
_bottomAreaAlpha.value = alpha
}
@@ -223,4 +240,18 @@ class FakeKeyguardRepository : KeyguardRepository {
override fun isUdfpsSupported(): Boolean {
return _isUdfpsSupported.value
}
override fun setKeyguardAlpha(alpha: Float) {
_keyguardAlpha.value = alpha
}
override fun setKeyguardVisibility(
statusBarState: Int,
goingToFullShade: Boolean,
occlusionTransitionRunning: Boolean
) {
_keyguardRootViewVisibility.value = KeyguardRootViewVisibilityState(
statusBarState, goingToFullShade, occlusionTransitionRunning
)
}
}