diff options
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, () -> {}, |