From a3a7fabf3bd241a7ba46bd35903d0d61181a1355 Mon Sep 17 00:00:00 2001 From: Brad Hinegardner Date: Wed, 5 Jul 2023 19:22:51 +0000 Subject: [PATCH] Split up KeyguardBottomArea This splits the KeyguardBottomArea into many pieces: - Left shortcut - Right shortcut - Ambient indication container - Customize Lockscreen button Each of the elements above are now able to be independently laid out. The alpha of the entire KeyguardRootView can now be easily modified, rather than having to adjust the alpha of each sub-element individually. Fixes: 278057014 Test: atest KeyguardQuickAffordancesCombinedViewModelTest.kt Test: atest KeyguardRootViewModelTest.kt Change-Id: I098174809144cdf09dd71665ab724f8c508ae9e9 --- .../res/layout/super_notification_shade.xml | 10 +- packages/SystemUI/res/values/dimens.xml | 6 + .../keyguard/KeyguardViewConfigurator.kt | 151 +++- .../data/repository/KeyguardRepository.kt | 43 ++ .../domain/interactor/KeyguardInteractor.kt | 46 +- .../KeyguardQuickAffordanceInteractor.kt | 5 +- .../model/KeyguardRootViewVisibilityState.kt | 31 + ...KeyguardAmbientIndicationAreaViewBinder.kt | 127 ++++ .../ui/binder/KeyguardBottomAreaViewBinder.kt | 24 +- .../ui/binder/KeyguardIndicationAreaBinder.kt | 34 +- .../KeyguardQuickAffordanceViewBinder.kt | 315 ++++++++ .../ui/binder/KeyguardRootViewBinder.kt | 87 ++- .../ui/binder/KeyguardSettingsViewBinder.kt | 131 ++++ .../ui/preview/KeyguardPreviewRenderer.kt | 90 ++- .../keyguard/ui/view/KeyguardRootView.kt | 70 ++ .../ui/view/layout/DefaultLockscreenLayout.kt | 144 +++- .../ui/view/layout/KeyguardLayoutManager.kt | 10 +- .../ui/view/layout/LockscreenLayoutModule.kt | 6 + .../ShortcutsBesideUdfpsLockscreenLayout.kt | 305 ++++++++ .../KeyguardAmbientIndicationViewModel.kt | 51 ++ .../KeyguardIndicationAreaViewModel.kt | 23 +- ...yguardQuickAffordancesCombinedViewModel.kt | 174 +++++ .../ui/viewmodel/KeyguardRootViewModel.kt | 75 ++ .../NotificationPanelViewController.java | 122 +++- .../statusbar/phone/KeyguardBottomAreaView.kt | 6 + .../KeyguardQuickAffordanceInteractorTest.kt | 18 - .../layout/DefaultLockscreenLayoutTest.kt | 5 +- .../KeyguardIndicationAreaViewModelTest.kt | 5 + ...rdQuickAffordancesCombinedViewModelTest.kt | 681 ++++++++++++++++++ .../ui/viewmodel/KeyguardRootViewModelTest.kt | 107 +++ ...tificationPanelViewControllerBaseTest.java | 5 +- .../data/repository/FakeKeyguardRepository.kt | 31 + 32 files changed, 2821 insertions(+), 117 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardRootViewVisibilityState.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardAmbientIndicationAreaViewBinder.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/ShortcutsBesideUdfpsLockscreenLayout.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardAmbientIndicationViewModel.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml index a33625212d340..d4b73a4e3de07 100644 --- a/packages/SystemUI/res/layout/super_notification_shade.xml +++ b/packages/SystemUI/res/layout/super_notification_shade.xml @@ -65,17 +65,17 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + - - 24dp 12dp + + 24dp + 24dp + 8dp + 12dp + 8dp diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 03a270ebea68f..4495943c72c91 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard +import android.content.res.Configuration import android.view.View import android.view.ViewGroup import com.android.systemui.CoreStartable @@ -24,18 +25,29 @@ import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.ui.binder.KeyguardAmbientIndicationAreaViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder +import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder +import com.android.systemui.keyguard.ui.binder.KeyguardSettingsViewBinder import com.android.systemui.keyguard.ui.view.KeyguardRootView +import com.android.systemui.keyguard.ui.viewmodel.KeyguardAmbientIndicationViewModel import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManager import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManagerCommandListener import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import javax.inject.Inject import kotlinx.coroutines.DisposableHandle @@ -49,26 +61,44 @@ class KeyguardViewConfigurator constructor( private val keyguardRootView: KeyguardRootView, private val sharedNotificationContainer: SharedNotificationContainer, + private val keyguardRootViewModel: KeyguardRootViewModel, private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel, private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, + private val keyguardAmbientIndicationViewModel: KeyguardAmbientIndicationViewModel, private val notificationShadeWindowView: NotificationShadeWindowView, private val featureFlags: FeatureFlags, private val indicationController: KeyguardIndicationController, private val keyguardLayoutManager: KeyguardLayoutManager, private val keyguardLayoutManagerCommandListener: KeyguardLayoutManagerCommandListener, + private val keyguardQuickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel, + private val falsingManager: FalsingManager, + private val vibratorHelper: VibratorHelper, + private val keyguardStateController: KeyguardStateController, + private val keyguardSettingsMenuViewModel: KeyguardSettingsMenuViewModel, + private val activityStarter: ActivityStarter, private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, private val chipbarCoordinator: ChipbarCoordinator, ) : CoreStartable { + private var rootViewHandle: DisposableHandle? = null private var indicationAreaHandle: DisposableHandle? = null + private var leftShortcutHandle: KeyguardQuickAffordanceViewBinder.Binding? = null + private var rightShortcutHandle: KeyguardQuickAffordanceViewBinder.Binding? = null + private var ambientIndicationAreaHandle: KeyguardAmbientIndicationAreaViewBinder.Binding? = null + private var settingsPopupMenuHandle: DisposableHandle? = null override fun start() { bindKeyguardRootView() val notificationPanel = notificationShadeWindowView.requireViewById(R.id.notification_panel) as ViewGroup - bindIndicationArea(notificationPanel) + unbindKeyguardBottomArea(notificationPanel) + bindIndicationArea() bindLockIconView(notificationPanel) setupNotificationStackScrollLayout(notificationPanel) + bindLeftShortcut() + bindRightShortcut() + bindAmbientIndicationArea() + bindSettingsPopupMenu() keyguardLayoutManager.layoutViews() keyguardLayoutManagerCommandListener.start() @@ -90,16 +120,21 @@ constructor( } } - fun bindIndicationArea(legacyParent: ViewGroup) { + override fun onConfigurationChanged(newConfig: Configuration?) { + super.onConfigurationChanged(newConfig) + leftShortcutHandle?.onConfigurationChanged() + rightShortcutHandle?.onConfigurationChanged() + ambientIndicationAreaHandle?.onConfigurationChanged() + + keyguardLayoutManager.layoutViews() + } + + fun bindIndicationArea() { indicationAreaHandle?.dispose() // At startup, 2 views with the ID `R.id.keyguard_indication_area` will be available. // Disable one of them - if (featureFlags.isEnabled(Flags.MIGRATE_INDICATION_AREA)) { - legacyParent.findViewById(R.id.keyguard_indication_area)?.let { - legacyParent.removeView(it) - } - } else { + if (!featureFlags.isEnabled(Flags.MIGRATE_INDICATION_AREA)) { keyguardRootView.findViewById(R.id.keyguard_indication_area)?.let { keyguardRootView.removeView(it) } @@ -109,10 +144,24 @@ constructor( KeyguardIndicationAreaBinder.bind( notificationShadeWindowView, keyguardIndicationAreaViewModel, - indicationController + keyguardRootViewModel, + indicationController, + featureFlags, ) } + private fun bindKeyguardRootView() { + rootViewHandle?.dispose() + rootViewHandle = KeyguardRootViewBinder.bind( + keyguardRootView, + keyguardRootViewModel, + featureFlags, + occludingAppDeviceEntryMessageViewModel, + chipbarCoordinator, + keyguardStateController, + ) + } + private fun bindLockIconView(legacyParent: ViewGroup) { if (featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) { legacyParent.requireViewById(R.id.lock_icon_view).let { @@ -125,12 +174,84 @@ constructor( } } - private fun bindKeyguardRootView() { - KeyguardRootViewBinder.bind( - keyguardRootView, - featureFlags, - occludingAppDeviceEntryMessageViewModel, - chipbarCoordinator, - ) + private fun bindAmbientIndicationArea() { + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + ambientIndicationAreaHandle?.destroy() + ambientIndicationAreaHandle = + KeyguardAmbientIndicationAreaViewBinder.bind( + notificationShadeWindowView, + keyguardAmbientIndicationViewModel, + keyguardRootViewModel, + ) + } else { + keyguardRootView.findViewById(R.id.ambient_indication_container)?.let { + keyguardRootView.removeView(it) + } + } + } + + private fun bindSettingsPopupMenu() { + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + settingsPopupMenuHandle?.dispose() + settingsPopupMenuHandle = + KeyguardSettingsViewBinder.bind( + keyguardRootView, + keyguardSettingsMenuViewModel, + vibratorHelper, + activityStarter, + ) + } else { + keyguardRootView.findViewById(R.id.keyguard_settings_button)?.let { + keyguardRootView.removeView(it) + } + } + } + + private fun unbindKeyguardBottomArea(legacyParent: ViewGroup) { + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + legacyParent.requireViewById(R.id.keyguard_bottom_area).let { + legacyParent.removeView(it) + } + } + } + + private fun bindLeftShortcut() { + leftShortcutHandle?.destroy() + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + leftShortcutHandle = + KeyguardQuickAffordanceViewBinder.bind( + keyguardRootView.requireViewById(R.id.start_button), + keyguardQuickAffordancesCombinedViewModel.startButton, + keyguardRootViewModel.alpha, + falsingManager, + vibratorHelper, + ) { + indicationController.showTransientIndication(it) + } + } else { + keyguardRootView.findViewById(R.id.start_button)?.let { + keyguardRootView.removeView(it) + } + } + } + + private fun bindRightShortcut() { + rightShortcutHandle?.destroy() + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + rightShortcutHandle = + KeyguardQuickAffordanceViewBinder.bind( + keyguardRootView.requireViewById(R.id.end_button), + keyguardQuickAffordancesCombinedViewModel.endButton, + keyguardRootViewModel.alpha, + falsingManager, + vibratorHelper, + ) { + indicationController.showTransientIndication(it) + } + } else { + keyguardRootView.findViewById(R.id.end_button)?.let { + keyguardRootView.removeView(it) + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 7475c42512113..9ee990243b96d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -37,6 +37,7 @@ import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel +import com.android.systemui.keyguard.shared.model.KeyguardRootViewVisibilityState import com.android.systemui.keyguard.shared.model.ScreenModel import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.WakefulnessModel @@ -76,6 +77,8 @@ interface KeyguardRepository { */ val bottomAreaAlpha: StateFlow + val keyguardAlpha: StateFlow + /** * Observable of the relative offset of the lock-screen clock from its natural position on the * screen. @@ -170,6 +173,9 @@ interface KeyguardRepository { /** Whether quick settings or quick-quick settings is visible. */ val isQuickSettingsVisible: Flow + /** Represents the current state of the KeyguardRootView visibility */ + val keyguardRootViewVisibility: Flow + /** Receive an event for doze time tick */ val dozeTimeTick: Flow @@ -194,8 +200,14 @@ interface KeyguardRepository { fun setAnimateDozingTransitions(animate: Boolean) /** Sets the current amount of alpha that should be used for rendering the bottom area. */ + @Deprecated("Deprecated as part of b/278057014") fun setBottomAreaAlpha(alpha: Float) + /** Sets the current amount of alpha that should be used for rendering the keyguard. */ + fun setKeyguardAlpha(alpha: Float) + + fun setKeyguardVisibility(statusBarState: Int, goingToFullShade: Boolean, occlusionTransitionRunning: Boolean) + /** * Sets the relative offset of the lock-screen clock from its natural position on the screen. */ @@ -244,6 +256,9 @@ constructor( private val _bottomAreaAlpha = MutableStateFlow(1f) override val bottomAreaAlpha = _bottomAreaAlpha.asStateFlow() + private val _keyguardAlpha = MutableStateFlow(1f) + override val keyguardAlpha = _keyguardAlpha.asStateFlow() + private val _clockPosition = MutableStateFlow(Position(0, 0)) override val clockPosition = _clockPosition.asStateFlow() @@ -686,6 +701,17 @@ constructor( private val _isActiveDreamLockscreenHosted = MutableStateFlow(false) override val isActiveDreamLockscreenHosted = _isActiveDreamLockscreenHosted.asStateFlow() + private val _keyguardRootViewVisibility = + MutableStateFlow( + KeyguardRootViewVisibilityState( + com.android.systemui.statusbar.StatusBarState.SHADE, + goingToFullShade = false, + occlusionTransitionRunning = false, + ) + ) + override val keyguardRootViewVisibility: Flow = + _keyguardRootViewVisibility.asStateFlow() + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } @@ -694,6 +720,23 @@ constructor( _bottomAreaAlpha.value = alpha } + override fun setKeyguardAlpha(alpha: Float) { + _keyguardAlpha.value = alpha + } + + override fun setKeyguardVisibility( + statusBarState: Int, + goingToFullShade: Boolean, + occlusionTransitionRunning: Boolean + ) { + _keyguardRootViewVisibility.value = + KeyguardRootViewVisibilityState( + statusBarState, + goingToFullShade, + occlusionTransitionRunning + ) + } + override fun setClockPosition(x: Int, y: Int) { _clockPosition.value = Position(x, y) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 1553525915d5a..cc159168009e8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -22,6 +22,7 @@ import android.graphics.Point import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.common.shared.model.Position import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags @@ -32,14 +33,17 @@ import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.DozeTransitionModel +import com.android.systemui.keyguard.shared.model.KeyguardRootViewVisibilityState import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.statusbar.CommandQueue import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -63,6 +67,18 @@ constructor( bouncerRepository: KeyguardBouncerRepository, configurationRepository: ConfigurationRepository, ) { + + data class PreviewMode( + val isInPreviewMode: Boolean = false, + val shouldHighlightSelectedAffordance: Boolean = false, + ) + + /** + * Whether this view-model instance is powering the preview experience that renders exclusively + * in the wallpaper picker application. This should _always_ be `false` for the real lock screen + * experience. + */ + val previewMode = MutableStateFlow(PreviewMode()) /** * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at * all. @@ -142,8 +158,6 @@ constructor( val alternateBouncerShowing: Flow = bouncerRepository.alternateBouncerVisible /** Observable for the [StatusBarState] */ val statusBarState: Flow = repository.statusBarState - /** Whether or not quick settings or quick quick settings are showing. */ - val isQuickSettingsVisible: Flow = repository.isQuickSettingsVisible /** * Observable for [BiometricUnlockModel] when biometrics like face or any fingerprint (rear, * side, under display) is used to unlock the device. @@ -182,6 +196,18 @@ constructor( /** Notifies when a new configuration is set */ val configurationChange: Flow = configurationRepository.onAnyConfigurationChange + /** Represents the current state of the KeyguardRootView visibility */ + val keyguardRootViewVisibilityState: Flow = + repository.keyguardRootViewVisibility + + /** The position of the keyguard clock. */ + val clockPosition: Flow = repository.clockPosition + + val keyguardAlpha: Flow = repository.keyguardAlpha + + /** Whether to animate the next doze mode transition. */ + val animateDozingTransitions: Flow = repository.animateBottomAreaDozingTransitions + fun dozeTransitionTo(vararg states: DozeStateModel): Flow { return dozeTransitionModel.filter { states.contains(it.to) } } @@ -211,6 +237,22 @@ constructor( repository.setQuickSettingsVisible(isVisible) } + fun setKeyguardRootVisibility(statusBarState: Int, goingToFullShade: Boolean, isOcclusionTransitionRunning: Boolean) { + repository.setKeyguardVisibility(statusBarState, goingToFullShade, isOcclusionTransitionRunning) + } + + fun setClockPosition(x: Int, y: Int) { + repository.setClockPosition(x, y) + } + + fun setAlpha(alpha: Float) { + repository.setKeyguardAlpha(alpha) + } + + fun setAnimateDozingTransitions(animate: Boolean) { + repository.setAnimateDozingTransitions(animate) + } + companion object { private const val TAG = "KeyguardInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 40e0604ae1b34..a486843813457 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -101,10 +101,9 @@ constructor( quickAffordanceAlwaysVisible(position), keyguardInteractor.isDozing, keyguardInteractor.isKeyguardShowing, - keyguardInteractor.isQuickSettingsVisible, biometricSettingsRepository.isCurrentUserInLockdown, - ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown -> - if (!isDozing && isKeyguardShowing && !isQuickSettingsVisible && !isUserInLockdown) { + ) { affordance, isDozing, isKeyguardShowing, isUserInLockdown -> + if (!isDozing && isKeyguardShowing && !isUserInLockdown) { affordance } else { KeyguardQuickAffordanceModel.Hidden diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardRootViewVisibilityState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardRootViewVisibilityState.kt new file mode 100644 index 0000000000000..9a57aefba3293 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardRootViewVisibilityState.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.shared.model + +/** + * Provides a stateful representation of the visibility of the KeyguardRootView + * + * @param statusBarState State of the status bar represented by [StatusBarState] + * @param goingToFullShade Whether status bar is going to full shade + * @param occlusionTransitionRunning Whether the occlusion transition is running in this instant + */ +data class KeyguardRootViewVisibilityState( + val statusBarState: Int, + val goingToFullShade: Boolean, + val occlusionTransitionRunning: Boolean, +) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardAmbientIndicationAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardAmbientIndicationAreaViewBinder.kt new file mode 100644 index 0000000000000..d6883dddba26c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardAmbientIndicationAreaViewBinder.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.binder + +import android.view.View +import android.view.ViewGroup +import android.view.ViewPropertyAnimator +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R +import com.android.systemui.keyguard.ui.viewmodel.KeyguardAmbientIndicationViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +object KeyguardAmbientIndicationAreaViewBinder { + /** + * Defines interface for an object that acts as the binding between the view and its view-model. + * + * Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after + * it is bound. + */ + interface Binding { + /** + * Returns a collection of [ViewPropertyAnimator] instances that can be used to animate the + * indication areas. + */ + fun getIndicationAreaAnimators(): List + + /** Notifies that device configuration has changed. */ + fun onConfigurationChanged() + + /** Destroys this binding, releases resources, and cancels any coroutines. */ + fun destroy() + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun bind( + view: ViewGroup, + viewModel: KeyguardAmbientIndicationViewModel, + keyguardRootViewModel: KeyguardRootViewModel, + ): Binding { + val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container) + val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) + + val disposableHandle = + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + keyguardRootViewModel.alpha.collect { alpha -> + view.importantForAccessibility = + if (alpha == 0f) { + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } else { + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + + ambientIndicationArea?.alpha = alpha + } + } + + launch { + viewModel.indicationAreaTranslationX.collect { translationX -> + ambientIndicationArea?.translationX = translationX + } + } + + launch { + configurationBasedDimensions + .map { it.defaultBurnInPreventionYOffsetPx } + .flatMapLatest { defaultBurnInOffsetY -> + viewModel.indicationAreaTranslationY(defaultBurnInOffsetY) + } + .collect { translationY -> + ambientIndicationArea?.translationY = translationY + } + } + + } + } + + + return object : Binding { + override fun getIndicationAreaAnimators(): List { + return listOf(ambientIndicationArea).mapNotNull { it?.animate() } + } + + override fun onConfigurationChanged() { + configurationBasedDimensions.value = loadFromResources(view) + } + + override fun destroy() { + disposableHandle.dispose() + } + } + } + + private fun loadFromResources(view: View): ConfigurationBasedDimensions { + return ConfigurationBasedDimensions( + defaultBurnInPreventionYOffsetPx = + view.resources.getDimensionPixelOffset(R.dimen.default_burn_in_prevention_offset), + ) + } + + private data class ConfigurationBasedDimensions( + val defaultBurnInPreventionYOffsetPx: Int, + ) +} \ No newline at end of file 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 index db84268f7c585..a0a2abe833155 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt @@ -63,6 +63,7 @@ import kotlinx.coroutines.launch * view-model to be reused for multiple view/view-binder bindings. */ @OptIn(ExperimentalCoroutinesApi::class) +@Deprecated("Deprecated as part of b/278057014") object KeyguardBottomAreaViewBinder { private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L @@ -75,6 +76,8 @@ object KeyguardBottomAreaViewBinder { * Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after * it is bound. */ + //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] + @Deprecated("Deprecated as part of b/278057014") interface Binding { /** * Returns a collection of [ViewPropertyAnimator] instances that can be used to animate the @@ -96,6 +99,7 @@ object KeyguardBottomAreaViewBinder { } /** Binds the view to the view-model, continuing to update the former based on the latter. */ + @Deprecated("Deprecated as part of b/278057014") @SuppressLint("ClickableViewAccessibility") @JvmStatic fun bind( @@ -132,6 +136,8 @@ object KeyguardBottomAreaViewBinder { val disposableHandle = view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { + + //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] launch { viewModel.startButton.collect { buttonModel -> updateButton( @@ -144,6 +150,7 @@ object KeyguardBottomAreaViewBinder { } } + //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] launch { viewModel.endButton.collect { buttonModel -> updateButton( @@ -180,6 +187,7 @@ object KeyguardBottomAreaViewBinder { } } + //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] launch { updateButtonAlpha( view = startButton, @@ -188,6 +196,7 @@ object KeyguardBottomAreaViewBinder { ) } + //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] launch { updateButtonAlpha( view = endButton, @@ -213,6 +222,7 @@ object KeyguardBottomAreaViewBinder { } } + //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] launch { configurationBasedDimensions.collect { dimensions -> startButton.updateLayoutParams { @@ -287,6 +297,8 @@ object KeyguardBottomAreaViewBinder { } } + @Deprecated("Deprecated as part of b/278057014") + // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] @SuppressLint("ClickableViewAccessibility") private fun updateButton( view: ImageView, @@ -394,6 +406,8 @@ object KeyguardBottomAreaViewBinder { view.isSelected = viewModel.isSelected } + @Deprecated("Deprecated as part of b/278057014") + //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] private suspend fun updateButtonAlpha( view: View, viewModel: Flow, @@ -405,6 +419,7 @@ object KeyguardBottomAreaViewBinder { .collect { view.alpha = it } } + @Deprecated("Deprecated as part of b/278057014") private fun View.animateVisibility(visible: Boolean) { animate() .withStartAction { @@ -422,6 +437,8 @@ object KeyguardBottomAreaViewBinder { .start() } + @Deprecated("Deprecated as part of b/278057014") + //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] private class OnLongClickListener( private val falsingManager: FalsingManager?, private val viewModel: KeyguardQuickAffordanceViewModel, @@ -455,9 +472,10 @@ object KeyguardBottomAreaViewBinder { } override fun onLongClickUseDefaultHapticFeedback(view: View?) = false - } + @Deprecated("Deprecated as part of b/278057014") + //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] private class OnClickListener( private val viewModel: KeyguardQuickAffordanceViewModel, private val falsingManager: FalsingManager, @@ -479,6 +497,7 @@ object KeyguardBottomAreaViewBinder { } } + @Deprecated("Deprecated as part of b/278057014") private fun loadFromResources(view: View): ConfigurationBasedDimensions { return ConfigurationBasedDimensions( defaultBurnInPreventionYOffsetPx = @@ -491,6 +510,7 @@ object KeyguardBottomAreaViewBinder { ) } + @Deprecated("Deprecated as part of b/278057014") /** Opens the wallpaper picker screen after the device is unlocked by the user. */ private fun navigateToLockScreenSettings( activityStarter: ActivityStarter, @@ -510,6 +530,8 @@ object KeyguardBottomAreaViewBinder { ) } + @Deprecated("Deprecated as part of b/278057014") + //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] private data class ConfigurationBasedDimensions( val defaultBurnInPreventionYOffsetPx: Int, val buttonSizePx: Size, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index 02e6765fa1d36..a385a0e5754b6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt @@ -23,7 +23,10 @@ import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.R +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.KeyguardIndicationController import kotlinx.coroutines.DisposableHandle @@ -49,7 +52,9 @@ object KeyguardIndicationAreaBinder { fun bind( view: ViewGroup, viewModel: KeyguardIndicationAreaViewModel, + keyguardRootViewModel: KeyguardRootViewModel, indicationController: KeyguardIndicationController, + featureFlags: FeatureFlags, ): DisposableHandle { val indicationArea: ViewGroup = view.requireViewById(R.id.keyguard_indication_area) indicationController.setIndicationArea(indicationArea) @@ -66,15 +71,28 @@ object KeyguardIndicationAreaBinder { view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { - viewModel.alpha.collect { alpha -> - view.importantForAccessibility = - if (alpha == 0f) { - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - } else { - View.IMPORTANT_FOR_ACCESSIBILITY_AUTO - } + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + keyguardRootViewModel.alpha.collect { alpha -> + view.importantForAccessibility = + if (alpha == 0f) { + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } else { + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } - indicationArea.alpha = alpha + indicationArea.alpha = alpha + } + } else { + viewModel.alpha.collect { alpha -> + view.importantForAccessibility = + if (alpha == 0f) { + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } else { + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + + indicationArea.alpha = alpha + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt new file mode 100644 index 0000000000000..63a67913d6cd8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.binder + +import android.annotation.SuppressLint +import android.graphics.drawable.Animatable2 +import android.util.Size +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.app.animation.Interpolators +import com.android.settingslib.Utils +import com.android.systemui.R +import com.android.systemui.animation.Expandable +import com.android.systemui.animation.view.LaunchableImageView +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** + * This is only for a SINGLE Quick affordance + */ +object KeyguardQuickAffordanceViewBinder { + + private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L + private const val SCALE_SELECTED_BUTTON = 1.23f + private const val DIM_ALPHA = 0.3f + + /** + * Defines interface for an object that acts as the binding between the view and its view-model. + * + * Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after + * it is bound. + */ + interface Binding { + /** Notifies that device configuration has changed. */ + fun onConfigurationChanged() + + /** Destroys this binding, releases resources, and cancels any coroutines. */ + fun destroy() + } + + fun bind( + view: LaunchableImageView, + viewModel: Flow, + alpha: Flow, + falsingManager: FalsingManager?, + vibratorHelper: VibratorHelper?, + messageDisplayer: (Int) -> Unit, + ): Binding { + val button = view as ImageView + val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) + val disposableHandle = + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.collect { buttonModel -> + updateButton( + view = button, + viewModel = buttonModel, + falsingManager = falsingManager, + messageDisplayer = messageDisplayer, + vibratorHelper = vibratorHelper, + ) + } + } + + launch { + updateButtonAlpha( + view = button, + viewModel = viewModel, + alphaFlow = alpha, + ) + } + + launch { + configurationBasedDimensions.collect { dimensions -> + button.updateLayoutParams { + width = dimensions.buttonSizePx.width + height = dimensions.buttonSizePx.height + } + } + } + } + } + + return object : Binding { + override fun onConfigurationChanged() { + configurationBasedDimensions.value = loadFromResources(view) + } + + override fun destroy() { + disposableHandle.dispose() + } + } + } + + @SuppressLint("ClickableViewAccessibility") + private fun updateButton( + view: ImageView, + viewModel: KeyguardQuickAffordanceViewModel, + falsingManager: FalsingManager?, + messageDisplayer: (Int) -> Unit, + vibratorHelper: VibratorHelper?, + ) { + if (!viewModel.isVisible) { + view.isInvisible = true + return + } + + if (!view.isVisible) { + view.isVisible = true + if (viewModel.animateReveal) { + view.alpha = 0f + view.translationY = view.height / 2f + view + .animate() + .alpha(1f) + .translationY(0f) + .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) + .setDuration(EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS) + .start() + } + } + + IconViewBinder.bind(viewModel.icon, view) + + (view.drawable as? Animatable2)?.let { animatable -> + (viewModel.icon as? Icon.Resource)?.res?.let { iconResourceId -> + // Always start the animation (we do call stop() below, if we need to skip it). + animatable.start() + + if (view.tag != iconResourceId) { + // Here when we haven't run the animation on a previous update. + // + // Save the resource ID for next time, so we know not to re-animate the same + // animation again. + view.tag = iconResourceId + } else { + // Here when we've already done this animation on a previous update and want to + // skip directly to the final frame of the animation to avoid running it. + // + // By calling stop after start, we go to the final frame of the animation. + animatable.stop() + } + } + } + + view.isActivated = viewModel.isActivated + view.drawable.setTint( + Utils.getColorAttrDefaultColor( + view.context, + if (viewModel.isActivated) { + com.android.internal.R.attr.materialColorOnPrimaryFixed + } else { + com.android.internal.R.attr.materialColorOnSurface + }, + ) + ) + + view.backgroundTintList = + if (!viewModel.isSelected) { + Utils.getColorAttr( + view.context, + if (viewModel.isActivated) { + com.android.internal.R.attr.materialColorPrimaryFixed + } else { + com.android.internal.R.attr.materialColorSurfaceContainerHigh + } + ) + } else { + null + } + view + .animate() + .scaleX(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f) + .scaleY(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f) + .start() + + view.isClickable = viewModel.isClickable + if (viewModel.isClickable) { + if (viewModel.useLongPress) { + val onTouchListener = KeyguardQuickAffordanceOnTouchListener( + view, + viewModel, + messageDisplayer, + vibratorHelper, + falsingManager, + ) + view.setOnTouchListener(onTouchListener) + view.onLongClickListener = + OnLongClickListener(falsingManager, viewModel, vibratorHelper, onTouchListener) + } else { + view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager))) + } + } else { + view.onLongClickListener = null + view.setOnClickListener(null) + view.setOnTouchListener(null) + } + + view.isSelected = viewModel.isSelected + } + + private suspend fun updateButtonAlpha( + view: View, + viewModel: Flow, + alphaFlow: Flow, + ) { + combine(viewModel.map { it.isDimmed }, alphaFlow) { isDimmed, alpha -> + if (isDimmed) DIM_ALPHA else alpha + } + .collect { view.alpha = it } + } + + private fun loadFromResources(view: View): ConfigurationBasedDimensions { + return ConfigurationBasedDimensions( + buttonSizePx = + Size( + view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width), + view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height), + ), + ) + } + + private class OnClickListener( + private val viewModel: KeyguardQuickAffordanceViewModel, + private val falsingManager: FalsingManager, + ) : View.OnClickListener { + override fun onClick(view: View) { + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + return + } + + if (viewModel.configKey != null) { + viewModel.onClicked( + KeyguardQuickAffordanceViewModel.OnClickedParameters( + configKey = viewModel.configKey, + expandable = Expandable.fromView(view), + slotId = viewModel.slotId, + ) + ) + } + } + } + + private class OnLongClickListener( + private val falsingManager: FalsingManager?, + private val viewModel: KeyguardQuickAffordanceViewModel, + private val vibratorHelper: VibratorHelper?, + private val onTouchListener: KeyguardQuickAffordanceOnTouchListener + ) : View.OnLongClickListener { + override fun onLongClick(view: View): Boolean { + if (falsingManager?.isFalseLongTap(FalsingManager.MODERATE_PENALTY) == true) { + return true + } + + if (viewModel.configKey != null) { + viewModel.onClicked( + KeyguardQuickAffordanceViewModel.OnClickedParameters( + configKey = viewModel.configKey, + expandable = Expandable.fromView(view), + slotId = viewModel.slotId, + ) + ) + vibratorHelper?.vibrate( + if (viewModel.isActivated) { + KeyguardBottomAreaVibrations.Activated + } else { + KeyguardBottomAreaVibrations.Deactivated + } + ) + } + + onTouchListener.cancel() + return true + } + + override fun onLongClickUseDefaultHapticFeedback(view: View?) = false + + } + + private data class ConfigurationBasedDimensions( + val buttonSizePx: Size, + ) + +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 1db596b346b38..19f622bab7ed1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -17,20 +17,26 @@ package com.android.systemui.keyguard.ui.binder import android.annotation.DrawableRes +import android.view.View import android.view.ViewGroup import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.app.animation.Interpolators import com.android.systemui.R import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.TintedIcon import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo +import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch @@ -40,31 +46,74 @@ object KeyguardRootViewBinder { @JvmStatic fun bind( view: ViewGroup, + viewModel: KeyguardRootViewModel, featureFlags: FeatureFlags, occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, chipbarCoordinator: ChipbarCoordinator, - ) { - if (featureFlags.isEnabled(Flags.FP_LISTEN_OCCLUDING_APPS)) { + keyguardStateController: KeyguardStateController, + ): DisposableHandle { + val disposableHandle = view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - occludingAppDeviceEntryMessageViewModel.message.collect { biometricMessage - -> - if (biometricMessage?.message != null) { - chipbarCoordinator.displayView( - createChipbarInfo( - biometricMessage.message, - R.drawable.ic_lock, + if (featureFlags.isEnabled(Flags.FP_LISTEN_OCCLUDING_APPS)) { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + occludingAppDeviceEntryMessageViewModel.message.collect { + biometricMessage -> + if (biometricMessage?.message != null) { + chipbarCoordinator.displayView( + createChipbarInfo( + biometricMessage.message, + R.drawable.ic_lock, + ) ) - ) - } else { - chipbarCoordinator.removeView(ID, "occludingAppMsgNull") + } else { + chipbarCoordinator.removeView(ID, "occludingAppMsgNull") + } + } + } + } + } + + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.keyguardRootViewVisibilityState.collect { visibilityState -> + view.animate().cancel() + val goingToFullShade = visibilityState.goingToFullShade + val statusBarState = visibilityState.statusBarState + val isOcclusionTransitionRunning = + visibilityState.occlusionTransitionRunning + if (goingToFullShade) { + view.animate().alpha(0f).setStartDelay( + keyguardStateController.keyguardFadingAwayDelay + ).setDuration( + keyguardStateController.shortenedFadingAwayDuration + ).setInterpolator( + Interpolators.ALPHA_OUT + ).withEndAction { view.visibility = View.GONE }.start() + } else if ( + statusBarState == StatusBarState.KEYGUARD || + statusBarState == StatusBarState.SHADE_LOCKED + ) { + view.visibility = View.VISIBLE + if (!isOcclusionTransitionRunning) { + view.alpha = 1f + } + } else { + view.visibility = View.GONE + } + } + } + + launch { + viewModel.alpha.collect { alpha -> + view.alpha = alpha } } } } } - } + return disposableHandle } /** @@ -73,10 +122,10 @@ object KeyguardRootViewBinder { private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo { return ChipbarInfo( startIcon = - TintedIcon( - Icon.Resource(icon, null), - ChipbarInfo.DEFAULT_ICON_TINT, - ), + TintedIcon( + Icon.Resource(icon, null), + ChipbarInfo.DEFAULT_ICON_TINT, + ), text = Text.Loaded(message), endItem = null, vibrationEffect = null, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt new file mode 100644 index 0000000000000..162c109ee299b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.binder + +import android.content.Intent +import android.view.View +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.view.LaunchableLinearLayout +import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.common.ui.binder.TextViewBinder +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.VibratorHelper +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch + +object KeyguardSettingsViewBinder { + fun bind( + parentView: View, + viewModel: KeyguardSettingsMenuViewModel, + vibratorHelper: VibratorHelper, + activityStarter: ActivityStarter + ): DisposableHandle { + val view = parentView.findViewById(R.id.keyguard_settings_button) + + val disposableHandle = + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.isVisible.distinctUntilChanged().collect { isVisible -> + view.animateVisibility(visible = isVisible) + if (isVisible) { + vibratorHelper.vibrate(KeyguardBottomAreaVibrations.Activated) + view.setOnTouchListener( + KeyguardSettingsButtonOnTouchListener( + view = view, + viewModel = viewModel, + ) + ) + IconViewBinder.bind( + icon = viewModel.icon, + view = view.requireViewById(R.id.icon), + ) + TextViewBinder.bind( + view = view.requireViewById(R.id.text), + viewModel = viewModel.text, + ) + } + } + } + + // activityStarter will only be null when rendering the preview that + // shows up in the Wallpaper Picker app. If we do that, then the + // settings menu should never be visible. + if (activityStarter != null) { + launch { + viewModel.shouldOpenSettings + .filter { it } + .collect { + navigateToLockScreenSettings( + activityStarter = activityStarter, + view = view, + ) + viewModel.onSettingsShown() + } + } + } + } + } + return disposableHandle + } + + /** Opens the wallpaper picker screen after the device is unlocked by the user. */ + private fun navigateToLockScreenSettings( + activityStarter: ActivityStarter, + view: View, + ) { + activityStarter.postStartActivityDismissingKeyguard( + Intent(Intent.ACTION_SET_WALLPAPER).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + view.context + .getString(R.string.config_wallpaperPickerPackage) + .takeIf { it.isNotEmpty() } + ?.let { packageName -> setPackage(packageName) } + }, + /* delay= */ 0, + /* animationController= */ ActivityLaunchAnimator.Controller.fromView(view), + /* customMessage= */ view.context.getString(R.string.keyguard_unlock_to_customize_ls) + ) + } + + private fun View.animateVisibility(visible: Boolean) { + animate() + .withStartAction { + if (visible) { + alpha = 0f + isVisible = true + } + } + .alpha(if (visible) 1f else 0f) + .withEndAction { + if (!visible) { + isVisible = false + } + } + .start() + } + +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index b92d10474ccd0..3e6e1585ca6d3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -42,17 +42,27 @@ import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder +import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder +import com.android.systemui.keyguard.ui.view.KeyguardRootView +import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManager import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewSmartspaceViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.monet.ColorScheme import com.android.systemui.plugins.ClockController +import com.android.systemui.plugins.FalsingManager import com.android.systemui.shared.clocks.ClockRegistry import com.android.systemui.shared.clocks.DefaultClockController import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants +import com.android.systemui.statusbar.KeyguardIndicationController +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import com.android.systemui.statusbar.phone.KeyguardBottomAreaView import dagger.assisted.Assisted @@ -71,6 +81,7 @@ constructor( private val clockViewModel: KeyguardPreviewClockViewModel, private val smartspaceViewModel: KeyguardPreviewSmartspaceViewModel, private val bottomAreaViewModel: KeyguardBottomAreaViewModel, + private val quickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel, displayManager: DisplayManager, private val windowManager: WindowManager, private val clockController: ClockEventController, @@ -78,6 +89,12 @@ constructor( private val broadcastDispatcher: BroadcastDispatcher, private val lockscreenSmartspaceController: LockscreenSmartspaceController, private val udfpsOverlayInteractor: UdfpsOverlayInteractor, + private val featureFlags: FeatureFlags, + private val keyguardLayoutManager: KeyguardLayoutManager, + private val falsingManager: FalsingManager, + private val vibratorHelper: VibratorHelper, + private val indicationController: KeyguardIndicationController, + private val keyguardRootViewModel: KeyguardRootViewModel, @Assisted bundle: Bundle, ) { @@ -106,14 +123,26 @@ constructor( private val disposables = mutableSetOf() private var isDestroyed = false + private val shortcutsBindings = mutableSetOf() + init { - bottomAreaViewModel.enablePreviewMode( - initiallySelectedSlotId = + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + keyguardRootViewModel.enablePreviewMode( + initiallySelectedSlotId = bundle.getString( KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID, ), - shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance, - ) + shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance, + ) + } else { + bottomAreaViewModel.enablePreviewMode( + initiallySelectedSlotId = + bundle.getString( + KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID, + ), + shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance, + ) + } runBlocking(mainDispatcher) { host = SurfaceControlViewHost( @@ -130,7 +159,20 @@ constructor( mainHandler.post { val rootView = FrameLayout(context) - setUpBottomArea(rootView) + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + val keyguardRootView = KeyguardRootView(context, null) + rootView.addView( + keyguardRootView, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + ), + ) + setupShortcuts(keyguardRootView) + keyguardLayoutManager.layoutViews(keyguardRootView) + } else { + setUpBottomArea(rootView) + } setUpSmartspace(rootView) smartSpaceView?.let { @@ -182,13 +224,20 @@ constructor( } fun onSlotSelected(slotId: String) { - bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId) + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + quickAffordancesCombinedViewModel.onPreviewSlotSelected(slotId = slotId) + } else { + bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId) + } } fun destroy() { isDestroyed = true lockscreenSmartspaceController.disconnect() disposables.forEach { it.dispose() } + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + shortcutsBindings.forEach { it.destroy() } + } } /** @@ -250,6 +299,7 @@ constructor( smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f } + @Deprecated("Deprecated as part of b/278057014") private fun setUpBottomArea(parentView: ViewGroup) { val bottomAreaView = LayoutInflater.from(context) @@ -259,7 +309,7 @@ constructor( false, ) as KeyguardBottomAreaView bottomAreaView.init( - viewModel = bottomAreaViewModel, + viewModel = bottomAreaViewModel, ) parentView.addView( bottomAreaView, @@ -270,6 +320,32 @@ constructor( ) } + private fun setupShortcuts(keyguardRootView: KeyguardRootView) { + shortcutsBindings.add( + KeyguardQuickAffordanceViewBinder.bind( + keyguardRootView.requireViewById(R.id.start_button), + quickAffordancesCombinedViewModel.startButton, + keyguardRootViewModel.alpha, + falsingManager, + vibratorHelper, + ) { + indicationController.showTransientIndication(it) + } + ) + + shortcutsBindings.add( + KeyguardQuickAffordanceViewBinder.bind( + keyguardRootView.requireViewById(R.id.end_button), + quickAffordancesCombinedViewModel.endButton, + keyguardRootViewModel.alpha, + falsingManager, + vibratorHelper, + ) { + indicationController.showTransientIndication(it) + } + ) + } + private fun setUpUdfps(parentView: ViewGroup) { val sensorBounds = udfpsOverlayInteractor.udfpsOverlayParams.value.sensorBounds diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardRootView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardRootView.kt index 0077f2d68c7e9..65fe990453875 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardRootView.kt @@ -20,8 +20,13 @@ package com.android.systemui.keyguard.ui.view import android.content.Context import android.util.AttributeSet import androidx.constraintlayout.widget.ConstraintLayout +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import androidx.core.content.res.ResourcesCompat import com.android.keyguard.LockIconView import com.android.systemui.R +import com.android.systemui.animation.view.LaunchableImageView /** Provides a container for all keyguard ui content. */ class KeyguardRootView( @@ -36,6 +41,10 @@ class KeyguardRootView( init { addIndicationTextArea() addLockIconView() + addAmbientIndicationArea() + addLeftShortcut() + addRightShortcut() + addSettingsPopupMenu() } private fun addIndicationTextArea() { @@ -47,4 +56,65 @@ class KeyguardRootView( val view = LockIconView(context, attrs).apply { id = R.id.lock_icon_view } addView(view) } + + private fun addAmbientIndicationArea() { + LayoutInflater.from(context).inflate(R.layout.ambient_indication, this) + } + + private fun addLeftShortcut() { + val view = LaunchableImageView(context, attrs) + .apply { + id = R.id.start_button + scaleType = ImageView.ScaleType.FIT_CENTER + background = + ResourcesCompat.getDrawable( + context.resources, + R.drawable.keyguard_bottom_affordance_bg, + context.theme + ) + foreground = + ResourcesCompat.getDrawable( + context.resources, + R.drawable.keyguard_bottom_affordance_selected_border, + context.theme + ) + visibility = View.INVISIBLE + } + addView(view) + } + + private fun addRightShortcut() { + val view = LaunchableImageView(context, attrs) + .apply { + id = R.id.end_button + scaleType = ImageView.ScaleType.FIT_CENTER + background = + ResourcesCompat.getDrawable( + context.resources, + R.drawable.keyguard_bottom_affordance_bg, + context.theme + ) + foreground = + ResourcesCompat.getDrawable( + context.resources, + R.drawable.keyguard_bottom_affordance_selected_border, + context.theme + ) + visibility = View.INVISIBLE + } + addView(view) + } + + private fun addSettingsPopupMenu() { + val view = LayoutInflater.from(context).inflate( + R.layout.keyguard_settings_popup_menu, + this, + false + ) + .apply { + id = R.id.keyguard_settings_button + visibility = GONE + } + addView(view) + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayout.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayout.kt index baaeb60f364ea..6be45c79e37e6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayout.kt @@ -24,17 +24,27 @@ import android.util.DisplayMetrics import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.view.ViewGroup.MarginLayoutParams import android.view.WindowManager +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.END +import androidx.constraintlayout.widget.ConstraintSet.LEFT +import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID +import androidx.constraintlayout.widget.ConstraintSet.RIGHT import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP +import androidx.core.view.setPadding +import androidx.core.view.updateLayoutParams import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R +import com.android.systemui.animation.view.LaunchableLinearLayout import com.android.systemui.biometrics.AuthController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.view.KeyguardRootView @@ -136,15 +146,139 @@ constructor( } } + override fun layoutShortcuts(rootView: KeyguardRootView) { + val leftShortcut = rootView.findViewById(R.id.start_button) ?: return + val rightShortcut = rootView.findViewById(R.id.end_button) ?: return + val width = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width) + val height = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height) + val horizontalOffsetMargin = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_horizontal_offset) + val verticalOffsetMargin = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_vertical_offset) + val padding = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_padding) + + leftShortcut.apply { + updateLayoutParams { + marginStart = horizontalOffsetMargin + bottomMargin = verticalOffsetMargin + } + setPadding(padding) + } + + rightShortcut.apply { + updateLayoutParams { + marginEnd = horizontalOffsetMargin + bottomMargin = verticalOffsetMargin + } + setPadding(padding) + } + + rootView.getConstraintSet().apply { + constrainWidth(leftShortcut.id, width) + constrainHeight(leftShortcut.id, height) + connect(leftShortcut.id, LEFT, PARENT_ID, LEFT) + connect(leftShortcut.id, BOTTOM, PARENT_ID, BOTTOM) + + constrainWidth(rightShortcut.id, width) + constrainHeight(rightShortcut.id, height) + connect(rightShortcut.id, RIGHT, PARENT_ID, RIGHT) + connect(rightShortcut.id, BOTTOM, PARENT_ID, BOTTOM) + applyTo(rootView) + } + } + + override fun layoutAmbientIndicationArea(rootView: KeyguardRootView) { + val ambientIndicationContainer = + rootView.findViewById(R.id.ambient_indication_container) ?: return + val lockIconView = rootView.findViewById(R.id.lock_icon_view) ?: return + val indicationArea = rootView.findViewById(R.id.keyguard_indication_area) ?: return + + rootView.getConstraintSet().apply { + constrainWidth(ambientIndicationContainer.id, MATCH_PARENT) + + if (keyguardUpdateMonitor.isUdfpsSupported) { + //constrain below udfps and above indication area + constrainHeight(ambientIndicationContainer.id, MATCH_CONSTRAINT) + connect(ambientIndicationContainer.id, TOP, lockIconView.id, BOTTOM) + connect(ambientIndicationContainer.id, BOTTOM, indicationArea.id, TOP) + connect(ambientIndicationContainer.id, LEFT, PARENT_ID, LEFT) + connect(ambientIndicationContainer.id, RIGHT, PARENT_ID, RIGHT) + } else { + //constrain above lock icon + constrainHeight(ambientIndicationContainer.id, WRAP_CONTENT) + connect(ambientIndicationContainer.id, BOTTOM, lockIconView.id, TOP) + connect(ambientIndicationContainer.id, LEFT, PARENT_ID, LEFT) + connect(ambientIndicationContainer.id, RIGHT, PARENT_ID, RIGHT) + } + + applyTo(rootView) + } + } + + override fun layoutSettingsPopupMenu(rootView: KeyguardRootView) { + val popupMenu = + rootView.findViewById(R.id.keyguard_settings_button) ?: return + val icon = popupMenu.findViewById(R.id.icon) ?: return + val textView = popupMenu.findViewById(R.id.text) ?: return + val horizontalOffsetMargin = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_horizontal_offset) + + icon.updateLayoutParams { + height = + context + .resources + .getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_height) + width = + context + .resources + .getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_width) + marginEnd = + context + .resources + .getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_end_margin) + } + + textView.updateLayoutParams { + height = WRAP_CONTENT + width = WRAP_CONTENT + } + + popupMenu.updateLayoutParams { + bottomMargin = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_vertical_offset) + marginStart = horizontalOffsetMargin + marginEnd = horizontalOffsetMargin + } + popupMenu.setPadding( + context.resources.getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_padding) + ) + + rootView.getConstraintSet().apply { + constrainWidth(popupMenu.id, WRAP_CONTENT) + constrainHeight(popupMenu.id, WRAP_CONTENT) + constrainMinHeight( + popupMenu.id, + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height) + ) + connect(popupMenu.id, LEFT, PARENT_ID, LEFT) + connect(popupMenu.id, RIGHT, PARENT_ID, RIGHT) + connect(popupMenu.id, BOTTOM, PARENT_ID, BOTTOM) + + applyTo(rootView) + } + } + private fun Int.dp(): Int { return context.resources.getDimensionPixelSize(this) } - private fun ConstraintLayout.getConstraintSet(): ConstraintSet { - val cs = ConstraintSet() - cs.clone(this) - return cs - } + private fun ConstraintLayout.getConstraintSet(): ConstraintSet = + ConstraintSet().also { + it.clone(this) + } companion object { const val DEFAULT = "default" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManager.kt index 9bc630265a3f5..6973cd914aade 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManager.kt @@ -63,8 +63,8 @@ constructor( return true } - fun layoutViews() { - layout?.layoutViews(keyguardRootView) + fun layoutViews(rootView: KeyguardRootView = keyguardRootView) { + layout?.layoutViews(rootView) } companion object { @@ -85,7 +85,13 @@ interface LockscreenLayout { .applyTo(rootView) layoutIndicationArea(rootView) layoutLockIcon(rootView) + layoutShortcuts(rootView) + layoutAmbientIndicationArea(rootView) + layoutSettingsPopupMenu(rootView) } fun layoutIndicationArea(rootView: KeyguardRootView) fun layoutLockIcon(rootView: KeyguardRootView) + fun layoutShortcuts(rootView: KeyguardRootView) + fun layoutAmbientIndicationArea(rootView: KeyguardRootView) + fun layoutSettingsPopupMenu(rootView: KeyguardRootView) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/LockscreenLayoutModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/LockscreenLayoutModule.kt index 00f93e33f3706..c0447af0590da 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/LockscreenLayoutModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/LockscreenLayoutModule.kt @@ -28,4 +28,10 @@ abstract class LockscreenLayoutModule { abstract fun bindDefaultLayout( defaultLockscreenLayout: DefaultLockscreenLayout ): LockscreenLayout + + @Binds + @IntoSet + abstract fun bindShortcutsBesideUdfpsLockscreenLayout( + shortcutsBesideUdfpsLockscreenLayout: ShortcutsBesideUdfpsLockscreenLayout + ): LockscreenLayout } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/ShortcutsBesideUdfpsLockscreenLayout.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/ShortcutsBesideUdfpsLockscreenLayout.kt new file mode 100644 index 0000000000000..569762d67c5fd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/ShortcutsBesideUdfpsLockscreenLayout.kt @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.view.layout + +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.util.DisplayMetrics +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.view.WindowManager +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.constraintlayout.widget.ConstraintSet.BOTTOM +import androidx.constraintlayout.widget.ConstraintSet.END +import androidx.constraintlayout.widget.ConstraintSet.LEFT +import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT +import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID +import androidx.constraintlayout.widget.ConstraintSet.RIGHT +import androidx.constraintlayout.widget.ConstraintSet.START +import androidx.constraintlayout.widget.ConstraintSet.TOP +import androidx.core.view.setPadding +import androidx.core.view.updateLayoutParams +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.R +import com.android.systemui.animation.view.LaunchableLinearLayout +import com.android.systemui.biometrics.AuthController +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.ui.view.KeyguardRootView +import javax.inject.Inject + +/** + * Positions elements of the lockscreen to the default position. + * + * This will be the most common use case for phones in portrait mode. + */ +@SysUISingleton +class ShortcutsBesideUdfpsLockscreenLayout +@Inject +constructor( + private val authController: AuthController, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val windowManager: WindowManager, + private val context: Context, +) : LockscreenLayout { + override val id: String = SHORTCUTS_BESIDE_UDFPS + + override fun layoutIndicationArea(rootView: KeyguardRootView) { + val indicationArea = rootView.findViewById(R.id.keyguard_indication_area) ?: return + + rootView.getConstraintSet().apply { + constrainWidth(indicationArea.id, MATCH_PARENT) + constrainHeight(indicationArea.id, WRAP_CONTENT) + connect( + indicationArea.id, + BOTTOM, + PARENT_ID, + BOTTOM, + R.dimen.keyguard_indication_margin_bottom.dp() + ) + connect(indicationArea.id, START, PARENT_ID, START) + connect(indicationArea.id, END, PARENT_ID, END) + applyTo(rootView) + } + } + + override fun layoutLockIcon(rootView: KeyguardRootView) { + val isUdfpsSupported = keyguardUpdateMonitor.isUdfpsSupported + val scaleFactor: Float = authController.scaleFactor + val mBottomPaddingPx = R.dimen.lock_icon_margin_bottom.dp() + val mDefaultPaddingPx = R.dimen.lock_icon_padding.dp() + val scaledPadding: Int = (mDefaultPaddingPx * scaleFactor).toInt() + val bounds = windowManager.currentWindowMetrics.bounds + val widthPixels = bounds.right.toFloat() + val heightPixels = bounds.bottom.toFloat() + val defaultDensity = + DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() / + DisplayMetrics.DENSITY_DEFAULT.toFloat() + val lockIconRadiusPx = (defaultDensity * 36).toInt() + + if (isUdfpsSupported) { + authController.udfpsLocation?.let { udfpsLocation -> + centerLockIcon(udfpsLocation, authController.udfpsRadius, scaledPadding, rootView) + } + } else { + centerLockIcon( + Point( + (widthPixels / 2).toInt(), + (heightPixels - ((mBottomPaddingPx + lockIconRadiusPx) * scaleFactor)).toInt() + ), + lockIconRadiusPx * scaleFactor, + scaledPadding, + rootView + ) + } + } + + @VisibleForTesting + internal fun centerLockIcon( + center: Point, + radius: Float, + drawablePadding: Int, + rootView: KeyguardRootView, + ) { + val lockIconView = rootView.findViewById(R.id.lock_icon_view) ?: return + val lockIcon = lockIconView.findViewById(R.id.lock_icon) ?: return + lockIcon.setPadding(drawablePadding, drawablePadding, drawablePadding, drawablePadding) + + val sensorRect = + Rect().apply { + set( + center.x - radius.toInt(), + center.y - radius.toInt(), + center.x + radius.toInt(), + center.y + radius.toInt(), + ) + } + + rootView.getConstraintSet().apply { + constrainWidth(lockIconView.id, sensorRect.right - sensorRect.left) + constrainHeight(lockIconView.id, sensorRect.bottom - sensorRect.top) + connect(lockIconView.id, TOP, PARENT_ID, TOP, sensorRect.top) + connect(lockIconView.id, START, PARENT_ID, START, sensorRect.left) + applyTo(rootView) + } + } + + override fun layoutShortcuts(rootView: KeyguardRootView) { + val leftShortcut = rootView.findViewById(R.id.start_button) ?: return + val rightShortcut = rootView.findViewById(R.id.end_button) ?: return + val lockIconView = rootView.findViewById(R.id.lock_icon_view) ?: return + val udfpsSupported = keyguardUpdateMonitor.isUdfpsSupported + val width = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width) + val height = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height) + val horizontalOffsetMargin = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_horizontal_offset) + val verticalOffsetMargin = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_vertical_offset) + val padding = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_padding) + + if (!udfpsSupported) { + leftShortcut.apply { + updateLayoutParams { + marginStart = horizontalOffsetMargin + bottomMargin = verticalOffsetMargin + } + setPadding(padding) + } + + rightShortcut.apply { + updateLayoutParams { + marginEnd = horizontalOffsetMargin + bottomMargin = verticalOffsetMargin + } + setPadding(padding) + } + } + + rootView.getConstraintSet().apply { + if (udfpsSupported) { + constrainWidth(leftShortcut.id, width) + constrainHeight(leftShortcut.id, height) + connect(leftShortcut.id, LEFT, PARENT_ID, LEFT) + connect(leftShortcut.id, RIGHT, lockIconView.id, LEFT) + connect(leftShortcut.id, TOP, lockIconView.id, TOP) + connect(leftShortcut.id, BOTTOM, lockIconView.id, BOTTOM) + + constrainWidth(rightShortcut.id, width) + constrainHeight(rightShortcut.id, height) + connect(rightShortcut.id, RIGHT, PARENT_ID, RIGHT) + connect(rightShortcut.id, LEFT, lockIconView.id, RIGHT) + connect(rightShortcut.id, TOP, lockIconView.id, TOP) + connect(rightShortcut.id, BOTTOM, lockIconView.id, BOTTOM) + } else { + constrainWidth(leftShortcut.id, width) + constrainHeight(leftShortcut.id, height) + connect(leftShortcut.id, LEFT, PARENT_ID, LEFT) + connect(leftShortcut.id, BOTTOM, PARENT_ID, BOTTOM) + + constrainWidth(rightShortcut.id, width) + constrainHeight(rightShortcut.id, height) + connect(rightShortcut.id, RIGHT, PARENT_ID, RIGHT) + connect(rightShortcut.id, BOTTOM, PARENT_ID, BOTTOM) + } + applyTo(rootView) + } + } + + override fun layoutAmbientIndicationArea(rootView: KeyguardRootView) { + val ambientIndicationContainer = + rootView.findViewById(R.id.ambient_indication_container) ?: return + val lockIconView = rootView.findViewById(R.id.lock_icon_view) ?: return + val indicationArea = rootView.findViewById(R.id.keyguard_indication_area) ?: return + + rootView.getConstraintSet().apply { + constrainWidth(ambientIndicationContainer.id, MATCH_PARENT) + + if (keyguardUpdateMonitor.isUdfpsSupported) { + //constrain below udfps and above indication area + constrainHeight(ambientIndicationContainer.id, MATCH_CONSTRAINT) + connect(ambientIndicationContainer.id, TOP, lockIconView.id, BOTTOM) + connect(ambientIndicationContainer.id, BOTTOM, indicationArea.id, TOP) + connect(ambientIndicationContainer.id, LEFT, PARENT_ID, LEFT) + connect(ambientIndicationContainer.id, RIGHT, PARENT_ID, RIGHT) + } else { + //constrain above lock icon + constrainHeight(ambientIndicationContainer.id, WRAP_CONTENT) + connect(ambientIndicationContainer.id, BOTTOM, lockIconView.id, TOP) + connect(ambientIndicationContainer.id, LEFT, PARENT_ID, LEFT) + connect(ambientIndicationContainer.id, RIGHT, PARENT_ID, RIGHT) + } + applyTo(rootView) + } + } + + override fun layoutSettingsPopupMenu(rootView: KeyguardRootView) { + val popupMenu = + rootView.findViewById(R.id.keyguard_settings_button) ?: return + val icon = popupMenu.findViewById(R.id.icon) ?: return + val textView = popupMenu.findViewById(R.id.text) ?: return + val horizontalOffsetMargin = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_horizontal_offset) + + icon.updateLayoutParams { + height = + context + .resources + .getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_height) + width = + context + .resources + .getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_width) + marginEnd = + context + .resources + .getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_icon_end_margin) + } + + textView.updateLayoutParams { + height = WRAP_CONTENT + width = WRAP_CONTENT + } + + popupMenu.updateLayoutParams { + bottomMargin = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_vertical_offset) + marginStart = horizontalOffsetMargin + marginEnd = horizontalOffsetMargin + } + popupMenu.setPadding( + context.resources.getDimensionPixelSize(R.dimen.keyguard_settings_popup_menu_padding) + ) + + rootView.getConstraintSet().apply { + constrainWidth(popupMenu.id, WRAP_CONTENT) + constrainHeight(popupMenu.id, WRAP_CONTENT) + constrainMinHeight( + popupMenu.id, + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height) + ) + connect(popupMenu.id, LEFT, PARENT_ID, LEFT) + connect(popupMenu.id, RIGHT, PARENT_ID, RIGHT) + connect(popupMenu.id, BOTTOM, PARENT_ID, BOTTOM) + + applyTo(rootView) + } + } + + private fun Int.dp(): Int { + return context.resources.getDimensionPixelSize(this) + } + + private fun ConstraintLayout.getConstraintSet(): ConstraintSet = + ConstraintSet().also { + it.clone(this) + } + + companion object { + const val SHORTCUTS_BESIDE_UDFPS = "shortcutsBesideUdfps" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardAmbientIndicationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardAmbientIndicationViewModel.kt new file mode 100644 index 0000000000000..dd3967aed99d7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardAmbientIndicationViewModel.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class KeyguardAmbientIndicationViewModel +@Inject +constructor( + private val keyguardInteractor: KeyguardInteractor, + private val burnInHelperWrapper: BurnInHelperWrapper, +) { + + /** An observable for the x-offset by which the indication area should be translated. */ + val indicationAreaTranslationX: Flow = + keyguardInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged() + + /** Returns an observable for the y-offset by which the indication area should be translated. */ + fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow { + return keyguardInteractor.dozeAmount + .map { dozeAmount -> + dozeAmount * + (burnInHelperWrapper.burnInOffset( + /* amplitude = */ defaultBurnInOffset * 2, + /* xAxis= */ false, + ) - defaultBurnInOffset) + } + .distinctUntilChanged() + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt index f1ceaaa391f51..980cc1b8ebe1f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt @@ -17,6 +17,8 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import javax.inject.Inject @@ -33,6 +35,8 @@ constructor( bottomAreaInteractor: KeyguardBottomAreaInteractor, keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel, private val burnInHelperWrapper: BurnInHelperWrapper, + private val shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel, + private val featureFlags: FeatureFlags, ) { /** Notifies when a new configuration is set */ @@ -43,15 +47,28 @@ constructor( /** An observable for whether the indication area should be padded. */ val isIndicationAreaPadded: Flow = - combine(keyguardBottomAreaViewModel.startButton, keyguardBottomAreaViewModel.endButton) { + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + combine(shortcutsCombinedViewModel.startButton, shortcutsCombinedViewModel.endButton) { startButtonModel, endButtonModel -> startButtonModel.isVisible || endButtonModel.isVisible } - .distinctUntilChanged() + .distinctUntilChanged() + } else { + combine(keyguardBottomAreaViewModel.startButton, keyguardBottomAreaViewModel.endButton) { + startButtonModel, + endButtonModel -> + startButtonModel.isVisible || endButtonModel.isVisible + } + .distinctUntilChanged() + } /** An observable for the x-offset by which the indication area should be translated. */ val indicationAreaTranslationX: Flow = - bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged() + if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + keyguardInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged() + } else { + bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged() + } /** Returns an observable for the y-offset by which the indication area should be translated. */ fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt new file mode 100644 index 0000000000000..56a98455d8b37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.annotation.VisibleForTesting +import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel +import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +class KeyguardQuickAffordancesCombinedViewModel +@Inject +constructor( + private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor, + private val keyguardInteractor: KeyguardInteractor, +) { + + /** + * ID of the slot that's currently selected in the preview that renders exclusively in the + * wallpaper picker application. This is ignored for the actual, real lock screen experience. + */ + private val selectedPreviewSlotId = + MutableStateFlow(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START) + + /** + * Whether quick affordances are "opaque enough" to be considered visible to and interactive by + * the user. If they are not interactive, user input should not be allowed on them. + * + * Note that there is a margin of error, where we allow very, very slightly transparent views to + * be considered "fully opaque" for the purpose of being interactive. This is to accommodate the + * error margin of floating point arithmetic. + * + * A view that is visible but with an alpha of less than our threshold either means it's not + * fully done fading in or is fading/faded out. Either way, it should not be + * interactive/clickable unless "fully opaque" to avoid issues like in b/241830987. + */ + private val areQuickAffordancesFullyOpaque: Flow = + keyguardInteractor.keyguardAlpha + .map { alpha -> alpha >= AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD } + .distinctUntilChanged() + + /** An observable for the view-model of the "start button" quick affordance. */ + val startButton: Flow = + button(KeyguardQuickAffordancePosition.BOTTOM_START) + + /** An observable for the view-model of the "end button" quick affordance. */ + val endButton: Flow = + button(KeyguardQuickAffordancePosition.BOTTOM_END) + + /** + * Notifies that a slot with the given ID has been selected in the preview experience that is + * rendering in the wallpaper picker. This is ignored for the real lock screen experience. + * + * @see [KeyguardRootViewModel.enablePreviewMode] + */ + fun onPreviewSlotSelected(slotId: String) { + selectedPreviewSlotId.value = slotId + } + + private fun button( + position: KeyguardQuickAffordancePosition + ): Flow { + return keyguardInteractor.previewMode.flatMapLatest { previewMode -> + combine( + if (previewMode.isInPreviewMode) { + quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position) + } else { + quickAffordanceInteractor.quickAffordance(position = position) + }, + keyguardInteractor.animateDozingTransitions.distinctUntilChanged(), + areQuickAffordancesFullyOpaque, + selectedPreviewSlotId, + quickAffordanceInteractor.useLongPress(), + ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress -> + val slotId = position.toSlotId() + val isSelected = selectedPreviewSlotId == slotId + model.toViewModel( + animateReveal = !previewMode.isInPreviewMode && animateReveal, + isClickable = isFullyOpaque && !previewMode.isInPreviewMode, + isSelected = + previewMode.isInPreviewMode && + previewMode.shouldHighlightSelectedAffordance && + isSelected, + isDimmed = + previewMode.isInPreviewMode && + previewMode.shouldHighlightSelectedAffordance && + !isSelected, + forceInactive = previewMode.isInPreviewMode, + slotId = slotId, + useLongPress = useLongPress, + ) + } + .distinctUntilChanged() + } + } + + private fun KeyguardQuickAffordanceModel.toViewModel( + animateReveal: Boolean, + isClickable: Boolean, + isSelected: Boolean, + isDimmed: Boolean, + forceInactive: Boolean, + slotId: String, + useLongPress: Boolean, + ): KeyguardQuickAffordanceViewModel { + return when (this) { + is KeyguardQuickAffordanceModel.Visible -> + KeyguardQuickAffordanceViewModel( + configKey = configKey, + isVisible = true, + animateReveal = animateReveal, + icon = icon, + onClicked = { parameters -> + quickAffordanceInteractor.onQuickAffordanceTriggered( + configKey = parameters.configKey, + expandable = parameters.expandable, + slotId = parameters.slotId, + ) + }, + isClickable = isClickable, + isActivated = !forceInactive && activationState is ActivationState.Active, + isSelected = isSelected, + useLongPress = useLongPress, + isDimmed = isDimmed, + slotId = slotId, + ) + is KeyguardQuickAffordanceModel.Hidden -> + KeyguardQuickAffordanceViewModel( + slotId = slotId, + ) + } + } + + companion object { + // We select a value that's less than 1.0 because we want floating point math precision to + // not be a factor in determining whether the affordance UI is fully opaque. The number we + // choose needs to be close enough 1.0 such that the user can't easily tell the difference + // between the UI with an alpha at the threshold and when the alpha is 1.0. At the same + // time, we don't want the number to be too close to 1.0 such that there is a chance that we + // never treat the affordance UI as "fully opaque" as that would risk making it forever not + // clickable. + @VisibleForTesting + const val AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD = 0.95f + } + +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt new file mode 100644 index 0000000000000..316ca771bf8f9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.shared.model.KeyguardRootViewVisibilityState +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +class KeyguardRootViewModel +@Inject +constructor( + private val keyguardInteractor: KeyguardInteractor, + private val keyguardQuickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel +) +{ + /** Represents the current state of the KeyguardRootView visibility */ + val keyguardRootViewVisibilityState: Flow = + keyguardInteractor.keyguardRootViewVisibilityState + + /** An observable for the alpha level for the entire keyguard root view. */ + val alpha: Flow = + keyguardInteractor.previewMode.flatMapLatest { + if (it.isInPreviewMode) { + flowOf(1f) + } else { + keyguardInteractor.keyguardAlpha.distinctUntilChanged() + } + } + + /** + * Puts this view-model in "preview mode", which means it's being used for UI that is rendering + * the lock screen preview in wallpaper picker / settings and not the real experience on the + * lock screen. + * + * @param initiallySelectedSlotId The ID of the initial slot to render as the selected one. + * @param shouldHighlightSelectedAffordance Whether the selected quick affordance should be + * highlighted (while all others are dimmed to make the selected one stand out). + */ + fun enablePreviewMode( + initiallySelectedSlotId: String?, + shouldHighlightSelectedAffordance: Boolean, + ) { + keyguardInteractor.previewMode.value = + KeyguardInteractor.PreviewMode( + isInPreviewMode = true, + shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance, + ) + keyguardQuickAffordancesCombinedViewModel.onPreviewSlotSelected( + initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + ) + } + +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index ed7cbffc880b7..773f35e0c6659 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -138,6 +138,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.shared.model.WakefulnessModel; import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder; +import com.android.systemui.keyguard.ui.view.KeyguardRootView; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; @@ -348,6 +349,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final AccessibilityDelegate mAccessibilityDelegate = new ShadeAccessibilityDelegate(); private final NotificationGutsManager mGutsManager; private final AlternateBouncerInteractor mAlternateBouncerInteractor; + private final KeyguardRootView mKeyguardRootView; private final QuickSettingsController mQsController; private final TouchHandler mTouchHandler = new TouchHandler(); @@ -363,6 +365,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private boolean mTracking; private boolean mIsTrackingExpansionFromStatusBar; private boolean mHintAnimationRunning; + @Deprecated private KeyguardBottomAreaView mKeyguardBottomArea; private boolean mExpanding; private boolean mSplitShadeEnabled; @@ -739,7 +742,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump KeyguardInteractor keyguardInteractor, ActivityStarter activityStarter, KeyguardViewConfigurator keyguardViewConfigurator, - KeyguardFaceAuthInteractor keyguardFaceAuthInteractor) { + KeyguardFaceAuthInteractor keyguardFaceAuthInteractor, + KeyguardRootView keyguardRootView) { keyguardStateController.addCallback(new KeyguardStateController.Callback() { @Override public void onKeyguardFadingAwayChanged() { @@ -939,6 +943,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } }); mAlternateBouncerInteractor = alternateBouncerInteractor; + mKeyguardRootView = keyguardRootView; dumpManager.registerDumpable(this); } @@ -1029,7 +1034,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mShadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged); mShadeHeadsUpTracker.addTrackingHeadsUpListener( mNotificationStackScrollLayoutController::setTrackingHeadsUp); - setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area)); + if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area)); + } initBottomArea(); @@ -1311,14 +1318,17 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump updateViewControllers(mView.findViewById(R.id.keyguard_status_view), userAvatarView, keyguardUserSwitcherView); - // Update keyguard bottom area - int index = mView.indexOfChild(mKeyguardBottomArea); - mView.removeView(mKeyguardBottomArea); - KeyguardBottomAreaView oldBottomArea = mKeyguardBottomArea; - setKeyguardBottomArea(mKeyguardBottomAreaViewControllerProvider.get().getView()); - mKeyguardBottomArea.initFrom(oldBottomArea); - mView.addView(mKeyguardBottomArea, index); - initBottomArea(); + if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + // Update keyguard bottom area + int index = mView.indexOfChild(mKeyguardBottomArea); + mView.removeView(mKeyguardBottomArea); + KeyguardBottomAreaView oldBottomArea = mKeyguardBottomArea; + setKeyguardBottomArea(mKeyguardBottomAreaViewControllerProvider.get().getView()); + mKeyguardBottomArea.initFrom(oldBottomArea); + mView.addView(mKeyguardBottomArea, index); + + initBottomArea(); + } mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(), mStatusBarStateController.getInterpolatedDozeAmount()); @@ -1341,7 +1351,12 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump false, mBarState); } - setKeyguardBottomAreaVisibility(mBarState, false); + + if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + setKeyguardVisibility(mBarState, false); + } else { + setKeyguardBottomAreaVisibility(mBarState, false); + } mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView)); mNotificationPanelUnfoldAnimationController.ifPresent(u -> u.setup(mView)); @@ -1352,7 +1367,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void initBottomArea() { - mKeyguardBottomArea.init( + if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + mKeyguardBottomArea.init( mKeyguardBottomAreaViewModel, mFalsingManager, mLockIconViewController, @@ -1361,8 +1377,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mVibratorHelper, mActivityStarter); - // Rebind (for now), as a new bottom area and indication area may have been created - mKeyguardViewConfigurator.bindIndicationArea(mKeyguardBottomArea); + // Rebind (for now), as a new bottom area and indication area may have been created + mKeyguardViewConfigurator.bindIndicationArea(); + } } @VisibleForTesting @@ -1398,6 +1415,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return mHintAnimationRunning || mUnlockedScreenOffAnimationController.isAnimationPlaying(); } + @Deprecated private void setKeyguardBottomArea(KeyguardBottomAreaView keyguardBottomArea) { mKeyguardBottomArea = keyguardBottomArea; } @@ -1521,8 +1539,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mClockPositionAlgorithm.run(mClockPositionResult); mKeyguardStatusViewController.setLockscreenClockY( mClockPositionAlgorithm.getExpandedPreferredClockY()); - mKeyguardBottomAreaInteractor.setClockPosition( + if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + mKeyguardInteractor.setClockPosition( mClockPositionResult.clockX, mClockPositionResult.clockY); + } else { + mKeyguardBottomAreaInteractor.setClockPosition( + mClockPositionResult.clockX, mClockPositionResult.clockY); + } boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange; mKeyguardStatusViewController.updatePosition( @@ -2169,6 +2192,15 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } } + private void setKeyguardVisibility(int statusBarState, boolean goingToFullShade) { + mKeyguardInteractor.setKeyguardRootVisibility( + statusBarState, + goingToFullShade, + mIsOcclusionTransitionRunning + ); + } + + @Deprecated private void setKeyguardBottomAreaVisibility(int statusBarState, boolean goingToFullShade) { mKeyguardBottomArea.animate().cancel(); if (goingToFullShade) { @@ -2177,8 +2209,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardStateController.getShortenedFadingAwayDuration()).setInterpolator( Interpolators.ALPHA_OUT).withEndAction( mAnimateKeyguardBottomAreaInvisibleEndRunnable).start(); - } else if (statusBarState == KEYGUARD - || statusBarState == StatusBarState.SHADE_LOCKED) { + } else if (statusBarState == KEYGUARD || statusBarState == StatusBarState.SHADE_LOCKED) { mKeyguardBottomArea.setVisibility(View.VISIBLE); if (!mIsOcclusionTransitionRunning) { mKeyguardBottomArea.setAlpha(1f); @@ -2514,7 +2545,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump getExpandedFraction()); float alpha = Math.min(expansionAlpha, 1 - mQsController.computeExpansionFraction()); alpha *= mBottomAreaShadeAlpha; - mKeyguardBottomAreaInteractor.setAlpha(alpha); + if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + mKeyguardInteractor.setAlpha(alpha); + } else { + mKeyguardBottomAreaInteractor.setAlpha(alpha); + } mLockIconViewController.setAlpha(alpha); } @@ -2773,7 +2808,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void updateDozingVisibilities(boolean animate) { - mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate); + if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + mKeyguardInteractor.setAnimateDozingTransitions(animate); + } else { + mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate); + } if (!mDozing && animate) { mKeyguardStatusBarViewController.animateKeyguardStatusBarIn(); } @@ -2977,7 +3016,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mDozing = dozing; // TODO (b/) make listeners for this mNotificationStackScrollLayoutController.setDozing(mDozing, animate); - mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate); + if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + mKeyguardInteractor.setAnimateDozingTransitions(animate); + } else { + mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate); + } mKeyguardStatusBarViewController.setDozing(mDozing); mQsController.setDozing(mDozing); @@ -3898,20 +3941,37 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump animator.start(); setAnimator(animator); - final List indicationAnimators = - mKeyguardBottomArea.getIndicationAreaAnimators(); - for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) { - indicationAreaAnimator + + if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + final ViewPropertyAnimator mKeyguardRootViewAnimator = mKeyguardRootView.animate(); + mKeyguardRootViewAnimator .translationY(-mHintDistance) .setDuration(250) .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .withEndAction(() -> indicationAreaAnimator + .withEndAction(() -> mKeyguardRootViewAnimator .translationY(0) .setDuration(450) .setInterpolator(mBounceInterpolator) .start()) .start(); + } else { + final List indicationAnimators = + mKeyguardBottomArea.getIndicationAreaAnimators(); + + for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) { + indicationAreaAnimator + .translationY(-mHintDistance) + .setDuration(250) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .withEndAction(() -> indicationAreaAnimator + .translationY(0) + .setDuration(450) + .setInterpolator(mBounceInterpolator) + .start()) + .start(); + } } + } private void setAnimator(ValueAnimator animator) { @@ -4352,7 +4412,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump goingToFullShade, mBarState); - setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade); + if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + setKeyguardVisibility(statusBarState, goingToFullShade); + } else { + setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade); + } // TODO: maybe add a listener for barstate mBarState = statusBarState; @@ -4589,7 +4653,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardStatusViewController.setAlpha(alpha); stackScroller.setAlpha(alpha); - mKeyguardBottomAreaInteractor.setAlpha(alpha); + if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + mKeyguardInteractor.setAlpha(alpha); + } else { + mKeyguardBottomAreaInteractor.setAlpha(alpha); + } mLockIconViewController.setAlpha(alpha); if (mKeyguardQsUserSwitchController != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt index d433814d7ce4f..34bbd1359df72 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt @@ -38,6 +38,7 @@ import com.android.systemui.statusbar.VibratorHelper * elements. A secondary concern is the interaction of the quick affordance elements with the * indication area between them, though the indication area is primarily controlled elsewhere. */ +@Deprecated("Deprecated as part of b/278057014") class KeyguardBottomAreaView @JvmOverloads constructor( @@ -53,6 +54,7 @@ constructor( defStyleRes, ) { + @Deprecated("Deprecated as part of b/278057014") interface MessageDisplayer { fun display(@StringRes stringResourceId: Int) } @@ -62,6 +64,7 @@ constructor( private var lockIconViewController: LockIconViewController? = null /** Initializes the view. */ + @Deprecated("Deprecated as part of b/278057014") fun init( viewModel: KeyguardBottomAreaViewModel, falsingManager: FalsingManager? = null, @@ -88,6 +91,7 @@ constructor( * Initializes this instance of [KeyguardBottomAreaView] based on the given instance of another * [KeyguardBottomAreaView] */ + @Deprecated("Deprecated as part of b/278057014") fun initFrom(oldBottomArea: KeyguardBottomAreaView) { // if it exists, continue to use the original ambient indication container // instead of the newly inflated one @@ -122,9 +126,11 @@ constructor( } /** Returns a list of animators to use to animate the indication areas. */ + @Deprecated("Deprecated as part of b/278057014") val indicationAreaAnimators: List get() = checkNotNull(binding).getIndicationAreaAnimators() + override fun hasOverlappingRendering(): Boolean { return false } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 07caf59833112..e8542aad9cd2b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -292,24 +292,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { assertThat(collectedValue).isInstanceOf(KeyguardQuickAffordanceModel.Hidden::class.java) } - @Test - fun quickAffordance_hiddenWhenQuickSettingsIsVisible() = - testScope.runTest { - repository.setQuickSettingsVisible(true) - quickAccessWallet.setState( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = ICON, - ) - ) - - val collectedValue = - collectLastValue( - underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) - ) - - assertThat(collectedValue()).isEqualTo(KeyguardQuickAffordanceModel.Hidden) - } - @Test fun quickAffordance_hiddenWhenUserIsInLockdownMode() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayoutTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayoutTest.kt index 2e97208e44deb..d97813b1f56f2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayoutTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayoutTest.kt @@ -26,6 +26,8 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.ui.view.KeyguardRootView import com.android.systemui.monet.utils.ArgbSubject.assertThat import org.junit.Before @@ -42,9 +44,10 @@ class DefaultLockscreenLayoutTest : SysuiTestCase() { private lateinit var defaultLockscreenLayout: DefaultLockscreenLayout private lateinit var rootView: KeyguardRootView @Mock private lateinit var authController: AuthController - @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var windowManager: WindowManager + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Before fun setup() { MockitoAnnotations.initMocks(this) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt index dff0f29091265..34d93fc8788e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt @@ -20,6 +20,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory @@ -45,6 +46,8 @@ import org.mockito.MockitoAnnotations class KeyguardIndicationAreaViewModelTest : SysuiTestCase() { @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper + @Mock private lateinit var shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel + @Mock private lateinit var featureFlags: FeatureFlags private lateinit var underTest: KeyguardIndicationAreaViewModel private lateinit var repository: FakeKeyguardRepository @@ -83,6 +86,8 @@ class KeyguardIndicationAreaViewModelTest : SysuiTestCase() { bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository), keyguardBottomAreaViewModel = bottomAreaViewModel, burnInHelperWrapper = burnInHelperWrapper, + shortcutsCombinedViewModel = shortcutsCombinedViewModel, + featureFlags = featureFlags, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt new file mode 100644 index 0000000000000..ef38d54dc42fc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -0,0 +1,681 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import android.app.admin.DevicePolicyManager +import android.content.Intent +import android.os.UserHandle +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dock.DockManagerFake +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager +import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory +import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import kotlin.math.max +import kotlin.math.min + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { + + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var expandable: Expandable + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var lockPatternUtils: LockPatternUtils + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var launchAnimator: DialogLaunchAnimator + @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger + + private lateinit var underTest: KeyguardQuickAffordancesCombinedViewModel + + private lateinit var testScope: TestScope + private lateinit var repository: FakeKeyguardRepository + private lateinit var homeControlsQuickAffordanceConfig: FakeKeyguardQuickAffordanceConfig + private lateinit var quickAccessWalletAffordanceConfig: FakeKeyguardQuickAffordanceConfig + private lateinit var qrCodeScannerAffordanceConfig: FakeKeyguardQuickAffordanceConfig + private lateinit var dockManager: DockManagerFake + private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository + private lateinit var keyguardInteractor: KeyguardInteractor + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + overrideResource(R.bool.custom_lockscreen_shortcuts_enabled, true) + overrideResource( + R.array.config_keyguardQuickAffordanceDefaults, + arrayOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + ":" + + BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS, + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END + ":" + + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) + ) + + homeControlsQuickAffordanceConfig = + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) + quickAccessWalletAffordanceConfig = + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) + qrCodeScannerAffordanceConfig = + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) + dockManager = DockManagerFake() + biometricSettingsRepository = FakeBiometricSettingsRepository() + val featureFlags = + FakeFeatureFlags().apply { + set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true) + set(Flags.FACE_AUTH_REFACTOR, true) + set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) + set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false) + } + + val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags) + keyguardInteractor = withDeps.keyguardInteractor + repository = withDeps.repository + + whenever(userTracker.userHandle).thenReturn(mock()) + whenever(lockPatternUtils.getStrongAuthForUser(ArgumentMatchers.anyInt())) + .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) + val testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( + context = context, + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, + ) + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = testScope.backgroundScope, + userTracker = userTracker, + clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), + userHandle = UserHandle.SYSTEM, + ) + val quickAffordanceRepository = + KeyguardQuickAffordanceRepository( + appContext = context, + scope = testScope.backgroundScope, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = testScope.backgroundScope, + backgroundDispatcher = testDispatcher, + secureSettings = FakeSettings(), + selectionsManager = localUserSelectionManager, + ), + configs = + setOf( + homeControlsQuickAffordanceConfig, + quickAccessWalletAffordanceConfig, + qrCodeScannerAffordanceConfig, + ), + dumpManager = mock(), + userHandle = UserHandle.SYSTEM, + ) + + underTest = KeyguardQuickAffordancesCombinedViewModel( + quickAffordanceInteractor = + KeyguardQuickAffordanceInteractor( + keyguardInteractor = keyguardInteractor, + lockPatternUtils = lockPatternUtils, + keyguardStateController = keyguardStateController, + userTracker = userTracker, + activityStarter = activityStarter, + featureFlags = featureFlags, + repository = { quickAffordanceRepository }, + launchAnimator = launchAnimator, + logger = logger, + devicePolicyManager = devicePolicyManager, + dockManager = dockManager, + biometricSettingsRepository = biometricSettingsRepository, + backgroundDispatcher = testDispatcher, + appContext = mContext, + ), + keyguardInteractor = keyguardInteractor + ) + } + + @Test + fun startButton_present_visibleModel_startsActivityOnClick() = + testScope.runTest { + repository.setKeyguardShowing(true) + val latest = collectLastValue(underTest.startButton) + + val testConfig = + TestConfig( + isVisible = true, + isClickable = true, + isActivated = true, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, + ) + + assertQuickAffordanceViewModel( + viewModel = latest(), + testConfig = testConfig, + configKey = configKey, + ) + } + + @Test + fun startButton_hiddenWhenDevicePolicyDisablesAllKeyguardFeatures() = + testScope.runTest { + whenever( + devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId) + ).thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL) + repository.setKeyguardShowing(true) + val latest by collectLastValue(underTest.startButton) + + val testConfig = + TestConfig( + isVisible = true, + isClickable = true, + isActivated = true, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, + ) + + assertQuickAffordanceViewModel( + viewModel = latest, + testConfig = + TestConfig( + isVisible = false, + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ), + configKey = configKey, + ) + } + + @Test + fun startButton_inPreviewMode_visibleEvenWhenKeyguardNotShowing() = + testScope.runTest { + underTest.onPreviewSlotSelected( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + ) + keyguardInteractor.previewMode.value = + KeyguardInteractor.PreviewMode( + isInPreviewMode = true, + shouldHighlightSelectedAffordance = true, + ) + + repository.setKeyguardShowing(false) + val latest = collectLastValue(underTest.startButton) + + val icon: Icon = mock() + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = + TestConfig( + isVisible = true, + isClickable = true, + isActivated = true, + icon = icon, + canShowWhileLocked = false, + intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ), + ) + + assertQuickAffordanceViewModel( + viewModel = latest(), + testConfig = + TestConfig( + isVisible = true, + isClickable = false, + isActivated = false, + icon = icon, + canShowWhileLocked = false, + intent = Intent("action"), + isSelected = true, + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ), + configKey = configKey, + ) + Truth.assertThat(latest()?.isSelected).isTrue() + } + + @Test + fun endButton_inHiglightedPreviewMode_dimmedWhenOtherIsSelected() = + testScope.runTest { + underTest.onPreviewSlotSelected( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + ) + keyguardInteractor.previewMode.value = + KeyguardInteractor.PreviewMode( + isInPreviewMode = true, + shouldHighlightSelectedAffordance = true, + ) + + repository.setKeyguardShowing(false) + val endButton = collectLastValue(underTest.endButton) + + val icon: Icon = mock() + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = + TestConfig( + isVisible = true, + isClickable = true, + isActivated = true, + icon = icon, + canShowWhileLocked = false, + intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_END, + testConfig = + TestConfig( + isVisible = true, + isClickable = true, + isActivated = true, + icon = icon, + canShowWhileLocked = false, + intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(), + ), + ) + + assertQuickAffordanceViewModel( + viewModel = endButton(), + testConfig = + TestConfig( + isVisible = true, + isClickable = false, + isActivated = false, + icon = icon, + canShowWhileLocked = false, + intent = Intent("action"), + isDimmed = true, + slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(), + ), + configKey = configKey, + ) + } + + @Test + fun endButton_present_visibleModel_doNothingOnClick() = + testScope.runTest { + repository.setKeyguardShowing(true) + val latest = collectLastValue(underTest.endButton) + + val config = + TestConfig( + isVisible = true, + isClickable = true, + icon = mock(), + canShowWhileLocked = false, + intent = + null, // This will cause it to tell the system that the click was handled. + slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_END, + testConfig = config, + ) + + assertQuickAffordanceViewModel( + viewModel = latest(), + testConfig = config, + configKey = configKey, + ) + } + + @Test + fun startButton_notPresent_modelIsHidden() = + testScope.runTest { + val latest = collectLastValue(underTest.startButton) + + val config = + TestConfig( + isVisible = false, + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = config, + ) + + assertQuickAffordanceViewModel( + viewModel = latest(), + testConfig = config, + configKey = configKey, + ) + } + + @Test + fun animateButtonReveal() = + testScope.runTest { + repository.setKeyguardShowing(true) + val testConfig = + TestConfig( + isVisible = true, + isClickable = true, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ) + + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, + ) + + val value = collectLastValue(underTest.startButton.map { it.animateReveal }) + + Truth.assertThat(value()).isFalse() + repository.setAnimateDozingTransitions(true) + Truth.assertThat(value()).isTrue() + repository.setAnimateDozingTransitions(false) + Truth.assertThat(value()).isFalse() + } + + @Test + fun isClickable_trueWhenAlphaAtThreshold() = + testScope.runTest { + repository.setKeyguardShowing(true) + repository.setKeyguardAlpha( + KeyguardQuickAffordancesCombinedViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + ) + + val testConfig = + TestConfig( + isVisible = true, + isClickable = true, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, + ) + + val latest = collectLastValue(underTest.startButton) + + assertQuickAffordanceViewModel( + viewModel = latest(), + testConfig = testConfig, + configKey = configKey, + ) + } + + @Test + fun isClickable_trueWhenAlphaAboveThreshold() = + testScope.runTest { + repository.setKeyguardShowing(true) + val latest = collectLastValue(underTest.startButton) + repository.setKeyguardAlpha( + min( + 1f, + KeyguardQuickAffordancesCombinedViewModel + .AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f + ), + ) + + val testConfig = + TestConfig( + isVisible = true, + isClickable = true, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, + ) + + assertQuickAffordanceViewModel( + viewModel = latest(), + testConfig = testConfig, + configKey = configKey, + ) + } + + @Test + fun isClickable_falseWhenAlphaBelowThreshold() = + testScope.runTest { + repository.setKeyguardShowing(true) + val latest = collectLastValue(underTest.startButton) + repository.setKeyguardAlpha( + max( + 0f, + KeyguardQuickAffordancesCombinedViewModel + .AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f + ), + ) + + val testConfig = + TestConfig( + isVisible = true, + isClickable = false, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, + ) + + assertQuickAffordanceViewModel( + viewModel = latest(), + testConfig = testConfig, + configKey = configKey, + ) + } + + @Test + fun isClickable_falseWhenAlphaAtZero() = + testScope.runTest { + repository.setKeyguardShowing(true) + val latest = collectLastValue(underTest.startButton) + repository.setKeyguardAlpha(0f) + + val testConfig = + TestConfig( + isVisible = true, + isClickable = false, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, + ) + + assertQuickAffordanceViewModel( + viewModel = latest(), + testConfig = testConfig, + configKey = configKey, + ) + } + + private suspend fun setUpQuickAffordanceModel( + position: KeyguardQuickAffordancePosition, + testConfig: TestConfig, + ): String { + val config = + when (position) { + KeyguardQuickAffordancePosition.BOTTOM_START -> homeControlsQuickAffordanceConfig + KeyguardQuickAffordancePosition.BOTTOM_END -> quickAccessWalletAffordanceConfig + } + + val lockScreenState = + if (testConfig.isVisible) { + if (testConfig.intent != null) { + config.onTriggeredResult = + KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity( + intent = testConfig.intent, + canShowWhileLocked = testConfig.canShowWhileLocked, + ) + } + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = testConfig.icon ?: error("Icon is unexpectedly null!"), + activationState = + when (testConfig.isActivated) { + true -> ActivationState.Active + false -> ActivationState.Inactive + } + ) + } else { + KeyguardQuickAffordanceConfig.LockScreenState.Hidden + } + config.setState(lockScreenState) + return "${position.toSlotId()}::${config.key}" + } + + private fun assertQuickAffordanceViewModel( + viewModel: KeyguardQuickAffordanceViewModel?, + testConfig: TestConfig, + configKey: String, + ) { + checkNotNull(viewModel) + Truth.assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible) + Truth.assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable) + Truth.assertThat(viewModel.isActivated).isEqualTo(testConfig.isActivated) + Truth.assertThat(viewModel.isSelected).isEqualTo(testConfig.isSelected) + Truth.assertThat(viewModel.isDimmed).isEqualTo(testConfig.isDimmed) + Truth.assertThat(viewModel.slotId).isEqualTo(testConfig.slotId) + if (testConfig.isVisible) { + Truth.assertThat(viewModel.icon).isEqualTo(testConfig.icon) + viewModel.onClicked.invoke( + KeyguardQuickAffordanceViewModel.OnClickedParameters( + configKey = configKey, + expandable = expandable, + slotId = viewModel.slotId, + ) + ) + if (testConfig.intent != null) { + Truth.assertThat( + Mockito.mockingDetails(activityStarter).invocations + ).hasSize(1) + } else { + Mockito.verifyZeroInteractions(activityStarter) + } + } else { + Truth.assertThat(viewModel.isVisible).isFalse() + } + } + + private data class TestConfig( + val isVisible: Boolean, + val isClickable: Boolean = false, + val isActivated: Boolean = false, + val icon: Icon? = null, + val canShowWhileLocked: Boolean = false, + val intent: Intent? = null, + val isSelected: Boolean = false, + val isDimmed: Boolean = false, + val slotId: String = "" + ) { + init { + check(!isVisible || icon != null) { "Must supply non-null icon if visible!" } + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt new file mode 100644 index 0000000000000..05e933b9d572b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory +import com.google.common.truth.Truth +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardRootViewModelTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardRootViewModel + private lateinit var testScope: TestScope + private lateinit var repository: FakeKeyguardRepository + private lateinit var keyguardInteractor: KeyguardInteractor + @Mock private lateinit var keyguardQuickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + val testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + + val featureFlags = + FakeFeatureFlags().apply { + set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true) + set(Flags.FACE_AUTH_REFACTOR, true) + } + + val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags) + keyguardInteractor = withDeps.keyguardInteractor + repository = withDeps.repository + + underTest = KeyguardRootViewModel( + keyguardInteractor, + keyguardQuickAffordancesCombinedViewModel, + ) + } + + @Test + fun alpha() = + testScope.runTest { + val value = collectLastValue(underTest.alpha) + + Truth.assertThat(value()).isEqualTo(1f) + repository.setKeyguardAlpha(0.1f) + Truth.assertThat(value()).isEqualTo(0.1f) + repository.setKeyguardAlpha(0.5f) + Truth.assertThat(value()).isEqualTo(0.5f) + repository.setKeyguardAlpha(0.2f) + Truth.assertThat(value()).isEqualTo(0.2f) + repository.setKeyguardAlpha(0f) + Truth.assertThat(value()).isEqualTo(0f) + } + + @Test + fun alpha_inPreviewMode_doesNotChange() = + testScope.runTest { + val value = collectLastValue(underTest.alpha) + underTest.enablePreviewMode( + initiallySelectedSlotId = null, + shouldHighlightSelectedAffordance = false, + ) + + Truth.assertThat(value()).isEqualTo(1f) + repository.setKeyguardAlpha(0.1f) + Truth.assertThat(value()).isEqualTo(1f) + repository.setKeyguardAlpha(0.5f) + Truth.assertThat(value()).isEqualTo(1f) + repository.setKeyguardAlpha(0.2f) + Truth.assertThat(value()).isEqualTo(1f) + repository.setKeyguardAlpha(0f) + Truth.assertThat(value()).isEqualTo(1f) + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 202c7bf0d5fdf..47ca49d03cc7d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -97,6 +97,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteracto import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.ui.view.KeyguardRootView; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; @@ -314,6 +315,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock private ShadeInteractor mShadeInteractor; @Mock private JavaAdapter mJavaAdapter; @Mock private CastController mCastController; + @Mock private KeyguardRootView mKeyguardRootView; protected final int mMaxUdfpsBurnInOffsetY = 5; protected KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; @@ -618,7 +620,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mKeyguardInteractor, mActivityStarter, mKeyguardViewConfigurator, - mKeyguardFaceAuthInteractor); + mKeyguardFaceAuthInteractor, + mKeyguardRootView); mNotificationPanelViewController.initDependencies( mCentralSurfaces, null, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 8428566270dea..e6894d7240f87 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -22,6 +22,7 @@ import com.android.systemui.common.shared.model.Position import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.DozeTransitionModel +import com.android.systemui.keyguard.shared.model.KeyguardRootViewVisibilityState import com.android.systemui.keyguard.shared.model.ScreenModel import com.android.systemui.keyguard.shared.model.ScreenState import com.android.systemui.keyguard.shared.model.StatusBarState @@ -115,6 +116,20 @@ class FakeKeyguardRepository : KeyguardRepository { private val _isQuickSettingsVisible = MutableStateFlow(false) override val isQuickSettingsVisible: Flow = _isQuickSettingsVisible.asStateFlow() + private val _keyguardAlpha = MutableStateFlow(1f) + override val keyguardAlpha: StateFlow = _keyguardAlpha + + private val _keyguardRootViewVisibility = + MutableStateFlow( + KeyguardRootViewVisibilityState( + 0, + goingToFullShade = false, + occlusionTransitionRunning = false + ) + ) + override val keyguardRootViewVisibility: Flow = + _keyguardRootViewVisibility.asStateFlow() + override fun setQuickSettingsVisible(isVisible: Boolean) { _isQuickSettingsVisible.value = isVisible } @@ -132,6 +147,8 @@ class FakeKeyguardRepository : KeyguardRepository { _animateBottomAreaDozingTransitions.tryEmit(animate) } + + @Deprecated("Deprecated as part of b/278057014") override fun setBottomAreaAlpha(alpha: Float) { _bottomAreaAlpha.value = alpha } @@ -223,4 +240,18 @@ class FakeKeyguardRepository : KeyguardRepository { override fun isUdfpsSupported(): Boolean { return _isUdfpsSupported.value } + + override fun setKeyguardAlpha(alpha: Float) { + _keyguardAlpha.value = alpha + } + + override fun setKeyguardVisibility( + statusBarState: Int, + goingToFullShade: Boolean, + occlusionTransitionRunning: Boolean + ) { + _keyguardRootViewVisibility.value = KeyguardRootViewVisibilityState( + statusBarState, goingToFullShade, occlusionTransitionRunning + ) + } }