summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/layout/keyguard_bottom_area.xml24
-rw-r--r--packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt307
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt114
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java43
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java106
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt467
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java15
9 files changed, 1131 insertions, 8 deletions
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index 12dfa1042dd7..0ca19d98a097 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -60,6 +60,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"
android:layout_width="@dimen/keyguard_affordance_fixed_width"
diff --git a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt
new file mode 100644
index 000000000000..d853e04fed90
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt
@@ -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)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
new file mode 100644
index 000000000000..04d30bfb00f7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -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,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
new file mode 100644
index 000000000000..4b69a81c8996
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -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()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
new file mode 100644
index 000000000000..2417998784e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -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?,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 24448bb0ed2e..4e74540ff761 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -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;
@@ -697,6 +701,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,
@@ -767,7 +777,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,
@@ -897,6 +912,7 @@ public final class NotificationPanelViewController extends PanelViewController {
mQsFrameTranslateController = qsFrameTranslateController;
updateUserSwitcherFlags();
+ mKeyguardBottomAreaViewModelProvider = keyguardBottomAreaViewModelProvider;
onFinishInflate();
keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() {
@@ -950,6 +966,10 @@ public final class NotificationPanelViewController extends PanelViewController {
}
});
mCameraGestureHelper = cameraGestureHelper;
+ mSetClockPositionUseCaseProvider = setClockPositionUseCaseProvider;
+ mSetKeyguardBottomAreaAlphaUseCaseProvider = setKeyguardBottomAreaAlphaUseCaseProvider;
+ mSetKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider =
+ setKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider;
}
@VisibleForTesting
@@ -1274,11 +1294,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
@@ -1454,6 +1480,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(
@@ -3220,6 +3248,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);
}
@@ -3419,6 +3448,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();
}
@@ -3721,6 +3751,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) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index 43a5451f4bb6..dc77d10486a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -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;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
new file mode 100644
index 000000000000..cb9cbbaa169f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -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
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index e2673bb74084..c9405c890278 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -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,
() -> {},