diff options
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 +        ) +    }  }  |