blob: ab9e6a4ce045eb20408c03791e912d0025a31d3e [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.keyguard.ui.viewmodel
import androidx.annotation.VisibleForTesting
import com.android.systemui.doze.util.BurnInHelperWrapper
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 javax.inject.Inject
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
/** View-model for the keyguard bottom area view */
@OptIn(ExperimentalCoroutinesApi::class)
class KeyguardBottomAreaViewModel
@Inject
constructor(
private val keyguardInteractor: KeyguardInteractor,
private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor,
private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
private val burnInHelperWrapper: BurnInHelperWrapper,
) {
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.
*/
private val previewMode = MutableStateFlow(PreviewMode())
/**
* 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> =
bottomAreaInteractor.alpha
.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)
/** An observable for whether the overlay container should be visible. */
val isOverlayContainerVisible: Flow<Boolean> =
keyguardInteractor.isDozing.map { !it }.distinctUntilChanged()
/** An observable for the alpha level for the entire bottom area. */
val alpha: Flow<Float> =
previewMode.flatMapLatest {
if (it.isInPreviewMode) {
flowOf(1f)
} else {
bottomAreaInteractor.alpha.distinctUntilChanged()
}
}
/** An observable for whether the indication area should be padded. */
val isIndicationAreaPadded: Flow<Boolean> =
combine(startButton, 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()
/** 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()
}
/**
* Returns whether the keyguard bottom area should be constrained to the top of the lock icon
*/
fun shouldConstrainToTopOfLockIcon(): Boolean =
bottomAreaInteractor.shouldConstrainToTopOfLockIcon()
/**
* 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,
) {
previewMode.value =
PreviewMode(
isInPreviewMode = true,
shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
)
onPreviewSlotSelected(
initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
)
}
/**
* 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 enablePreviewMode
*/
fun onPreviewSlotSelected(slotId: String) {
selectedPreviewSlotId.value = slotId
}
private fun button(
position: KeyguardQuickAffordancePosition
): Flow<KeyguardQuickAffordanceViewModel> {
return previewMode.flatMapLatest { previewMode ->
combine(
if (previewMode.isInPreviewMode) {
quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position)
} else {
quickAffordanceInteractor.quickAffordance(position = position)
},
bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(),
areQuickAffordancesFullyOpaque,
selectedPreviewSlotId,
) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId ->
val isSelected = selectedPreviewSlotId == position.toSlotId()
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
)
}
.distinctUntilChanged()
}
}
private fun KeyguardQuickAffordanceModel.toViewModel(
animateReveal: Boolean,
isClickable: Boolean,
isSelected: Boolean,
isDimmed: Boolean,
forceInactive: 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,
)
},
isClickable = isClickable,
isActivated = !forceInactive && activationState is ActivationState.Active,
isSelected = isSelected,
useLongPress = quickAffordanceInteractor.useLongPress,
isDimmed = isDimmed,
)
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
}
}
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
}
}