diff options
| author | 2023-07-27 15:52:17 +0000 | |
|---|---|---|
| committer | 2023-07-27 15:52:17 +0000 | |
| commit | c45ff2e9002228103f350ca972b14ab51a99bd4d (patch) | |
| tree | c059b6fc64643d0b65fdc046e235143b493c2323 | |
| parent | 651440d116179eb4d7bdf82bd2c0d988a216ac8c (diff) | |
| parent | a3a7fabf3bd241a7ba46bd35903d0d61181a1355 (diff) | |
Merge "Split up KeyguardBottomArea" into udc-qpr-dev
32 files changed, 2821 insertions, 117 deletions
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml index a33625212d34..d4b73a4e3de0 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" /> + <include layout="@layout/status_bar_expanded" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="invisible" /> + <!-- Root for all keyguard content. It was previously located within the shade. --> <com.android.systemui.keyguard.ui.view.KeyguardRootView android:id="@id/keyguard_root_view" android:layout_width="match_parent" android:layout_height="match_parent" /> - <include layout="@layout/status_bar_expanded" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="invisible" /> - <!-- Shared container for the notification stack. Can be positioned by either the keyguard_root_view or notification_panel --> <com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 9588498dddb3..de8287e5e8cb 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -823,6 +823,12 @@ <dimen name="keyguard_affordance_fixed_radius">24dp</dimen> <dimen name="keyguard_affordance_fixed_padding">12dp</dimen> + <!-- The width/height/padding of the keyguard settings popup menu --> + <dimen name="keyguard_settings_popup_menu_icon_height">24dp</dimen> + <dimen name="keyguard_settings_popup_menu_icon_width">24dp</dimen> + <dimen name="keyguard_settings_popup_menu_icon_end_margin">8dp</dimen> + <dimen name="keyguard_settings_popup_menu_padding">12dp</dimen> + <!-- Amount the button should shake when it's not long-pressed for long enough. --> <dimen name="keyguard_affordance_shake_amplitude">8dp</dimen> diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 03a270ebea68..4495943c72c9 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<View>(R.id.keyguard_indication_area)?.let { - legacyParent.removeView(it) - } - } else { + if (!featureFlags.isEnabled(Flags.MIGRATE_INDICATION_AREA)) { keyguardRootView.findViewById<View?>(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<View>(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<View?>(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<View?>(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<View>(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<View?>(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<View?>(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 7475c4251211..9ee990243b96 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<Float> + val keyguardAlpha: StateFlow<Float> + /** * 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<Boolean> + /** Represents the current state of the KeyguardRootView visibility */ + val keyguardRootViewVisibility: Flow<KeyguardRootViewVisibilityState> + /** Receive an event for doze time tick */ val dozeTimeTick: Flow<Unit> @@ -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<KeyguardRootViewVisibilityState> = + _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 1553525915d5..cc159168009e 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<Boolean> = bouncerRepository.alternateBouncerVisible /** Observable for the [StatusBarState] */ val statusBarState: Flow<StatusBarState> = repository.statusBarState - /** Whether or not quick settings or quick quick settings are showing. */ - val isQuickSettingsVisible: Flow<Boolean> = 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<Unit> = configurationRepository.onAnyConfigurationChange + /** Represents the current state of the KeyguardRootView visibility */ + val keyguardRootViewVisibilityState: Flow<KeyguardRootViewVisibilityState> = + repository.keyguardRootViewVisibility + + /** The position of the keyguard clock. */ + val clockPosition: Flow<Position> = repository.clockPosition + + val keyguardAlpha: Flow<Float> = repository.keyguardAlpha + + /** Whether to animate the next doze mode transition. */ + val animateDozingTransitions: Flow<Boolean> = repository.animateBottomAreaDozingTransitions + fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> { 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 40e0604ae1b3..a48684381345 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 000000000000..9a57aefba329 --- /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 000000000000..d6883dddba26 --- /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<ViewPropertyAnimator> + + /** 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<ViewPropertyAnimator> { + 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 db84268f7c58..a0a2abe83315 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<ViewGroup.LayoutParams> { @@ -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<KeyguardQuickAffordanceViewModel>, @@ -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 02e6765fa1d3..a385a0e5754b 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 000000000000..63a67913d6cd --- /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<KeyguardQuickAffordanceViewModel>, + alpha: Flow<Float>, + 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<ViewGroup.LayoutParams> { + 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<KeyguardQuickAffordanceViewModel>, + alphaFlow: Flow<Float>, + ) { + 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 1db596b346b3..19f622bab7ed 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 000000000000..162c109ee299 --- /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<LaunchableLinearLayout>(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 b92d10474ccd..3e6e1585ca6d 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<DisposableHandle>() private var isDestroyed = false + private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>() + 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 0077f2d68c7e..65fe99045387 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 baaeb60f364e..6be45c79e37e 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<View>(R.id.start_button) ?: return + val rightShortcut = rootView.findViewById<View>(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<MarginLayoutParams> { + marginStart = horizontalOffsetMargin + bottomMargin = verticalOffsetMargin + } + setPadding(padding) + } + + rightShortcut.apply { + updateLayoutParams<MarginLayoutParams> { + 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<View>(R.id.ambient_indication_container) ?: return + val lockIconView = rootView.findViewById<View>(R.id.lock_icon_view) ?: return + val indicationArea = rootView.findViewById<View>(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<LaunchableLinearLayout>(R.id.keyguard_settings_button) ?: return + val icon = popupMenu.findViewById<ImageView>(R.id.icon) ?: return + val textView = popupMenu.findViewById<TextView>(R.id.text) ?: return + val horizontalOffsetMargin = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_horizontal_offset) + + icon.updateLayoutParams<LinearLayout.LayoutParams> { + 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<LinearLayout.LayoutParams> { + height = WRAP_CONTENT + width = WRAP_CONTENT + } + + popupMenu.updateLayoutParams<MarginLayoutParams> { + 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 9bc630265a3f..6973cd914aad 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 00f93e33f370..c0447af0590d 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 000000000000..569762d67c5f --- /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<View>(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<View>(R.id.lock_icon_view) ?: return + val lockIcon = lockIconView.findViewById<View>(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<View>(R.id.start_button) ?: return + val rightShortcut = rootView.findViewById<View>(R.id.end_button) ?: return + val lockIconView = rootView.findViewById<View>(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<ViewGroup.MarginLayoutParams> { + marginStart = horizontalOffsetMargin + bottomMargin = verticalOffsetMargin + } + setPadding(padding) + } + + rightShortcut.apply { + updateLayoutParams<ViewGroup.MarginLayoutParams> { + 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<View>(R.id.ambient_indication_container) ?: return + val lockIconView = rootView.findViewById<View>(R.id.lock_icon_view) ?: return + val indicationArea = rootView.findViewById<View>(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<LaunchableLinearLayout>(R.id.keyguard_settings_button) ?: return + val icon = popupMenu.findViewById<ImageView>(R.id.icon) ?: return + val textView = popupMenu.findViewById<TextView>(R.id.text) ?: return + val horizontalOffsetMargin = + context.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_horizontal_offset) + + icon.updateLayoutParams<LinearLayout.LayoutParams> { + 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<LinearLayout.LayoutParams> { + height = WRAP_CONTENT + width = WRAP_CONTENT + } + + popupMenu.updateLayoutParams<ViewGroup.MarginLayoutParams> { + 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 000000000000..dd3967aed99d --- /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<Float> = + 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<Float> { + 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 f1ceaaa391f5..980cc1b8ebe1 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<Boolean> = - 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<Float> = - 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<Float> { 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 000000000000..56a98455d8b3 --- /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<Boolean> = + 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<KeyguardQuickAffordanceViewModel> = + button(KeyguardQuickAffordancePosition.BOTTOM_START) + + /** An observable for the view-model of the "end button" quick affordance. */ + val endButton: Flow<KeyguardQuickAffordanceViewModel> = + button(KeyguardQuickAffordancePosition.BOTTOM_END) + + /** + * 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<KeyguardQuickAffordanceViewModel> { + 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 000000000000..316ca771bf8f --- /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<KeyguardRootViewVisibilityState> = + keyguardInteractor.keyguardRootViewVisibilityState + + /** An observable for the alpha level for the entire keyguard root view. */ + val alpha: Flow<Float> = + 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 ed7cbffc880b..773f35e0c665 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<ViewPropertyAnimator> 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<ViewPropertyAnimator> 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 d433814d7ce4..34bbd1359df7 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<ViewPropertyAnimator> 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 07caf5983311..e8542aad9cd2 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 @@ -293,24 +293,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { } @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 { biometricSettingsRepository.setIsUserInLockdown(true) 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 2e97208e44de..d97813b1f56f 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 dff0f2909126..34d93fc8788e 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 000000000000..ef38d54dc42f --- /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<UserFileManager>().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 000000000000..05e933b9d572 --- /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 202c7bf0d5fd..47ca49d03cc7 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 8428566270de..e6894d7240f8 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<Boolean> = _isQuickSettingsVisible.asStateFlow() + private val _keyguardAlpha = MutableStateFlow(1f) + override val keyguardAlpha: StateFlow<Float> = _keyguardAlpha + + private val _keyguardRootViewVisibility = + MutableStateFlow( + KeyguardRootViewVisibilityState( + 0, + goingToFullShade = false, + occlusionTransitionRunning = false + ) + ) + override val keyguardRootViewVisibility: Flow<KeyguardRootViewVisibilityState> = + _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 + ) + } } |