KeyguardBottomAreaView new impl. - UI layer
- In this third CL in the series, we create the UI layer for the KeyguardBottomAreaView and also start using it based on our feature flag - The UI layer contains both the view-models and view-binder that uses them - The KeyguardBottomAreaView class is rigged to use the binder/view-model combo instead of its old logic, which is kept intact Bug: 235403546 Fix: 235403546 Test: Unit tests are included for the view-model only. Also manually verified that the bottom area behaves as expected with the feature flag on and with the feature flag off. Specifically: - The buttons appear at the right times, fade away as you scroll up/down and disappear when the device is unlocked - All three buttons (home controls, quick access wallet, and QR code scanner) work when clicked - The indication area text properly shifts its x/y position to avoid pixel burn-in. This is especially visible when moving between AOD and lock-screen Change-Id: Ia2e838d81be2b0f2675efdd10d046e3ed5e79ef1
This commit is contained in:
@@ -59,6 +59,30 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/start_button"
|
||||
android:layout_height="@dimen/keyguard_affordance_fixed_height"
|
||||
android:layout_width="@dimen/keyguard_affordance_fixed_width"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:scaleType="center"
|
||||
android:tint="?android:attr/textColorPrimary"
|
||||
android:background="@drawable/keyguard_bottom_affordance_bg"
|
||||
android:layout_marginStart="@dimen/keyguard_affordance_horizontal_offset"
|
||||
android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/end_button"
|
||||
android:layout_height="@dimen/keyguard_affordance_fixed_height"
|
||||
android:layout_width="@dimen/keyguard_affordance_fixed_width"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:scaleType="center"
|
||||
android:tint="?android:attr/textColorPrimary"
|
||||
android:background="@drawable/keyguard_bottom_affordance_bg"
|
||||
android:layout_marginEnd="@dimen/keyguard_affordance_horizontal_offset"
|
||||
android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/wallet_button"
|
||||
android:layout_height="@dimen/keyguard_affordance_fixed_height"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.doze.util
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Injectable wrapper around `BurnInHelper` functions */
|
||||
class BurnInHelperWrapper @Inject constructor() {
|
||||
|
||||
fun burnInOffset(amplitude: Int, xAxis: Boolean): Int {
|
||||
return getBurnInOffset(amplitude, xAxis)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* 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.binder
|
||||
|
||||
import android.util.Size
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.android.settingslib.Utils
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
||||
import com.android.systemui.animation.Interpolators
|
||||
import com.android.systemui.containeddrawable.ContainedDrawable
|
||||
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
|
||||
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
|
||||
import com.android.systemui.lifecycle.repeatWhenAttached
|
||||
import com.android.systemui.plugins.FalsingManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Binds a keyguard bottom area view to its view-model.
|
||||
*
|
||||
* To use this properly, users should maintain a one-to-one relationship between the [View] and the
|
||||
* view-binding, binding each view only once. It is okay and expected for the same instance of the
|
||||
* view-model to be reused for multiple view/view-binder bindings.
|
||||
*/
|
||||
object KeyguardBottomAreaViewBinder {
|
||||
|
||||
private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
|
||||
/** Binds the view to the view-model, continuing to update the former based on the latter. */
|
||||
@JvmStatic
|
||||
fun bind(
|
||||
view: ViewGroup,
|
||||
viewModel: KeyguardBottomAreaViewModel,
|
||||
falsingManager: FalsingManager,
|
||||
): Binding {
|
||||
val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
|
||||
val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container)
|
||||
val startButton: ImageView = view.requireViewById(R.id.start_button)
|
||||
val endButton: ImageView = view.requireViewById(R.id.end_button)
|
||||
val overlayContainer: View = view.requireViewById(R.id.overlay_container)
|
||||
val indicationText: TextView = view.requireViewById(R.id.keyguard_indication_text)
|
||||
val indicationTextBottom: TextView =
|
||||
view.requireViewById(R.id.keyguard_indication_text_bottom)
|
||||
|
||||
view.clipChildren = false
|
||||
view.clipToPadding = false
|
||||
|
||||
val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
|
||||
|
||||
view.repeatWhenAttached {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch {
|
||||
combine(viewModel.startButton, viewModel.animateButtonReveal) {
|
||||
buttonModel,
|
||||
animateReveal ->
|
||||
Pair(buttonModel, animateReveal)
|
||||
}
|
||||
.collect { (buttonModel, animateReveal) ->
|
||||
updateButton(
|
||||
view = startButton,
|
||||
viewModel = buttonModel,
|
||||
animateReveal = animateReveal,
|
||||
falsingManager = falsingManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
combine(viewModel.endButton, viewModel.animateButtonReveal) {
|
||||
buttonModel,
|
||||
animateReveal ->
|
||||
Pair(buttonModel, animateReveal)
|
||||
}
|
||||
.collect { (buttonModel, animateReveal) ->
|
||||
updateButton(
|
||||
view = endButton,
|
||||
viewModel = buttonModel,
|
||||
animateReveal = animateReveal,
|
||||
falsingManager = falsingManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.isOverlayContainerVisible.collect { isVisible ->
|
||||
overlayContainer.visibility =
|
||||
if (isVisible) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.INVISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.alpha.collect { alpha ->
|
||||
view.importantForAccessibility =
|
||||
if (alpha == 0f) {
|
||||
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
||||
} else {
|
||||
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
||||
}
|
||||
|
||||
ambientIndicationArea?.alpha = alpha
|
||||
indicationArea.alpha = alpha
|
||||
startButton.alpha = alpha
|
||||
endButton.alpha = alpha
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.indicationAreaTranslationX.collect { translationX ->
|
||||
indicationArea.translationX = translationX
|
||||
ambientIndicationArea?.translationX = translationX
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
combine(
|
||||
viewModel.isIndicationAreaPadded,
|
||||
configurationBasedDimensions.map { it.indicationAreaPaddingPx },
|
||||
) { isPadded, paddingIfPaddedPx ->
|
||||
if (isPadded) {
|
||||
paddingIfPaddedPx
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
.collect { paddingPx ->
|
||||
indicationArea.setPadding(paddingPx, 0, paddingPx, 0)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
configurationBasedDimensions
|
||||
.map { it.defaultBurnInPreventionYOffsetPx }
|
||||
.flatMapLatest { defaultBurnInOffsetY ->
|
||||
viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
|
||||
}
|
||||
.collect { translationY ->
|
||||
indicationArea.translationY = translationY
|
||||
ambientIndicationArea?.translationY = translationY
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
configurationBasedDimensions.collect { dimensions ->
|
||||
indicationText.setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX,
|
||||
dimensions.indicationTextSizePx.toFloat(),
|
||||
)
|
||||
indicationTextBottom.setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX,
|
||||
dimensions.indicationTextSizePx.toFloat(),
|
||||
)
|
||||
|
||||
startButton.updateLayoutParams<ViewGroup.LayoutParams> {
|
||||
width = dimensions.buttonSizePx.width
|
||||
height = dimensions.buttonSizePx.height
|
||||
}
|
||||
endButton.updateLayoutParams<ViewGroup.LayoutParams> {
|
||||
width = dimensions.buttonSizePx.width
|
||||
height = dimensions.buttonSizePx.height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return object : Binding {
|
||||
override fun getIndicationAreaAnimators(): List<ViewPropertyAnimator> {
|
||||
return listOf(indicationArea, ambientIndicationArea).mapNotNull { it?.animate() }
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged() {
|
||||
configurationBasedDimensions.value = loadFromResources(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateButton(
|
||||
view: ImageView,
|
||||
viewModel: KeyguardQuickAffordanceViewModel,
|
||||
animateReveal: Boolean,
|
||||
falsingManager: FalsingManager,
|
||||
) {
|
||||
if (!viewModel.isVisible) {
|
||||
view.isVisible = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!view.isVisible) {
|
||||
view.isVisible = true
|
||||
if (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()
|
||||
}
|
||||
}
|
||||
|
||||
when (viewModel.icon) {
|
||||
is ContainedDrawable.WithDrawable -> view.setImageDrawable(viewModel.icon.drawable)
|
||||
is ContainedDrawable.WithResource -> view.setImageResource(viewModel.icon.resourceId)
|
||||
}
|
||||
|
||||
view.drawable.setTint(
|
||||
Utils.getColorAttrDefaultColor(
|
||||
view.context,
|
||||
com.android.internal.R.attr.textColorPrimary
|
||||
)
|
||||
)
|
||||
view.backgroundTintList =
|
||||
Utils.getColorAttr(view.context, com.android.internal.R.attr.colorSurface)
|
||||
|
||||
view.contentDescription = view.context.getString(viewModel.contentDescriptionResourceId)
|
||||
view.setOnClickListener {
|
||||
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
if (viewModel.configKey != null) {
|
||||
viewModel.onClicked(
|
||||
KeyguardQuickAffordanceViewModel.OnClickedParameters(
|
||||
configKey = viewModel.configKey,
|
||||
animationController = ActivityLaunchAnimator.Controller.fromView(view),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFromResources(view: View): ConfigurationBasedDimensions {
|
||||
return ConfigurationBasedDimensions(
|
||||
defaultBurnInPreventionYOffsetPx =
|
||||
view.resources.getDimensionPixelOffset(R.dimen.default_burn_in_prevention_offset),
|
||||
indicationAreaPaddingPx =
|
||||
view.resources.getDimensionPixelOffset(R.dimen.keyguard_indication_area_padding),
|
||||
indicationTextSizePx =
|
||||
view.resources.getDimensionPixelSize(
|
||||
com.android.internal.R.dimen.text_size_small_material,
|
||||
),
|
||||
buttonSizePx =
|
||||
Size(
|
||||
view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width),
|
||||
view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private data class ConfigurationBasedDimensions(
|
||||
val defaultBurnInPreventionYOffsetPx: Int,
|
||||
val indicationAreaPaddingPx: Int,
|
||||
val indicationTextSizePx: Int,
|
||||
val buttonSizePx: Size,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 com.android.systemui.doze.util.BurnInHelperWrapper
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveAnimateBottomAreaTransitionsUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveBottomAreaAlphaUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveClockPositionUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveDozeAmountUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveIsDozingUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveKeyguardQuickAffordanceUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.OnKeyguardQuickAffordanceClickedUseCase
|
||||
import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel
|
||||
import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/** View-model for the keyguard bottom area view */
|
||||
class KeyguardBottomAreaViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val observeQuickAffordanceUseCase: ObserveKeyguardQuickAffordanceUseCase,
|
||||
private val onQuickAffordanceClickedUseCase: OnKeyguardQuickAffordanceClickedUseCase,
|
||||
observeBottomAreaAlphaUseCase: ObserveBottomAreaAlphaUseCase,
|
||||
observeIsDozingUseCase: ObserveIsDozingUseCase,
|
||||
observeAnimateBottomAreaTransitionsUseCase: ObserveAnimateBottomAreaTransitionsUseCase,
|
||||
private val observeDozeAmountUseCase: ObserveDozeAmountUseCase,
|
||||
observeClockPositionUseCase: ObserveClockPositionUseCase,
|
||||
private val burnInHelperWrapper: BurnInHelperWrapper,
|
||||
) {
|
||||
/** 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 next time a quick action button becomes visible, it should
|
||||
* animate.
|
||||
*/
|
||||
val animateButtonReveal: Flow<Boolean> =
|
||||
observeAnimateBottomAreaTransitionsUseCase().distinctUntilChanged()
|
||||
/** An observable for whether the overlay container should be visible. */
|
||||
val isOverlayContainerVisible: Flow<Boolean> =
|
||||
observeIsDozingUseCase().map { !it }.distinctUntilChanged()
|
||||
/** An observable for the alpha level for the entire bottom area. */
|
||||
val alpha: Flow<Float> = observeBottomAreaAlphaUseCase().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> =
|
||||
observeClockPositionUseCase().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 observeDozeAmountUseCase()
|
||||
.map { dozeAmount ->
|
||||
dozeAmount *
|
||||
(burnInHelperWrapper.burnInOffset(
|
||||
/* amplitude = */ defaultBurnInOffset * 2,
|
||||
/* xAxis= */ false,
|
||||
) - defaultBurnInOffset)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun button(
|
||||
position: KeyguardQuickAffordancePosition
|
||||
): Flow<KeyguardQuickAffordanceViewModel> {
|
||||
return observeQuickAffordanceUseCase(position)
|
||||
.map { model -> model.toViewModel() }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun KeyguardQuickAffordanceModel.toViewModel(): KeyguardQuickAffordanceViewModel {
|
||||
return when (this) {
|
||||
is KeyguardQuickAffordanceModel.Visible ->
|
||||
KeyguardQuickAffordanceViewModel(
|
||||
configKey = configKey,
|
||||
isVisible = true,
|
||||
icon = icon,
|
||||
contentDescriptionResourceId = contentDescriptionResourceId,
|
||||
onClicked = { parameters ->
|
||||
onQuickAffordanceClickedUseCase(
|
||||
configKey = parameters.configKey,
|
||||
animationController = parameters.animationController,
|
||||
)
|
||||
},
|
||||
)
|
||||
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.StringRes
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
||||
import com.android.systemui.containeddrawable.ContainedDrawable
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/** Models the UI state of a keyguard quick affordance button. */
|
||||
data class KeyguardQuickAffordanceViewModel(
|
||||
val configKey: KClass<*>? = null,
|
||||
val isVisible: Boolean = false,
|
||||
val icon: ContainedDrawable = ContainedDrawable.WithResource(0),
|
||||
@StringRes val contentDescriptionResourceId: Int = 0,
|
||||
val onClicked: (OnClickedParameters) -> Unit = {},
|
||||
) {
|
||||
data class OnClickedParameters(
|
||||
val configKey: KClass<*>,
|
||||
val animationController: ActivityLaunchAnimator.Controller?,
|
||||
)
|
||||
}
|
||||
@@ -128,6 +128,10 @@ import com.android.systemui.flags.Flags;
|
||||
import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
|
||||
import com.android.systemui.fragments.FragmentService;
|
||||
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
|
||||
import com.android.systemui.keyguard.domain.usecase.SetClockPositionUseCase;
|
||||
import com.android.systemui.keyguard.domain.usecase.SetKeyguardBottomAreaAlphaUseCase;
|
||||
import com.android.systemui.keyguard.domain.usecase.SetKeyguardBottomAreaAnimateDozingTransitionsUseCase;
|
||||
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
|
||||
import com.android.systemui.media.KeyguardMediaController;
|
||||
import com.android.systemui.media.MediaDataManager;
|
||||
import com.android.systemui.media.MediaHierarchyManager;
|
||||
@@ -702,6 +706,12 @@ public final class NotificationPanelViewController extends PanelViewController {
|
||||
};
|
||||
|
||||
private final CameraGestureHelper mCameraGestureHelper;
|
||||
private final Provider<KeyguardBottomAreaViewModel> mKeyguardBottomAreaViewModelProvider;
|
||||
private final Provider<SetClockPositionUseCase> mSetClockPositionUseCaseProvider;
|
||||
private final Provider<SetKeyguardBottomAreaAlphaUseCase>
|
||||
mSetKeyguardBottomAreaAlphaUseCaseProvider;
|
||||
private final Provider<SetKeyguardBottomAreaAnimateDozingTransitionsUseCase>
|
||||
mSetKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider;
|
||||
|
||||
@Inject
|
||||
public NotificationPanelViewController(NotificationPanelView view,
|
||||
@@ -772,7 +782,12 @@ public final class NotificationPanelViewController extends PanelViewController {
|
||||
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
|
||||
ShadeTransitionController shadeTransitionController,
|
||||
SystemClock systemClock,
|
||||
CameraGestureHelper cameraGestureHelper) {
|
||||
CameraGestureHelper cameraGestureHelper,
|
||||
Provider<KeyguardBottomAreaViewModel> keyguardBottomAreaViewModelProvider,
|
||||
Provider<SetClockPositionUseCase> setClockPositionUseCaseProvider,
|
||||
Provider<SetKeyguardBottomAreaAlphaUseCase> setKeyguardBottomAreaAlphaUseCaseProvider,
|
||||
Provider<SetKeyguardBottomAreaAnimateDozingTransitionsUseCase>
|
||||
setKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider) {
|
||||
super(view,
|
||||
falsingManager,
|
||||
dozeLog,
|
||||
@@ -902,6 +917,7 @@ public final class NotificationPanelViewController extends PanelViewController {
|
||||
|
||||
mQsFrameTranslateController = qsFrameTranslateController;
|
||||
updateUserSwitcherFlags();
|
||||
mKeyguardBottomAreaViewModelProvider = keyguardBottomAreaViewModelProvider;
|
||||
onFinishInflate();
|
||||
keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
|
||||
new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() {
|
||||
@@ -955,6 +971,10 @@ public final class NotificationPanelViewController extends PanelViewController {
|
||||
}
|
||||
});
|
||||
mCameraGestureHelper = cameraGestureHelper;
|
||||
mSetClockPositionUseCaseProvider = setClockPositionUseCaseProvider;
|
||||
mSetKeyguardBottomAreaAlphaUseCaseProvider = setKeyguardBottomAreaAlphaUseCaseProvider;
|
||||
mSetKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider =
|
||||
setKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -1286,11 +1306,17 @@ public final class NotificationPanelViewController extends PanelViewController {
|
||||
}
|
||||
|
||||
private void initBottomArea() {
|
||||
mKeyguardBottomArea.init(
|
||||
mFalsingManager,
|
||||
mQuickAccessWalletController,
|
||||
mControlsComponent,
|
||||
mQRCodeScannerController);
|
||||
if (mFeatureFlags.isEnabled(Flags.MODERN_BOTTOM_AREA)) {
|
||||
mKeyguardBottomArea.init(mKeyguardBottomAreaViewModelProvider.get(), mFalsingManager);
|
||||
} else {
|
||||
// TODO(b/235403546): remove this method call when the new implementation is complete
|
||||
// and these are not needed.
|
||||
mKeyguardBottomArea.init(
|
||||
mFalsingManager,
|
||||
mQuickAccessWalletController,
|
||||
mControlsComponent,
|
||||
mQRCodeScannerController);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -1466,6 +1492,8 @@ public final class NotificationPanelViewController extends PanelViewController {
|
||||
mKeyguardStatusViewController.getClockBottom(mStatusBarHeaderHeightKeyguard),
|
||||
mKeyguardStatusViewController.isClockTopAligned());
|
||||
mClockPositionAlgorithm.run(mClockPositionResult);
|
||||
mSetClockPositionUseCaseProvider.get().invoke(
|
||||
mClockPositionResult.clockX, mClockPositionResult.clockY);
|
||||
boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
|
||||
boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange;
|
||||
mKeyguardStatusViewController.updatePosition(
|
||||
@@ -3232,6 +3260,7 @@ public final class NotificationPanelViewController extends PanelViewController {
|
||||
float alpha = Math.min(expansionAlpha, 1 - computeQsExpansionFraction());
|
||||
alpha *= mBottomAreaShadeAlpha;
|
||||
mKeyguardBottomArea.setComponentAlphas(alpha);
|
||||
mSetKeyguardBottomAreaAlphaUseCaseProvider.get().invoke(alpha);
|
||||
mLockIconViewController.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@@ -3431,6 +3460,7 @@ public final class NotificationPanelViewController extends PanelViewController {
|
||||
|
||||
private void updateDozingVisibilities(boolean animate) {
|
||||
mKeyguardBottomArea.setDozing(mDozing, animate);
|
||||
mSetKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider.get().invoke(animate);
|
||||
if (!mDozing && animate) {
|
||||
mKeyguardStatusBarViewController.animateKeyguardStatusBarIn();
|
||||
}
|
||||
@@ -3733,6 +3763,7 @@ public final class NotificationPanelViewController extends PanelViewController {
|
||||
mDozing = dozing;
|
||||
mNotificationStackScrollLayoutController.setDozing(mDozing, animate, wakeUpTouchLocation);
|
||||
mKeyguardBottomArea.setDozing(mDozing, animate);
|
||||
mSetKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider.get().invoke(animate);
|
||||
mKeyguardStatusBarViewController.setDozing(mDozing);
|
||||
|
||||
if (dozing) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.systemui.statusbar.phone;
|
||||
|
||||
import static com.android.internal.util.Preconditions.checkNotNull;
|
||||
import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE;
|
||||
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
|
||||
import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE;
|
||||
@@ -57,6 +58,8 @@ import com.android.systemui.controls.dagger.ControlsComponent;
|
||||
import com.android.systemui.controls.management.ControlsListingController;
|
||||
import com.android.systemui.controls.ui.ControlsActivity;
|
||||
import com.android.systemui.controls.ui.ControlsUiController;
|
||||
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder;
|
||||
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
|
||||
import com.android.systemui.plugins.ActivityStarter;
|
||||
import com.android.systemui.plugins.FalsingManager;
|
||||
import com.android.systemui.qrcodescanner.controller.QRCodeScannerController;
|
||||
@@ -125,6 +128,9 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
};
|
||||
|
||||
@Nullable private KeyguardBottomAreaViewBinder.Binding mBinding;
|
||||
private boolean mUsesBinder;
|
||||
|
||||
public KeyguardBottomAreaView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
@@ -142,13 +148,36 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
/** Initializes the {@link KeyguardBottomAreaView} with the given dependencies */
|
||||
/**
|
||||
* Initializes the view.
|
||||
*/
|
||||
public void init(
|
||||
final KeyguardBottomAreaViewModel viewModel,
|
||||
final FalsingManager falsingManager) {
|
||||
Log.i(TAG, System.identityHashCode(this) + " initialized with a binder");
|
||||
mUsesBinder = true;
|
||||
mBinding = KeyguardBottomAreaViewBinder.bind(this, viewModel, falsingManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the {@link KeyguardBottomAreaView} with the given dependencies
|
||||
*
|
||||
* @deprecated Use
|
||||
* {@link #init(KeyguardBottomAreaViewModel, FalsingManager)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public void init(
|
||||
FalsingManager falsingManager,
|
||||
QuickAccessWalletController controller,
|
||||
ControlsComponent controlsComponent,
|
||||
QRCodeScannerController qrCodeScannerController) {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "initialized without a binder");
|
||||
mFalsingManager = falsingManager;
|
||||
|
||||
mQuickAccessWalletController = controller;
|
||||
mQuickAccessWalletController.setupWalletChangeObservers(
|
||||
mCardRetriever, WALLET_PREFERENCE_CHANGE, DEFAULT_PAYMENT_APP_CHANGE);
|
||||
@@ -174,6 +203,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
* another {@link KeyguardBottomAreaView}
|
||||
*/
|
||||
public void initFrom(KeyguardBottomAreaView oldBottomArea) {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if it exists, continue to use the original ambient indication container
|
||||
// instead of the newly inflated one
|
||||
if (mAmbientIndicationArea != null) {
|
||||
@@ -201,6 +234,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
mOverlayContainer = findViewById(R.id.overlay_container);
|
||||
mWalletButton = findViewById(R.id.wallet_button);
|
||||
mQRCodeScannerButton = findViewById(R.id.qr_code_scanner_button);
|
||||
@@ -229,6 +266,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
final IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
|
||||
mKeyguardStateController.addCallback(mKeyguardStateCallback);
|
||||
@@ -237,6 +278,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
mKeyguardStateController.removeCallback(mKeyguardStateCallback);
|
||||
|
||||
if (mQuickAccessWalletController != null) {
|
||||
@@ -259,6 +304,13 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
@Override
|
||||
protected void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
if (mUsesBinder) {
|
||||
if (mBinding != null) {
|
||||
mBinding.onConfigurationChanged();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
mIndicationBottomMargin = getResources().getDimensionPixelSize(
|
||||
R.dimen.keyguard_indication_margin_bottom);
|
||||
mBurnInYOffset = getResources().getDimensionPixelSize(
|
||||
@@ -301,6 +353,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void updateWalletVisibility() {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mDozing
|
||||
|| mQuickAccessWalletController == null
|
||||
|| !mQuickAccessWalletController.isWalletEnabled()
|
||||
@@ -318,6 +374,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void updateControlsVisibility() {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mControlsComponent == null) return;
|
||||
|
||||
mControlsButton.setImageResource(mControlsComponent.getTileImageId());
|
||||
@@ -344,6 +404,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
|
||||
public void setDarkAmount(float darkAmount) {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (darkAmount == mDarkAmount) {
|
||||
return;
|
||||
}
|
||||
@@ -355,6 +419,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
* Returns a list of animators to use to animate the indication areas.
|
||||
*/
|
||||
public List<ViewPropertyAnimator> getIndicationAreaAnimators() {
|
||||
if (mUsesBinder) {
|
||||
return checkNotNull(mBinding).getIndicationAreaAnimators();
|
||||
}
|
||||
|
||||
List<ViewPropertyAnimator> animators =
|
||||
new ArrayList<>(mAmbientIndicationArea != null ? 2 : 1);
|
||||
animators.add(mIndicationArea.animate());
|
||||
@@ -394,6 +462,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
|
||||
public void setDozing(boolean dozing, boolean animate) {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
mDozing = dozing;
|
||||
|
||||
updateWalletVisibility();
|
||||
@@ -411,6 +483,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
|
||||
public void dozeTimeTick() {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
int burnInYOffset = getBurnInOffset(mBurnInYOffset * 2, false /* xAxis */)
|
||||
- mBurnInYOffset;
|
||||
mIndicationArea.setTranslationY(burnInYOffset * mDarkAmount);
|
||||
@@ -420,6 +496,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
|
||||
public void setAntiBurnInOffsetX(int burnInXOffset) {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mBurnInXOffset == burnInXOffset) {
|
||||
return;
|
||||
}
|
||||
@@ -435,6 +515,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
* action buttons. Does not set the alpha of the lock icon.
|
||||
*/
|
||||
public void setComponentAlphas(float alpha) {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
setImportantForAccessibility(
|
||||
alpha == 0f
|
||||
? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
||||
@@ -461,6 +545,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void updateQRCodeButtonVisibility() {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mQuickAccessWalletController != null
|
||||
&& mQuickAccessWalletController.isWalletEnabled()) {
|
||||
// Don't enable if quick access wallet is enabled
|
||||
@@ -481,6 +569,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void onQRCodeScannerClicked(View view) {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = mQRCodeScannerController.getIntent();
|
||||
if (intent != null) {
|
||||
try {
|
||||
@@ -500,6 +592,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void updateAffordanceColors() {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
int iconColor = Utils.getColorAttrDefaultColor(
|
||||
mContext,
|
||||
com.android.internal.R.attr.textColorPrimary);
|
||||
@@ -516,6 +612,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void onWalletClick(View v) {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// More coming here; need to inform the user about how to proceed
|
||||
if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
return;
|
||||
@@ -531,6 +631,10 @@ public class KeyguardBottomAreaView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void onControlsClick(View v) {
|
||||
if (mUsesBinder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
/*
|
||||
* 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 android.content.Intent
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
||||
import com.android.systemui.containeddrawable.ContainedDrawable
|
||||
import com.android.systemui.doze.util.BurnInHelperWrapper
|
||||
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
|
||||
import com.android.systemui.keyguard.data.repository.FakeKeyguardQuickAffordanceConfig
|
||||
import com.android.systemui.keyguard.data.repository.FakeKeyguardQuickAffordanceConfigs
|
||||
import com.android.systemui.keyguard.data.repository.FakeKeyguardQuickAffordanceRepository
|
||||
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
|
||||
import com.android.systemui.keyguard.domain.usecase.FakeLaunchKeyguardQuickAffordanceUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveAnimateBottomAreaTransitionsUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveBottomAreaAlphaUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveClockPositionUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveDozeAmountUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveIsDozingUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.ObserveKeyguardQuickAffordanceUseCase
|
||||
import com.android.systemui.keyguard.domain.usecase.OnKeyguardQuickAffordanceClickedUseCase
|
||||
import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel
|
||||
import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlin.reflect.KClass
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
||||
|
||||
@Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
|
||||
@Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
|
||||
|
||||
private lateinit var underTest: KeyguardBottomAreaViewModel
|
||||
|
||||
private lateinit var affordanceRepository: FakeKeyguardQuickAffordanceRepository
|
||||
private lateinit var repository: FakeKeyguardRepository
|
||||
private lateinit var isDozingUseCase: ObserveIsDozingUseCase
|
||||
private lateinit var dozeAmountUseCase: ObserveDozeAmountUseCase
|
||||
private lateinit var launchQuickAffordanceUseCase: FakeLaunchKeyguardQuickAffordanceUseCase
|
||||
private lateinit var homeControlsQuickAffordanceConfig: FakeKeyguardQuickAffordanceConfig
|
||||
private lateinit var quickAccessWalletAffordanceConfig: FakeKeyguardQuickAffordanceConfig
|
||||
private lateinit var qrCodeScannerAffordanceConfig: FakeKeyguardQuickAffordanceConfig
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
|
||||
.thenReturn(RETURNED_BURN_IN_OFFSET)
|
||||
|
||||
affordanceRepository = FakeKeyguardQuickAffordanceRepository()
|
||||
repository = FakeKeyguardRepository()
|
||||
isDozingUseCase =
|
||||
ObserveIsDozingUseCase(
|
||||
repository = repository,
|
||||
)
|
||||
dozeAmountUseCase =
|
||||
ObserveDozeAmountUseCase(
|
||||
repository = repository,
|
||||
)
|
||||
launchQuickAffordanceUseCase = FakeLaunchKeyguardQuickAffordanceUseCase()
|
||||
homeControlsQuickAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
|
||||
quickAccessWalletAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
|
||||
qrCodeScannerAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
|
||||
|
||||
underTest =
|
||||
KeyguardBottomAreaViewModel(
|
||||
observeQuickAffordanceUseCase =
|
||||
ObserveKeyguardQuickAffordanceUseCase(
|
||||
repository = affordanceRepository,
|
||||
isDozingUseCase = isDozingUseCase,
|
||||
dozeAmountUseCase = dozeAmountUseCase,
|
||||
),
|
||||
onQuickAffordanceClickedUseCase =
|
||||
OnKeyguardQuickAffordanceClickedUseCase(
|
||||
configs =
|
||||
FakeKeyguardQuickAffordanceConfigs(
|
||||
mapOf(
|
||||
KeyguardQuickAffordancePosition.BOTTOM_START to
|
||||
listOf(
|
||||
homeControlsQuickAffordanceConfig,
|
||||
),
|
||||
KeyguardQuickAffordancePosition.BOTTOM_END to
|
||||
listOf(
|
||||
quickAccessWalletAffordanceConfig,
|
||||
qrCodeScannerAffordanceConfig,
|
||||
),
|
||||
),
|
||||
),
|
||||
launchAffordanceUseCase = launchQuickAffordanceUseCase,
|
||||
),
|
||||
observeBottomAreaAlphaUseCase =
|
||||
ObserveBottomAreaAlphaUseCase(
|
||||
repository = repository,
|
||||
),
|
||||
observeIsDozingUseCase = isDozingUseCase,
|
||||
observeAnimateBottomAreaTransitionsUseCase =
|
||||
ObserveAnimateBottomAreaTransitionsUseCase(
|
||||
repository = repository,
|
||||
),
|
||||
observeDozeAmountUseCase =
|
||||
ObserveDozeAmountUseCase(
|
||||
repository = repository,
|
||||
),
|
||||
observeClockPositionUseCase =
|
||||
ObserveClockPositionUseCase(
|
||||
repository = repository,
|
||||
),
|
||||
burnInHelperWrapper = burnInHelperWrapper,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startButton - present and not dozing - visible model - starts activity on click`() =
|
||||
runBlockingTest {
|
||||
var latest: KeyguardQuickAffordanceViewModel? = null
|
||||
val job = underTest.startButton.onEach { latest = it }.launchIn(this)
|
||||
|
||||
repository.setDozing(false)
|
||||
val testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = testConfig,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest,
|
||||
testConfig = testConfig,
|
||||
configKey = configKey,
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `endButton - present and not dozing - visible model - do nothing on click`() =
|
||||
runBlockingTest {
|
||||
var latest: KeyguardQuickAffordanceViewModel? = null
|
||||
val job = underTest.endButton.onEach { latest = it }.launchIn(this)
|
||||
|
||||
repository.setDozing(false)
|
||||
val config =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent =
|
||||
null, // This will cause it to tell the system that the click was handled.
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_END,
|
||||
testConfig = config,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest,
|
||||
testConfig = config,
|
||||
configKey = configKey,
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startButton - not present and not dozing - model is none`() = runBlockingTest {
|
||||
var latest: KeyguardQuickAffordanceViewModel? = null
|
||||
val job = underTest.startButton.onEach { latest = it }.launchIn(this)
|
||||
|
||||
repository.setDozing(false)
|
||||
val config =
|
||||
TestConfig(
|
||||
isVisible = false,
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = config,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest,
|
||||
testConfig = config,
|
||||
configKey = configKey,
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startButton - present but dozing - model is none`() = runBlockingTest {
|
||||
var latest: KeyguardQuickAffordanceViewModel? = null
|
||||
val job = underTest.startButton.onEach { latest = it }.launchIn(this)
|
||||
|
||||
repository.setDozing(true)
|
||||
val config =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
intent = Intent("action"),
|
||||
)
|
||||
val configKey =
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig = config,
|
||||
)
|
||||
|
||||
assertQuickAffordanceViewModel(
|
||||
viewModel = latest,
|
||||
testConfig = TestConfig(isVisible = false),
|
||||
configKey = configKey,
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateButtonReveal() = runBlockingTest {
|
||||
val values = mutableListOf<Boolean>()
|
||||
val job = underTest.animateButtonReveal.onEach(values::add).launchIn(this)
|
||||
|
||||
repository.setAnimateDozingTransitions(true)
|
||||
repository.setAnimateDozingTransitions(false)
|
||||
|
||||
assertThat(values).isEqualTo(listOf(false, true, false))
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isOverlayContainerVisible() = runBlockingTest {
|
||||
val values = mutableListOf<Boolean>()
|
||||
val job = underTest.isOverlayContainerVisible.onEach(values::add).launchIn(this)
|
||||
|
||||
repository.setDozing(true)
|
||||
repository.setDozing(false)
|
||||
|
||||
assertThat(values).isEqualTo(listOf(true, false, true))
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun alpha() = runBlockingTest {
|
||||
val values = mutableListOf<Float>()
|
||||
val job = underTest.alpha.onEach(values::add).launchIn(this)
|
||||
|
||||
repository.setBottomAreaAlpha(0.1f)
|
||||
repository.setBottomAreaAlpha(0.5f)
|
||||
repository.setBottomAreaAlpha(0.2f)
|
||||
repository.setBottomAreaAlpha(0f)
|
||||
|
||||
assertThat(values).isEqualTo(listOf(1f, 0.1f, 0.5f, 0.2f, 0f))
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isIndicationAreaPadded() = runBlockingTest {
|
||||
val values = mutableListOf<Boolean>()
|
||||
val job = underTest.isIndicationAreaPadded.onEach(values::add).launchIn(this)
|
||||
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = true,
|
||||
)
|
||||
)
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_END,
|
||||
testConfig =
|
||||
TestConfig(
|
||||
isVisible = true,
|
||||
icon = mock(),
|
||||
canShowWhileLocked = false,
|
||||
)
|
||||
)
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_START,
|
||||
testConfig =
|
||||
TestConfig(
|
||||
isVisible = false,
|
||||
)
|
||||
)
|
||||
setUpQuickAffordanceModel(
|
||||
position = KeyguardQuickAffordancePosition.BOTTOM_END,
|
||||
testConfig =
|
||||
TestConfig(
|
||||
isVisible = false,
|
||||
)
|
||||
)
|
||||
|
||||
assertThat(values)
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
// Initially, no button is visible so the indication area is not padded.
|
||||
false,
|
||||
// Once we add the first visible button, the indication area becomes padded.
|
||||
// This
|
||||
// continues to be true after we add the second visible button and even after we
|
||||
// make the first button not visible anymore.
|
||||
true,
|
||||
// Once both buttons are not visible, the indication area is, again, not padded.
|
||||
false,
|
||||
)
|
||||
)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun indicationAreaTranslationX() = runBlockingTest {
|
||||
val values = mutableListOf<Float>()
|
||||
val job = underTest.indicationAreaTranslationX.onEach(values::add).launchIn(this)
|
||||
|
||||
repository.setClockPosition(100, 100)
|
||||
repository.setClockPosition(200, 100)
|
||||
repository.setClockPosition(200, 200)
|
||||
repository.setClockPosition(300, 100)
|
||||
|
||||
assertThat(values).isEqualTo(listOf(0f, 100f, 200f, 300f))
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun indicationAreaTranslationY() = runBlockingTest {
|
||||
val values = mutableListOf<Float>()
|
||||
val job =
|
||||
underTest
|
||||
.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET)
|
||||
.onEach(values::add)
|
||||
.launchIn(this)
|
||||
|
||||
val expectedTranslationValues =
|
||||
listOf(
|
||||
-0f, // Negative 0 - apparently there's a difference in floating point arithmetic -
|
||||
// FML
|
||||
setDozeAmountAndCalculateExpectedTranslationY(0.1f),
|
||||
setDozeAmountAndCalculateExpectedTranslationY(0.2f),
|
||||
setDozeAmountAndCalculateExpectedTranslationY(0.5f),
|
||||
setDozeAmountAndCalculateExpectedTranslationY(1f),
|
||||
)
|
||||
|
||||
assertThat(values).isEqualTo(expectedTranslationValues)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
|
||||
repository.setDozeAmount(dozeAmount)
|
||||
return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
|
||||
}
|
||||
|
||||
private suspend fun setUpQuickAffordanceModel(
|
||||
position: KeyguardQuickAffordancePosition,
|
||||
testConfig: TestConfig,
|
||||
): KClass<*> {
|
||||
val config =
|
||||
when (position) {
|
||||
KeyguardQuickAffordancePosition.BOTTOM_START -> homeControlsQuickAffordanceConfig
|
||||
KeyguardQuickAffordancePosition.BOTTOM_END -> quickAccessWalletAffordanceConfig
|
||||
}
|
||||
|
||||
affordanceRepository.setModel(
|
||||
position = position,
|
||||
model =
|
||||
if (testConfig.isVisible) {
|
||||
if (testConfig.intent != null) {
|
||||
config.onClickedResult =
|
||||
KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
|
||||
intent = testConfig.intent,
|
||||
canShowWhileLocked = testConfig.canShowWhileLocked,
|
||||
)
|
||||
}
|
||||
KeyguardQuickAffordanceModel.Visible(
|
||||
configKey = config::class,
|
||||
icon = testConfig.icon ?: error("Icon is unexpectedly null!"),
|
||||
contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
|
||||
)
|
||||
} else {
|
||||
KeyguardQuickAffordanceModel.Hidden
|
||||
}
|
||||
)
|
||||
return config::class
|
||||
}
|
||||
|
||||
private fun assertQuickAffordanceViewModel(
|
||||
viewModel: KeyguardQuickAffordanceViewModel?,
|
||||
testConfig: TestConfig,
|
||||
configKey: KClass<*>,
|
||||
) {
|
||||
checkNotNull(viewModel)
|
||||
assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
|
||||
if (testConfig.isVisible) {
|
||||
assertThat(viewModel.icon).isEqualTo(testConfig.icon)
|
||||
viewModel.onClicked.invoke(
|
||||
KeyguardQuickAffordanceViewModel.OnClickedParameters(
|
||||
configKey = configKey,
|
||||
animationController = animationController,
|
||||
)
|
||||
)
|
||||
testConfig.intent?.let { intent ->
|
||||
assertThat(launchQuickAffordanceUseCase.invocations)
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
FakeLaunchKeyguardQuickAffordanceUseCase.Invocation(
|
||||
intent = intent,
|
||||
canShowWhileLocked = testConfig.canShowWhileLocked,
|
||||
animationController = animationController,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
?: run { assertThat(launchQuickAffordanceUseCase.invocations).isEmpty() }
|
||||
} else {
|
||||
assertThat(viewModel.isVisible).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
private data class TestConfig(
|
||||
val isVisible: Boolean,
|
||||
val icon: ContainedDrawable? = null,
|
||||
val canShowWhileLocked: Boolean = false,
|
||||
val intent: Intent? = null,
|
||||
) {
|
||||
init {
|
||||
check(!isVisible || icon != null) { "Must supply non-null icon if visible!" }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_BURN_IN_OFFSET = 5
|
||||
private const val RETURNED_BURN_IN_OFFSET = 3
|
||||
private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,10 @@ import com.android.systemui.flags.FeatureFlags;
|
||||
import com.android.systemui.fragments.FragmentHostManager;
|
||||
import com.android.systemui.fragments.FragmentService;
|
||||
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
|
||||
import com.android.systemui.keyguard.domain.usecase.SetClockPositionUseCase;
|
||||
import com.android.systemui.keyguard.domain.usecase.SetKeyguardBottomAreaAlphaUseCase;
|
||||
import com.android.systemui.keyguard.domain.usecase.SetKeyguardBottomAreaAnimateDozingTransitionsUseCase;
|
||||
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
|
||||
import com.android.systemui.media.KeyguardMediaController;
|
||||
import com.android.systemui.media.MediaDataManager;
|
||||
import com.android.systemui.media.MediaHierarchyManager;
|
||||
@@ -373,6 +377,11 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
|
||||
private ViewParent mViewParent;
|
||||
@Mock
|
||||
private ViewTreeObserver mViewTreeObserver;
|
||||
@Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
|
||||
@Mock private SetClockPositionUseCase mSetClockPositionUseCase;
|
||||
@Mock private SetKeyguardBottomAreaAlphaUseCase mSetKeyguardBottomAreaAlphaUseCase;
|
||||
@Mock private SetKeyguardBottomAreaAnimateDozingTransitionsUseCase
|
||||
mSetKeyguardBottomAreaAnimateDozingTransitionsUseCase;
|
||||
private NotificationPanelViewController.PanelEventsEmitter mPanelEventsEmitter;
|
||||
private Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty();
|
||||
private SysuiStatusBarStateController mStatusBarStateController;
|
||||
@@ -564,7 +573,11 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
|
||||
mUnlockedScreenOffAnimationController,
|
||||
mShadeTransitionController,
|
||||
mSystemClock,
|
||||
mock(CameraGestureHelper.class));
|
||||
mock(CameraGestureHelper.class),
|
||||
() -> mKeyguardBottomAreaViewModel,
|
||||
() -> mSetClockPositionUseCase,
|
||||
() -> mSetKeyguardBottomAreaAlphaUseCase,
|
||||
() -> mSetKeyguardBottomAreaAnimateDozingTransitionsUseCase);
|
||||
mNotificationPanelViewController.initDependencies(
|
||||
mCentralSurfaces,
|
||||
() -> {},
|
||||
|
||||
Reference in New Issue
Block a user