Split up KeyguardBottomArea
This splits the KeyguardBottomArea into many pieces:
- Left shortcut
- Right shortcut
- Ambient indication container
- Customize Lockscreen button
Each of the elements above are now able to be independently laid out.
The alpha of the entire KeyguardRootView can now be easily modified, rather than having to adjust the alpha of each sub-element individually.
Fixes: 278057014
Test: atest KeyguardQuickAffordancesCombinedViewModelTest.kt
Test: atest KeyguardRootViewModelTest.kt
Change-Id: I098174809144cdf09dd71665ab724f8c508ae9e9
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index a336252..d4b73a4 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 5a15dce..d7a4b25 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -821,6 +821,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 03a270e..4495943 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.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 @@
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 @@
}
}
- 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 @@
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 @@
}
}
- 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 7475c42..9ee9902 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.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 @@
*/
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 @@
/** 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 @@
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 @@
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 @@
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 @@
_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 1553525..cc15916 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 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.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 @@
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 @@
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 @@
/** 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 @@
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 40e0604..a486843 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 @@
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 0000000..9a57aef
--- /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 0000000..d6883dd
--- /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 db84268..a0a2abe 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 @@
* 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 @@
* 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 @@
}
/** 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 @@
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 @@
}
}
+ //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
viewModel.endButton.collect { buttonModel ->
updateButton(
@@ -180,6 +187,7 @@
}
}
+ //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
updateButtonAlpha(
view = startButton,
@@ -188,6 +196,7 @@
)
}
+ //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
updateButtonAlpha(
view = endButton,
@@ -213,6 +222,7 @@
}
}
+ //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
configurationBasedDimensions.collect { dimensions ->
startButton.updateLayoutParams<ViewGroup.LayoutParams> {
@@ -287,6 +297,8 @@
}
}
+ @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 @@
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 @@
.collect { view.alpha = it }
}
+ @Deprecated("Deprecated as part of b/278057014")
private fun View.animateVisibility(visible: Boolean) {
animate()
.withStartAction {
@@ -422,6 +437,8 @@
.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 @@
}
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 @@
}
}
+ @Deprecated("Deprecated as part of b/278057014")
private fun loadFromResources(view: View): ConfigurationBasedDimensions {
return ConfigurationBasedDimensions(
defaultBurnInPreventionYOffsetPx =
@@ -491,6 +510,7 @@
)
}
+ @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 @@
)
}
+ @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 02e6765..a385a0e 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 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 @@
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 @@
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 0000000..63a6791
--- /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 1db596b..19f622b 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 @@
@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 @@
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 0000000..162c109
--- /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 b92d104..3e6e158 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.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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
false,
) as KeyguardBottomAreaView
bottomAreaView.init(
- viewModel = bottomAreaViewModel,
+ viewModel = bottomAreaViewModel,
)
parentView.addView(
bottomAreaView,
@@ -270,6 +320,32 @@
)
}
+ 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 0077f2d..65fe990 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 @@
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 @@
init {
addIndicationTextArea()
addLockIconView()
+ addAmbientIndicationArea()
+ addLeftShortcut()
+ addRightShortcut()
+ addSettingsPopupMenu()
}
private fun addIndicationTextArea() {
@@ -47,4 +56,65 @@
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 baaeb60..6be45c7 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.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 @@
}
}
+ 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 9bc6302..6973cd9 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 @@
return true
}
- fun layoutViews() {
- layout?.layoutViews(keyguardRootView)
+ fun layoutViews(rootView: KeyguardRootView = keyguardRootView) {
+ layout?.layoutViews(rootView)
}
companion object {
@@ -85,7 +85,13 @@
.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 00f93e3..c0447af 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 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 0000000..569762d
--- /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 0000000..dd3967a
--- /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 f1ceaaa..980cc1b 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 @@
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 @@
/** 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 0000000..56a98455
--- /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 0000000..316ca77
--- /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 ed7cbff..773f35e 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.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 @@
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 @@
private boolean mTracking;
private boolean mIsTrackingExpansionFromStatusBar;
private boolean mHintAnimationRunning;
+ @Deprecated
private KeyguardBottomAreaView mKeyguardBottomArea;
private boolean mExpanding;
private boolean mSplitShadeEnabled;
@@ -739,7 +742,8 @@
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 @@
}
});
mAlternateBouncerInteractor = alternateBouncerInteractor;
+ mKeyguardRootView = keyguardRootView;
dumpManager.registerDumpable(this);
}
@@ -1029,7 +1034,9 @@
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 @@
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 @@
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 @@
}
private void initBottomArea() {
- mKeyguardBottomArea.init(
+ if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+ mKeyguardBottomArea.init(
mKeyguardBottomAreaViewModel,
mFalsingManager,
mLockIconViewController,
@@ -1361,8 +1377,9 @@
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 @@
return mHintAnimationRunning || mUnlockedScreenOffAnimationController.isAnimationPlaying();
}
+ @Deprecated
private void setKeyguardBottomArea(KeyguardBottomAreaView keyguardBottomArea) {
mKeyguardBottomArea = keyguardBottomArea;
}
@@ -1521,8 +1539,13 @@
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 @@
}
}
+ 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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
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 d433814..34bbd13 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 @@
* 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 @@
defStyleRes,
) {
+ @Deprecated("Deprecated as part of b/278057014")
interface MessageDisplayer {
fun display(@StringRes stringResourceId: Int)
}
@@ -62,6 +64,7 @@
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 @@
* 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 @@
}
/** 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 07caf59..e8542aad 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 @@
}
@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 2e97208..d97813b 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.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 @@
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 dff0f29..34d93fc 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 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 @@
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 @@
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 0000000..ef38d54
--- /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 0000000..05e933b
--- /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 202c7bf..47ca49d0 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.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 @@
@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 @@
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 8428566..e6894d7 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.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 @@
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 @@
_animateBottomAreaDozingTransitions.tryEmit(animate)
}
+
+ @Deprecated("Deprecated as part of b/278057014")
override fun setBottomAreaAlpha(alpha: Float) {
_bottomAreaAlpha.value = alpha
}
@@ -223,4 +240,18 @@
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
+ )
+ }
}