Merge "Split up KeyguardBottomArea" into udc-qpr-dev
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 9588498..de8287e 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -823,6 +823,12 @@
<dimen name="keyguard_affordance_fixed_radius">24dp</dimen>
<dimen name="keyguard_affordance_fixed_padding">12dp</dimen>
+ <!-- The width/height/padding of the keyguard settings popup menu -->
+ <dimen name="keyguard_settings_popup_menu_icon_height">24dp</dimen>
+ <dimen name="keyguard_settings_popup_menu_icon_width">24dp</dimen>
+ <dimen name="keyguard_settings_popup_menu_icon_end_margin">8dp</dimen>
+ <dimen name="keyguard_settings_popup_menu_padding">12dp</dimen>
+
<!-- Amount the button should shake when it's not long-pressed for long enough. -->
<dimen name="keyguard_affordance_shake_amplitude">8dp</dimen>
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 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
+ )
+ }
}