diff --git a/packages/SystemUI/res-keyguard/drawable/ic_palette.xml b/packages/SystemUI/res-keyguard/drawable/ic_palette.xml
new file mode 100644
index 0000000000000..cbea369c0236d
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/ic_palette.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/packages/SystemUI/res/anim/keyguard_settings_popup_ease_out_interpolator.xml b/packages/SystemUI/res/anim/keyguard_settings_popup_ease_out_interpolator.xml
new file mode 100644
index 0000000000000..8c2937c8c4836
--- /dev/null
+++ b/packages/SystemUI/res/anim/keyguard_settings_popup_ease_out_interpolator.xml
@@ -0,0 +1,22 @@
+
+
+
diff --git a/packages/SystemUI/res/anim/long_press_lock_screen_popup_enter.xml b/packages/SystemUI/res/anim/long_press_lock_screen_popup_enter.xml
new file mode 100644
index 0000000000000..5fa88224968b9
--- /dev/null
+++ b/packages/SystemUI/res/anim/long_press_lock_screen_popup_enter.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/anim/long_press_lock_screen_popup_exit.xml b/packages/SystemUI/res/anim/long_press_lock_screen_popup_exit.xml
new file mode 100644
index 0000000000000..a6938defd4be1
--- /dev/null
+++ b/packages/SystemUI/res/anim/long_press_lock_screen_popup_exit.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml b/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml
index 3807b92ae39df..a0ceb81d42f44 100644
--- a/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml
+++ b/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml
@@ -17,17 +17,17 @@
+ android:color="#4d000000">
-
-
+
-
-
-
+
+
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index 4048a39344bd6..c0f7029449a1b 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -20,7 +20,7 @@
android:id="@+id/keyguard_bottom_area"
android:layout_height="match_parent"
android:layout_width="match_parent"
- android:outlineProvider="none" >
+ android:outlineProvider="none" >
-
+
-
+
+
+
+
+
+
+
+
+
-
+ android:padding="12dp">
@@ -42,9 +40,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
- android:textColor="?android:attr/textColorPrimary"
+ android:textColor="?androidprv:attr/materialColorOnSecondaryFixed"
android:textSize="14sp"
android:maxLines="1"
android:ellipsize="end" />
-
\ No newline at end of file
+
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index a11ffcda98308..f1fca76035712 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -120,10 +120,6 @@
/>
-
-
+
+
24dp
16dp
-
- 96dp
-
-
0dp
0dp
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f1777f84cd6d8..74ae954a539c8 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3057,13 +3057,17 @@
Close
- Lock screen settings
+ Customize lock screen
Wi-Fi not available
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 064cea112b5a1..2098aea87ab24 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1399,4 +1399,9 @@
+
+
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardSettingsPopupMenuModel.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/MotionEventExt.kt
similarity index 50%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardSettingsPopupMenuModel.kt
rename to packages/SystemUI/src/com/android/systemui/common/ui/view/MotionEventExt.kt
index 7c61e7108265e..26fc36dd3c9e0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardSettingsPopupMenuModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/MotionEventExt.kt
@@ -5,26 +5,24 @@
* 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
+ * 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.domain.model
+package com.android.systemui.common.ui.view
-import com.android.systemui.common.shared.model.Position
+import android.util.MathUtils
+import android.view.MotionEvent
-/** Models a settings popup menu for the lock screen. */
-data class KeyguardSettingsPopupMenuModel(
- /** Where the menu should be anchored, roughly in screen space. */
- val position: Position,
- /** Callback to invoke when the menu gets clicked by the user. */
- val onClicked: () -> Unit,
- /** Callback to invoke when the menu gets dismissed by the user. */
- val onDismissed: () -> Unit,
-)
+/** Returns the distance from the position of this [MotionEvent] and the given coordinates. */
+fun MotionEvent.distanceFrom(
+ x: Float,
+ y: Float,
+): Float {
+ return MathUtils.dist(this.x, this.y, x, y)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt
index 6525a13fc44f7..ea6700e92731c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt
@@ -17,29 +17,29 @@
package com.android.systemui.keyguard.domain.interactor
-import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.view.accessibility.AccessibilityManager
+import androidx.annotation.VisibleForTesting
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
-import com.android.systemui.R
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.shared.model.Position
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.KeyguardRepository
-import com.android.systemui.keyguard.domain.model.KeyguardSettingsPopupMenuModel
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
/** Business logic for use-cases related to the keyguard long-press feature. */
@OptIn(ExperimentalCoroutinesApi::class)
@@ -54,18 +55,16 @@ import kotlinx.coroutines.flow.stateIn
class KeyguardLongPressInteractor
@Inject
constructor(
- @Application unsafeContext: Context,
- @Application scope: CoroutineScope,
+ @Application private val scope: CoroutineScope,
transitionInteractor: KeyguardTransitionInteractor,
repository: KeyguardRepository,
- private val activityStarter: ActivityStarter,
private val logger: UiEventLogger,
private val featureFlags: FeatureFlags,
broadcastDispatcher: BroadcastDispatcher,
+ private val accessibilityManager: AccessibilityManagerWrapper,
) {
- private val appContext = unsafeContext.applicationContext
-
- private val _isLongPressHandlingEnabled: StateFlow =
+ /** Whether the long-press handling feature should be enabled. */
+ val isLongPressHandlingEnabled: StateFlow =
if (isFeatureEnabled()) {
combine(
transitionInteractor.finishedKeyguardState.map {
@@ -84,19 +83,35 @@ constructor(
initialValue = false,
)
- /** Whether the long-press handling feature should be enabled. */
- val isLongPressHandlingEnabled: Flow = _isLongPressHandlingEnabled
-
- private val _menu = MutableStateFlow(null)
- /** Model for a menu that should be shown; `null` when no menu should be shown. */
- val menu: Flow =
- isLongPressHandlingEnabled.flatMapLatest { isEnabled ->
- if (isEnabled) {
- _menu
- } else {
- flowOf(null)
+ private val _isMenuVisible = MutableStateFlow(false)
+ /** Model for whether the menu should be shown. */
+ val isMenuVisible: StateFlow =
+ isLongPressHandlingEnabled
+ .flatMapLatest { isEnabled ->
+ if (isEnabled) {
+ _isMenuVisible.asStateFlow()
+ } else {
+ // Reset the state so we don't see a menu when long-press handling is enabled
+ // again in the future.
+ _isMenuVisible.value = false
+ flowOf(false)
+ }
}
- }
+ .stateIn(
+ scope = scope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
+
+ private val _shouldOpenSettings = MutableStateFlow(false)
+ /**
+ * Whether the long-press accessible "settings" flow should be opened.
+ *
+ * Note that [onSettingsShown] must be invoked to consume this, once the settings are opened.
+ */
+ val shouldOpenSettings = _shouldOpenSettings.asStateFlow()
+
+ private var delayedHideMenuJob: Job? = null
init {
if (isFeatureEnabled()) {
@@ -110,15 +125,46 @@ constructor(
}
/** Notifies that the user has long-pressed on the lock screen. */
- fun onLongPress(x: Int, y: Int) {
- if (!_isLongPressHandlingEnabled.value) {
+ fun onLongPress() {
+ if (!isLongPressHandlingEnabled.value) {
return
}
- showMenu(
- x = x,
- y = y,
- )
+ if (featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP)) {
+ showSettings()
+ } else {
+ showMenu()
+ }
+ }
+
+ /** Notifies that the user has touched outside of the pop-up. */
+ fun onTouchedOutside() {
+ hideMenu()
+ }
+
+ /** Notifies that the user has started a touch gesture on the menu. */
+ fun onMenuTouchGestureStarted() {
+ cancelAutomaticMenuHiding()
+ }
+
+ /** Notifies that the user has started a touch gesture on the menu. */
+ fun onMenuTouchGestureEnded(isClick: Boolean) {
+ if (isClick) {
+ hideMenu()
+ logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
+ showSettings()
+ } else {
+ scheduleAutomaticMenuHiding()
+ }
+ }
+
+ /** Notifies that the settings UI has been shown, consuming the event to show it. */
+ fun onSettingsShown() {
+ _shouldOpenSettings.value = false
+ }
+
+ private fun showSettings() {
+ _shouldOpenSettings.value = true
}
private fun isFeatureEnabled(): Boolean {
@@ -126,51 +172,40 @@ constructor(
featureFlags.isEnabled(Flags.REVAMPED_WALLPAPER_UI)
}
- /** Updates application state to ask to show the menu at the given coordinates. */
- private fun showMenu(
- x: Int,
- y: Int,
- ) {
- _menu.value =
- KeyguardSettingsPopupMenuModel(
- position =
- Position(
- x = x,
- y = y,
- ),
- onClicked = {
- hideMenu()
- navigateToLockScreenSettings()
- },
- onDismissed = { hideMenu() },
- )
+ /** Updates application state to ask to show the menu. */
+ private fun showMenu() {
+ _isMenuVisible.value = true
+ scheduleAutomaticMenuHiding()
logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
}
+ private fun scheduleAutomaticMenuHiding() {
+ cancelAutomaticMenuHiding()
+ delayedHideMenuJob =
+ scope.launch {
+ delay(timeOutMs())
+ hideMenu()
+ }
+ }
+
/** Updates application state to ask to hide the menu. */
private fun hideMenu() {
- _menu.value = null
+ cancelAutomaticMenuHiding()
+ _isMenuVisible.value = false
}
- /** Opens the wallpaper picker screen after the device is unlocked by the user. */
- private fun navigateToLockScreenSettings() {
- logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
- activityStarter.dismissKeyguardThenExecute(
- /* action= */ {
- appContext.startActivity(
- Intent(Intent.ACTION_SET_WALLPAPER).apply {
- flags = Intent.FLAG_ACTIVITY_NEW_TASK
- appContext
- .getString(R.string.config_wallpaperPickerPackage)
- .takeIf { it.isNotEmpty() }
- ?.let { packageName -> setPackage(packageName) }
- }
- )
- true
- },
- /* cancel= */ {},
- /* afterKeyguardGone= */ true,
- )
+ private fun cancelAutomaticMenuHiding() {
+ delayedHideMenuJob?.cancel()
+ delayedHideMenuJob = null
+ }
+
+ private fun timeOutMs(): Long {
+ return accessibilityManager
+ .getRecommendedTimeoutMillis(
+ DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS.toInt(),
+ AccessibilityManager.FLAG_CONTENT_ICONS or AccessibilityManager.FLAG_CONTENT_TEXT,
+ )
+ .toLong()
}
enum class LogEvents(
@@ -184,4 +219,8 @@ constructor(
override fun getId() = _id
}
+
+ companion object {
+ @VisibleForTesting const val DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS = 5000L
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt
new file mode 100644
index 0000000000000..568db2f543b69
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.os.VibrationEffect
+import kotlin.time.Duration.Companion.milliseconds
+
+object KeyguardBottomAreaVibrations {
+
+ val ShakeAnimationDuration = 300.milliseconds
+ const val ShakeAnimationCycles = 5f
+
+ private const val SmallVibrationScale = 0.3f
+ private const val BigVibrationScale = 0.6f
+
+ val Shake =
+ VibrationEffect.startComposition()
+ .apply {
+ val vibrationDelayMs =
+ (ShakeAnimationDuration.inWholeMilliseconds / ShakeAnimationCycles * 2).toInt()
+ val vibrationCount = ShakeAnimationCycles.toInt() * 2
+ repeat(vibrationCount) {
+ addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ SmallVibrationScale,
+ vibrationDelayMs,
+ )
+ }
+ }
+ .compose()
+
+ val Activated =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ BigVibrationScale,
+ 0,
+ )
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
+ 0.1f,
+ 0,
+ )
+ .compose()
+
+ val Deactivated =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ BigVibrationScale,
+ 0,
+ )
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
+ 0.1f,
+ 0,
+ )
+ .compose()
+}
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 d63636c6fccce..68ac7e1c71e5e 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
@@ -17,41 +17,42 @@
package com.android.systemui.keyguard.ui.binder
import android.annotation.SuppressLint
+import android.content.Intent
+import android.graphics.Rect
import android.graphics.drawable.Animatable2
-import android.os.VibrationEffect
import android.util.Size
import android.util.TypedValue
-import android.view.MotionEvent
import android.view.View
-import android.view.ViewConfiguration
import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import android.widget.ImageView
import android.widget.TextView
-import androidx.core.animation.CycleInterpolator
-import androidx.core.animation.ObjectAnimator
+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.settingslib.Utils
import com.android.systemui.R
+import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.Interpolators
+import com.android.systemui.animation.view.LaunchableLinearLayout
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.common.ui.binder.TextViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
-import kotlin.math.pow
-import kotlin.math.sqrt
-import kotlin.time.Duration.Companion.milliseconds
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.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -91,15 +92,20 @@ object KeyguardBottomAreaViewBinder {
* icon
*/
fun shouldConstrainToTopOfLockIcon(): Boolean
+
+ /** Destroys this binding, releases resources, and cancels any coroutines. */
+ fun destroy()
}
/** Binds the view to the view-model, continuing to update the former based on the latter. */
+ @SuppressLint("ClickableViewAccessibility")
@JvmStatic
fun bind(
view: ViewGroup,
viewModel: KeyguardBottomAreaViewModel,
falsingManager: FalsingManager?,
vibratorHelper: VibratorHelper?,
+ activityStarter: ActivityStarter?,
messageDisplayer: (Int) -> Unit,
): Binding {
val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
@@ -110,137 +116,192 @@ object KeyguardBottomAreaViewBinder {
val indicationText: TextView = view.requireViewById(R.id.keyguard_indication_text)
val indicationTextBottom: TextView =
view.requireViewById(R.id.keyguard_indication_text_bottom)
+ val settingsMenu: LaunchableLinearLayout =
+ view.requireViewById(R.id.keyguard_settings_button)
view.clipChildren = false
view.clipToPadding = false
+ view.setOnTouchListener { _, event ->
+ if (settingsMenu.isVisible) {
+ val hitRect = Rect()
+ settingsMenu.getHitRect(hitRect)
+ if (!hitRect.contains(event.x.toInt(), event.y.toInt())) {
+ viewModel.onTouchedOutsideLockScreenSettingsMenu()
+ }
+ }
+
+ false
+ }
val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
- view.repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- launch {
- viewModel.startButton.collect { buttonModel ->
- updateButton(
+ val disposableHandle =
+ view.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.startButton.collect { buttonModel ->
+ updateButton(
+ view = startButton,
+ viewModel = buttonModel,
+ falsingManager = falsingManager,
+ messageDisplayer = messageDisplayer,
+ vibratorHelper = vibratorHelper,
+ )
+ }
+ }
+
+ launch {
+ viewModel.endButton.collect { buttonModel ->
+ updateButton(
+ view = endButton,
+ viewModel = buttonModel,
+ falsingManager = falsingManager,
+ messageDisplayer = messageDisplayer,
+ vibratorHelper = vibratorHelper,
+ )
+ }
+ }
+
+ launch {
+ viewModel.isOverlayContainerVisible.collect { isVisible ->
+ overlayContainer.visibility =
+ if (isVisible) {
+ View.VISIBLE
+ } else {
+ View.INVISIBLE
+ }
+ }
+ }
+
+ launch {
+ viewModel.alpha.collect { alpha ->
+ view.importantForAccessibility =
+ if (alpha == 0f) {
+ View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ } else {
+ View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ }
+
+ ambientIndicationArea?.alpha = alpha
+ indicationArea.alpha = alpha
+ }
+ }
+
+ launch {
+ updateButtonAlpha(
view = startButton,
- viewModel = buttonModel,
- falsingManager = falsingManager,
- messageDisplayer = messageDisplayer,
- vibratorHelper = vibratorHelper,
+ viewModel = viewModel.startButton,
+ alphaFlow = viewModel.alpha,
)
}
- }
- launch {
- viewModel.endButton.collect { buttonModel ->
- updateButton(
+ launch {
+ updateButtonAlpha(
view = endButton,
- viewModel = buttonModel,
- falsingManager = falsingManager,
- messageDisplayer = messageDisplayer,
- vibratorHelper = vibratorHelper,
+ viewModel = viewModel.endButton,
+ alphaFlow = viewModel.alpha,
)
}
- }
- launch {
- viewModel.isOverlayContainerVisible.collect { isVisible ->
- overlayContainer.visibility =
+ launch {
+ viewModel.indicationAreaTranslationX.collect { translationX ->
+ indicationArea.translationX = translationX
+ ambientIndicationArea?.translationX = translationX
+ }
+ }
+
+ launch {
+ combine(
+ viewModel.isIndicationAreaPadded,
+ configurationBasedDimensions.map { it.indicationAreaPaddingPx },
+ ) { isPadded, paddingIfPaddedPx ->
+ if (isPadded) {
+ paddingIfPaddedPx
+ } else {
+ 0
+ }
+ }
+ .collect { paddingPx ->
+ indicationArea.setPadding(paddingPx, 0, paddingPx, 0)
+ }
+ }
+
+ launch {
+ configurationBasedDimensions
+ .map { it.defaultBurnInPreventionYOffsetPx }
+ .flatMapLatest { defaultBurnInOffsetY ->
+ viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
+ }
+ .collect { translationY ->
+ indicationArea.translationY = translationY
+ ambientIndicationArea?.translationY = translationY
+ }
+ }
+
+ launch {
+ configurationBasedDimensions.collect { dimensions ->
+ indicationText.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ dimensions.indicationTextSizePx.toFloat(),
+ )
+ indicationTextBottom.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ dimensions.indicationTextSizePx.toFloat(),
+ )
+
+ startButton.updateLayoutParams {
+ width = dimensions.buttonSizePx.width
+ height = dimensions.buttonSizePx.height
+ }
+ endButton.updateLayoutParams {
+ width = dimensions.buttonSizePx.width
+ height = dimensions.buttonSizePx.height
+ }
+ }
+ }
+
+ launch {
+ viewModel.settingsMenuViewModel.isVisible.distinctUntilChanged().collect {
+ isVisible ->
+ settingsMenu.animateVisibility(visible = isVisible)
if (isVisible) {
- View.VISIBLE
- } else {
- View.INVISIBLE
- }
- }
- }
-
- launch {
- viewModel.alpha.collect { alpha ->
- view.importantForAccessibility =
- if (alpha == 0f) {
- View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
- } else {
- View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
- }
-
- ambientIndicationArea?.alpha = alpha
- indicationArea.alpha = alpha
- }
- }
-
- launch {
- updateButtonAlpha(
- view = startButton,
- viewModel = viewModel.startButton,
- alphaFlow = viewModel.alpha,
- )
- }
-
- launch {
- updateButtonAlpha(
- view = endButton,
- viewModel = viewModel.endButton,
- alphaFlow = viewModel.alpha,
- )
- }
-
- launch {
- viewModel.indicationAreaTranslationX.collect { translationX ->
- indicationArea.translationX = translationX
- ambientIndicationArea?.translationX = translationX
- }
- }
-
- launch {
- combine(
- viewModel.isIndicationAreaPadded,
- configurationBasedDimensions.map { it.indicationAreaPaddingPx },
- ) { isPadded, paddingIfPaddedPx ->
- if (isPadded) {
- paddingIfPaddedPx
- } else {
- 0
+ vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Activated)
+ settingsMenu.setOnTouchListener(
+ KeyguardSettingsButtonOnTouchListener(
+ view = settingsMenu,
+ viewModel = viewModel.settingsMenuViewModel,
+ )
+ )
+ IconViewBinder.bind(
+ icon = viewModel.settingsMenuViewModel.icon,
+ view = settingsMenu.requireViewById(R.id.icon),
+ )
+ TextViewBinder.bind(
+ view = settingsMenu.requireViewById(R.id.text),
+ viewModel = viewModel.settingsMenuViewModel.text,
+ )
}
}
- .collect { paddingPx ->
- indicationArea.setPadding(paddingPx, 0, paddingPx, 0)
- }
- }
+ }
- launch {
- configurationBasedDimensions
- .map { it.defaultBurnInPreventionYOffsetPx }
- .flatMapLatest { defaultBurnInOffsetY ->
- viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
- }
- .collect { translationY ->
- indicationArea.translationY = translationY
- ambientIndicationArea?.translationY = translationY
- }
- }
-
- launch {
- configurationBasedDimensions.collect { dimensions ->
- indicationText.setTextSize(
- TypedValue.COMPLEX_UNIT_PX,
- dimensions.indicationTextSizePx.toFloat(),
- )
- indicationTextBottom.setTextSize(
- TypedValue.COMPLEX_UNIT_PX,
- dimensions.indicationTextSizePx.toFloat(),
- )
-
- startButton.updateLayoutParams {
- width = dimensions.buttonSizePx.width
- height = dimensions.buttonSizePx.height
- }
- endButton.updateLayoutParams {
- width = dimensions.buttonSizePx.width
- height = dimensions.buttonSizePx.height
+ // 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.settingsMenuViewModel.shouldOpenSettings
+ .filter { it }
+ .collect {
+ navigateToLockScreenSettings(
+ activityStarter = activityStarter,
+ view = settingsMenu,
+ )
+ viewModel.settingsMenuViewModel.onSettingsShown()
+ }
}
}
}
}
- }
return object : Binding {
override fun getIndicationAreaAnimators(): List {
@@ -253,6 +314,10 @@ object KeyguardBottomAreaViewBinder {
override fun shouldConstrainToTopOfLockIcon(): Boolean =
viewModel.shouldConstrainToTopOfLockIcon()
+
+ override fun destroy() {
+ disposableHandle.dispose()
+ }
}
}
@@ -265,7 +330,7 @@ object KeyguardBottomAreaViewBinder {
vibratorHelper: VibratorHelper?,
) {
if (!viewModel.isVisible) {
- view.isVisible = false
+ view.isInvisible = true
return
}
@@ -342,7 +407,7 @@ object KeyguardBottomAreaViewBinder {
if (viewModel.isClickable) {
if (viewModel.useLongPress) {
view.setOnTouchListener(
- OnTouchListener(
+ KeyguardQuickAffordanceOnTouchListener(
view,
viewModel,
messageDisplayer,
@@ -372,187 +437,21 @@ object KeyguardBottomAreaViewBinder {
.collect { view.alpha = it }
}
- private class OnTouchListener(
- private val view: View,
- private val viewModel: KeyguardQuickAffordanceViewModel,
- private val messageDisplayer: (Int) -> Unit,
- private val vibratorHelper: VibratorHelper?,
- private val falsingManager: FalsingManager?,
- ) : View.OnTouchListener {
-
- private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
- private var longPressAnimator: ViewPropertyAnimator? = null
-
- @SuppressLint("ClickableViewAccessibility")
- override fun onTouch(v: View?, event: MotionEvent?): Boolean {
- return when (event?.actionMasked) {
- MotionEvent.ACTION_DOWN ->
- if (viewModel.configKey != null) {
- if (isUsingAccurateTool(event)) {
- // For accurate tool types (stylus, mouse, etc.), we don't require a
- // long-press.
- } else {
- // When not using a stylus, we require a long-press to activate the
- // quick affordance, mostly to do "falsing" (e.g. protect from false
- // clicks in the pocket/bag).
- longPressAnimator =
- view
- .animate()
- .scaleX(PRESSED_SCALE)
- .scaleY(PRESSED_SCALE)
- .setDuration(longPressDurationMs)
- .withEndAction {
- if (
- falsingManager
- ?.isFalseLongTap(
- FalsingManager.MODERATE_PENALTY
- ) == false
- ) {
- dispatchClick(viewModel.configKey)
- }
- cancel()
- }
- }
- true
- } else {
- false
- }
- MotionEvent.ACTION_MOVE -> {
- if (!isUsingAccurateTool(event)) {
- // Moving too far while performing a long-press gesture cancels that
- // gesture.
- val distanceMoved = distanceMoved(event)
- if (distanceMoved > ViewConfiguration.getTouchSlop()) {
- cancel()
- }
- }
- true
- }
- MotionEvent.ACTION_UP -> {
- if (isUsingAccurateTool(event)) {
- // When using an accurate tool type (stylus, mouse, etc.), we don't require
- // a long-press gesture to activate the quick affordance. Therefore, lifting
- // the pointer performs a click.
- if (
- viewModel.configKey != null &&
- distanceMoved(event) <= ViewConfiguration.getTouchSlop() &&
- falsingManager?.isFalseTap(FalsingManager.NO_PENALTY) == false
- ) {
- dispatchClick(viewModel.configKey)
- }
- } else {
- // When not using a stylus, lifting the finger/pointer will actually cancel
- // the long-press gesture. Calling cancel after the quick affordance was
- // already long-press activated is a no-op, so it's safe to call from here.
- cancel(
- onAnimationEnd =
- if (event.eventTime - event.downTime < longPressDurationMs) {
- Runnable {
- messageDisplayer.invoke(
- R.string.keyguard_affordance_press_too_short
- )
- val amplitude =
- view.context.resources
- .getDimensionPixelSize(
- R.dimen.keyguard_affordance_shake_amplitude
- )
- .toFloat()
- val shakeAnimator =
- ObjectAnimator.ofFloat(
- view,
- "translationX",
- -amplitude / 2,
- amplitude / 2,
- )
- shakeAnimator.duration =
- ShakeAnimationDuration.inWholeMilliseconds
- shakeAnimator.interpolator =
- CycleInterpolator(ShakeAnimationCycles)
- shakeAnimator.start()
-
- vibratorHelper?.vibrate(Vibrations.Shake)
- }
- } else {
- null
- }
- )
- }
- true
- }
- MotionEvent.ACTION_CANCEL -> {
- cancel()
- true
- }
- else -> false
- }
- }
-
- private fun dispatchClick(
- configKey: String,
- ) {
- view.setOnClickListener {
- vibratorHelper?.vibrate(
- if (viewModel.isActivated) {
- Vibrations.Activated
- } else {
- Vibrations.Deactivated
- }
- )
- viewModel.onClicked(
- KeyguardQuickAffordanceViewModel.OnClickedParameters(
- configKey = configKey,
- expandable = Expandable.fromView(view),
- slotId = viewModel.slotId,
- )
- )
- }
- view.performClick()
- view.setOnClickListener(null)
- }
-
- private fun cancel(onAnimationEnd: Runnable? = null) {
- longPressAnimator?.cancel()
- longPressAnimator = null
- view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd)
- }
-
- companion object {
- private const val PRESSED_SCALE = 1.5f
-
- /**
- * Returns `true` if the tool type at the given pointer index is an accurate tool (like
- * stylus or mouse), which means we can trust it to not be a false click; `false`
- * otherwise.
- */
- private fun isUsingAccurateTool(
- event: MotionEvent,
- pointerIndex: Int = 0,
- ): Boolean {
- return when (event.getToolType(pointerIndex)) {
- MotionEvent.TOOL_TYPE_STYLUS -> true
- MotionEvent.TOOL_TYPE_MOUSE -> true
- else -> false
+ private fun View.animateVisibility(visible: Boolean) {
+ animate()
+ .withStartAction {
+ if (visible) {
+ alpha = 0f
+ isVisible = true
}
}
-
- /**
- * Returns the amount of distance the pointer moved since the historical record at the
- * [since] index.
- */
- private fun distanceMoved(
- event: MotionEvent,
- since: Int = 0,
- ): Float {
- return if (event.historySize > 0) {
- sqrt(
- (event.y - event.getHistoricalY(since)).pow(2) +
- (event.x - event.getHistoricalX(since)).pow(2)
- )
- } else {
- 0f
+ .alpha(if (visible) 1f else 0f)
+ .withEndAction {
+ if (!visible) {
+ isVisible = false
}
}
- }
+ .start()
}
private class OnClickListener(
@@ -594,64 +493,28 @@ object KeyguardBottomAreaViewBinder {
)
}
+ /** Opens the wallpaper picker screen after the device is unlocked by the user. */
+ private fun navigateToLockScreenSettings(
+ activityStarter: ActivityStarter,
+ view: View,
+ ) {
+ activityStarter.startActivity(
+ 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) }
+ },
+ /* dismissShade= */ true,
+ ActivityLaunchAnimator.Controller.fromView(view),
+ )
+ }
+
private data class ConfigurationBasedDimensions(
val defaultBurnInPreventionYOffsetPx: Int,
val indicationAreaPaddingPx: Int,
val indicationTextSizePx: Int,
val buttonSizePx: Size,
)
-
- private val ShakeAnimationDuration = 300.milliseconds
- private val ShakeAnimationCycles = 5f
-
- object Vibrations {
-
- private const val SmallVibrationScale = 0.3f
- private const val BigVibrationScale = 0.6f
-
- val Shake =
- VibrationEffect.startComposition()
- .apply {
- val vibrationDelayMs =
- (ShakeAnimationDuration.inWholeMilliseconds / (ShakeAnimationCycles * 2))
- .toInt()
- val vibrationCount = ShakeAnimationCycles.toInt() * 2
- repeat(vibrationCount) {
- addPrimitive(
- VibrationEffect.Composition.PRIMITIVE_TICK,
- SmallVibrationScale,
- vibrationDelayMs,
- )
- }
- }
- .compose()
-
- val Activated =
- VibrationEffect.startComposition()
- .addPrimitive(
- VibrationEffect.Composition.PRIMITIVE_TICK,
- BigVibrationScale,
- 0,
- )
- .addPrimitive(
- VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
- 0.1f,
- 0,
- )
- .compose()
-
- val Deactivated =
- VibrationEffect.startComposition()
- .addPrimitive(
- VibrationEffect.Composition.PRIMITIVE_TICK,
- BigVibrationScale,
- 0,
- )
- .addPrimitive(
- VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
- 0.1f,
- 0,
- )
- .compose()
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressPopupViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressPopupViewBinder.kt
deleted file mode 100644
index d85682b3bab8e..0000000000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressPopupViewBinder.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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.view.Gravity
-import android.view.LayoutInflater
-import android.view.View
-import android.view.WindowManager
-import android.widget.PopupWindow
-import com.android.systemui.R
-import com.android.systemui.common.ui.binder.IconViewBinder
-import com.android.systemui.common.ui.binder.TextViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsPopupMenuViewModel
-
-object KeyguardLongPressPopupViewBinder {
- @SuppressLint("InflateParams") // We don't care that the parent is null.
- fun createAndShow(
- container: View,
- viewModel: KeyguardSettingsPopupMenuViewModel,
- onDismissed: () -> Unit,
- ): () -> Unit {
- val contentView: View =
- LayoutInflater.from(container.context)
- .inflate(
- R.layout.keyguard_settings_popup_menu,
- null,
- )
-
- contentView.setOnClickListener { viewModel.onClicked() }
- IconViewBinder.bind(
- icon = viewModel.icon,
- view = contentView.requireViewById(R.id.icon),
- )
- TextViewBinder.bind(
- view = contentView.requireViewById(R.id.text),
- viewModel = viewModel.text,
- )
-
- val popupWindow =
- PopupWindow(container.context).apply {
- windowLayoutType = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
- setBackgroundDrawable(null)
- animationStyle = com.android.internal.R.style.Animation_Dialog
- isOutsideTouchable = true
- isFocusable = true
- setContentView(contentView)
- setOnDismissListener { onDismissed() }
- contentView.measure(
- View.MeasureSpec.makeMeasureSpec(
- 0,
- View.MeasureSpec.UNSPECIFIED,
- ),
- View.MeasureSpec.makeMeasureSpec(
- 0,
- View.MeasureSpec.UNSPECIFIED,
- ),
- )
- showAtLocation(
- container,
- Gravity.NO_GRAVITY,
- viewModel.position.x - contentView.measuredWidth / 2,
- viewModel.position.y -
- contentView.measuredHeight -
- container.context.resources.getDimensionPixelSize(
- R.dimen.keyguard_long_press_settings_popup_vertical_offset
- ),
- )
- }
-
- return { popupWindow.dismiss() }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
index 86717537efd33..9cc503c079556 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
@@ -50,10 +50,7 @@ object KeyguardLongPressViewBinder {
return
}
- viewModel.onLongPress(
- x = x,
- y = y,
- )
+ viewModel.onLongPress()
}
override fun onSingleTapDetected(view: View) {
@@ -72,23 +69,6 @@ object KeyguardLongPressViewBinder {
view.setLongPressHandlingEnabled(isEnabled)
}
}
-
- launch {
- var dismissMenu: (() -> Unit)? = null
-
- viewModel.menu.collect { menuOrNull ->
- if (menuOrNull != null) {
- dismissMenu =
- KeyguardLongPressPopupViewBinder.createAndShow(
- container = view,
- viewModel = menuOrNull,
- onDismissed = menuOrNull.onDismissed,
- )
- } else {
- dismissMenu?.invoke()
- }
- }
- }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt
new file mode 100644
index 0000000000000..779095cd1d1e1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.PointF
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewConfiguration
+import android.view.ViewPropertyAnimator
+import androidx.core.animation.CycleInterpolator
+import androidx.core.animation.ObjectAnimator
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.ui.view.distanceFrom
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
+
+class KeyguardQuickAffordanceOnTouchListener(
+ private val view: View,
+ private val viewModel: KeyguardQuickAffordanceViewModel,
+ private val messageDisplayer: (Int) -> Unit,
+ private val vibratorHelper: VibratorHelper?,
+ private val falsingManager: FalsingManager?,
+) : View.OnTouchListener {
+
+ private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
+ private var longPressAnimator: ViewPropertyAnimator? = null
+ private val down: PointF by lazy { PointF() }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouch(v: View, event: MotionEvent): Boolean {
+ return when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN ->
+ if (viewModel.configKey != null) {
+ down.set(event.x, event.y)
+ if (isUsingAccurateTool(event)) {
+ // For accurate tool types (stylus, mouse, etc.), we don't require a
+ // long-press.
+ } else {
+ // When not using a stylus, we require a long-press to activate the
+ // quick affordance, mostly to do "falsing" (e.g. protect from false
+ // clicks in the pocket/bag).
+ longPressAnimator =
+ view
+ .animate()
+ .scaleX(PRESSED_SCALE)
+ .scaleY(PRESSED_SCALE)
+ .setDuration(longPressDurationMs)
+ .withEndAction {
+ if (
+ falsingManager?.isFalseLongTap(
+ FalsingManager.MODERATE_PENALTY
+ ) == false
+ ) {
+ dispatchClick(viewModel.configKey)
+ }
+ cancel()
+ }
+ }
+ true
+ } else {
+ false
+ }
+ MotionEvent.ACTION_MOVE -> {
+ if (!isUsingAccurateTool(event)) {
+ // Moving too far while performing a long-press gesture cancels that
+ // gesture.
+ if (event.distanceFrom(down.x, down.y) > ViewConfiguration.getTouchSlop()) {
+ cancel()
+ }
+ }
+ true
+ }
+ MotionEvent.ACTION_UP -> {
+ if (isUsingAccurateTool(event)) {
+ // When using an accurate tool type (stylus, mouse, etc.), we don't require
+ // a long-press gesture to activate the quick affordance. Therefore, lifting
+ // the pointer performs a click.
+ if (
+ viewModel.configKey != null &&
+ event.distanceFrom(down.x, down.y) <=
+ ViewConfiguration.getTouchSlop() &&
+ falsingManager?.isFalseTap(FalsingManager.NO_PENALTY) == false
+ ) {
+ dispatchClick(viewModel.configKey)
+ }
+ } else {
+ // When not using a stylus, lifting the finger/pointer will actually cancel
+ // the long-press gesture. Calling cancel after the quick affordance was
+ // already long-press activated is a no-op, so it's safe to call from here.
+ cancel(
+ onAnimationEnd =
+ if (event.eventTime - event.downTime < longPressDurationMs) {
+ Runnable {
+ messageDisplayer.invoke(
+ R.string.keyguard_affordance_press_too_short
+ )
+ val amplitude =
+ view.context.resources
+ .getDimensionPixelSize(
+ R.dimen.keyguard_affordance_shake_amplitude
+ )
+ .toFloat()
+ val shakeAnimator =
+ ObjectAnimator.ofFloat(
+ view,
+ "translationX",
+ -amplitude / 2,
+ amplitude / 2,
+ )
+ shakeAnimator.duration =
+ KeyguardBottomAreaVibrations.ShakeAnimationDuration
+ .inWholeMilliseconds
+ shakeAnimator.interpolator =
+ CycleInterpolator(
+ KeyguardBottomAreaVibrations.ShakeAnimationCycles
+ )
+ shakeAnimator.start()
+
+ vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake)
+ }
+ } else {
+ null
+ }
+ )
+ }
+ true
+ }
+ MotionEvent.ACTION_CANCEL -> {
+ cancel()
+ true
+ }
+ else -> false
+ }
+ }
+
+ private fun dispatchClick(
+ configKey: String,
+ ) {
+ view.setOnClickListener {
+ vibratorHelper?.vibrate(
+ if (viewModel.isActivated) {
+ KeyguardBottomAreaVibrations.Activated
+ } else {
+ KeyguardBottomAreaVibrations.Deactivated
+ }
+ )
+ viewModel.onClicked(
+ KeyguardQuickAffordanceViewModel.OnClickedParameters(
+ configKey = configKey,
+ expandable = Expandable.fromView(view),
+ slotId = viewModel.slotId,
+ )
+ )
+ }
+ view.performClick()
+ view.setOnClickListener(null)
+ }
+
+ private fun cancel(onAnimationEnd: Runnable? = null) {
+ longPressAnimator?.cancel()
+ longPressAnimator = null
+ view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd)
+ }
+
+ companion object {
+ private const val PRESSED_SCALE = 1.5f
+
+ /**
+ * Returns `true` if the tool type at the given pointer index is an accurate tool (like
+ * stylus or mouse), which means we can trust it to not be a false click; `false` otherwise.
+ */
+ private fun isUsingAccurateTool(
+ event: MotionEvent,
+ pointerIndex: Int = 0,
+ ): Boolean {
+ return when (event.getToolType(pointerIndex)) {
+ MotionEvent.TOOL_TYPE_STYLUS -> true
+ MotionEvent.TOOL_TYPE_MOUSE -> true
+ else -> false
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt
new file mode 100644
index 0000000000000..ad3fb637961ba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.graphics.PointF
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewConfiguration
+import com.android.systemui.animation.view.LaunchableLinearLayout
+import com.android.systemui.common.ui.view.distanceFrom
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel
+
+class KeyguardSettingsButtonOnTouchListener(
+ private val view: LaunchableLinearLayout,
+ private val viewModel: KeyguardSettingsMenuViewModel,
+) : View.OnTouchListener {
+
+ private val downPosition = PointF()
+
+ override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
+ when (motionEvent.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ view.isPressed = true
+ downPosition.set(motionEvent.x, motionEvent.y)
+ viewModel.onTouchGestureStarted()
+ }
+ MotionEvent.ACTION_UP -> {
+ view.isPressed = false
+ val distanceMoved = motionEvent.distanceFrom(downPosition.x, downPosition.y)
+ val isClick = distanceMoved < ViewConfiguration.getTouchSlop()
+ viewModel.onTouchGestureEnded(isClick)
+ if (isClick) {
+ view.performClick()
+ }
+ }
+ MotionEvent.ACTION_CANCEL -> {
+ view.isPressed = false
+ viewModel.onTouchGestureEnded(/* isClick= */ false)
+ }
+ }
+
+ return true
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index a8e346477690e..2d83be95183ec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -44,6 +44,8 @@ constructor(
private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor,
private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
private val burnInHelperWrapper: BurnInHelperWrapper,
+ private val longPressViewModel: KeyguardLongPressViewModel,
+ val settingsMenuViewModel: KeyguardSettingsMenuViewModel,
) {
data class PreviewMode(
val isInPreviewMode: Boolean = false,
@@ -161,6 +163,14 @@ constructor(
selectedPreviewSlotId.value = slotId
}
+ /**
+ * Notifies that some input gesture has started somewhere in the bottom area that's outside of
+ * the lock screen settings menu item pop-up.
+ */
+ fun onTouchedOutsideLockScreenSettingsMenu() {
+ longPressViewModel.onTouchedOutside()
+ }
+
private fun button(
position: KeyguardQuickAffordancePosition
): Flow {
@@ -225,9 +235,10 @@ constructor(
isDimmed = isDimmed,
slotId = slotId,
)
- is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel(
- slotId = slotId,
- )
+ is KeyguardQuickAffordanceModel.Hidden ->
+ KeyguardQuickAffordanceViewModel(
+ slotId = slotId,
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt
index d896390fd4719..c73931a124551 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt
@@ -17,15 +17,13 @@
package com.android.systemui.keyguard.ui.viewmodel
-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.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
/** Models UI state to support the lock screen long-press feature. */
+@SysUISingleton
class KeyguardLongPressViewModel
@Inject
constructor(
@@ -35,35 +33,16 @@ constructor(
/** Whether the long-press handling feature should be enabled. */
val isLongPressHandlingEnabled: Flow = interactor.isLongPressHandlingEnabled
- /** View-model for a menu that should be shown; `null` when no menu should be shown. */
- val menu: Flow =
- interactor.menu.map { model ->
- model?.let {
- KeyguardSettingsPopupMenuViewModel(
- icon =
- Icon.Resource(
- res = R.drawable.ic_settings,
- contentDescription = null,
- ),
- text =
- Text.Resource(
- res = R.string.lock_screen_settings,
- ),
- position = model.position,
- onClicked = model.onClicked,
- onDismissed = model.onDismissed,
- )
- }
- }
-
/** Notifies that the user has long-pressed on the lock screen. */
- fun onLongPress(
- x: Int,
- y: Int,
- ) {
- interactor.onLongPress(
- x = x,
- y = y,
- )
+ fun onLongPress() {
+ interactor.onLongPress()
+ }
+
+ /**
+ * Notifies that some input gesture has started somewhere outside of the lock screen settings
+ * menu item pop-up.
+ */
+ fun onTouchedOutside() {
+ interactor.onTouchedOutside()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt
new file mode 100644
index 0000000000000..c36da9da58a5f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.R
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Models the UI state of a keyguard settings popup menu. */
+class KeyguardSettingsMenuViewModel
+@Inject
+constructor(
+ private val interactor: KeyguardLongPressInteractor,
+) {
+ val isVisible: Flow = interactor.isMenuVisible
+ val shouldOpenSettings: Flow = interactor.shouldOpenSettings
+
+ val icon: Icon =
+ Icon.Resource(
+ res = R.drawable.ic_palette,
+ contentDescription = null,
+ )
+
+ val text: Text =
+ Text.Resource(
+ res = R.string.lock_screen_settings,
+ )
+
+ fun onTouchGestureStarted() {
+ interactor.onMenuTouchGestureStarted()
+ }
+
+ fun onTouchGestureEnded(isClick: Boolean) {
+ interactor.onMenuTouchGestureEnded(
+ isClick = isClick,
+ )
+ }
+
+ fun onSettingsShown() {
+ interactor.onSettingsShown()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsPopupMenuViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsPopupMenuViewModel.kt
deleted file mode 100644
index 0571b05b4751b..0000000000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsPopupMenuViewModel.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.common.shared.model.Icon
-import com.android.systemui.common.shared.model.Position
-import com.android.systemui.common.shared.model.Text
-
-/** Models the UI state of a keyguard settings popup menu. */
-data class KeyguardSettingsPopupMenuViewModel(
- val icon: Icon,
- val text: Text,
- /** Where the menu should be anchored, roughly in screen space. */
- val position: Position,
- /** Callback to invoke when the menu gets clicked by the user. */
- val onClicked: () -> Unit,
- /** Callback to invoke when the menu gets dismissed by the user. */
- val onDismissed: () -> Unit,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 79d3b26e01c7a..3316ca0c3fcdb 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -162,6 +162,8 @@ import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.plugins.ClockController;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.FalsingManager.FalsingTapListener;
@@ -694,23 +696,29 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mInteractionJankMonitor.end(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION);
}
};
+ private final ActivityStarter mActivityStarter;
@Inject
public NotificationPanelViewController(NotificationPanelView view,
@Main Handler handler,
LayoutInflater layoutInflater,
FeatureFlags featureFlags,
- NotificationWakeUpCoordinator coordinator, PulseExpansionHandler pulseExpansionHandler,
+ NotificationWakeUpCoordinator coordinator,
+ PulseExpansionHandler pulseExpansionHandler,
DynamicPrivacyController dynamicPrivacyController,
- KeyguardBypassController bypassController, FalsingManager falsingManager,
+ KeyguardBypassController bypassController,
+ FalsingManager falsingManager,
FalsingCollector falsingCollector,
KeyguardStateController keyguardStateController,
StatusBarStateController statusBarStateController,
StatusBarWindowStateController statusBarWindowStateController,
NotificationShadeWindowController notificationShadeWindowController,
DozeLog dozeLog,
- DozeParameters dozeParameters, CommandQueue commandQueue, VibratorHelper vibratorHelper,
- LatencyTracker latencyTracker, PowerManager powerManager,
+ DozeParameters dozeParameters,
+ CommandQueue commandQueue,
+ VibratorHelper vibratorHelper,
+ LatencyTracker latencyTracker,
+ PowerManager powerManager,
AccessibilityManager accessibilityManager, @DisplayId int displayId,
KeyguardUpdateMonitor keyguardUpdateMonitor,
MetricsLogger metricsLogger,
@@ -771,7 +779,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
Provider multiShadeInteractorProvider,
DumpManager dumpManager,
KeyguardLongPressViewModel keyguardLongPressViewModel,
- KeyguardInteractor keyguardInteractor) {
+ KeyguardInteractor keyguardInteractor,
+ ActivityStarter activityStarter) {
mInteractionJankMonitor = interactionJankMonitor;
keyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
@@ -952,6 +961,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
return Unit.INSTANCE;
},
mFalsingManager);
+ mActivityStarter = activityStarter;
onFinishInflate();
keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() {
@@ -1394,7 +1404,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mLockIconViewController,
stringResourceId ->
mKeyguardIndicationController.showTransientIndication(stringResourceId),
- mVibratorHelper);
+ mVibratorHelper,
+ mActivityStarter);
}
@VisibleForTesting
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 e4227dce94e79..d433814d7ce4f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
@@ -29,6 +29,7 @@ import com.android.systemui.R
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder.bind
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
+import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
@@ -57,7 +58,7 @@ constructor(
}
private var ambientIndicationArea: View? = null
- private lateinit var binding: KeyguardBottomAreaViewBinder.Binding
+ private var binding: KeyguardBottomAreaViewBinder.Binding? = null
private var lockIconViewController: LockIconViewController? = null
/** Initializes the view. */
@@ -67,13 +68,16 @@ constructor(
lockIconViewController: LockIconViewController? = null,
messageDisplayer: MessageDisplayer? = null,
vibratorHelper: VibratorHelper? = null,
+ activityStarter: ActivityStarter? = null,
) {
+ binding?.destroy()
binding =
bind(
this,
viewModel,
falsingManager,
vibratorHelper,
+ activityStarter,
) {
messageDisplayer?.display(it)
}
@@ -114,12 +118,12 @@ constructor(
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- binding.onConfigurationChanged()
+ binding?.onConfigurationChanged()
}
/** Returns a list of animators to use to animate the indication areas. */
val indicationAreaAnimators: List
- get() = binding.getIndicationAreaAnimators()
+ get() = checkNotNull(binding).getIndicationAreaAnimators()
override fun hasOverlappingRendering(): Boolean {
return false
@@ -139,7 +143,7 @@ constructor(
super.onLayout(changed, left, top, right, bottom)
findViewById(R.id.ambient_indication_container)?.let {
val (ambientLeft, ambientTop) = it.locationOnScreen
- if (binding.shouldConstrainToTopOfLockIcon()) {
+ if (binding?.shouldConstrainToTopOfLockIcon() == true) {
// make top of ambient indication view the bottom of the lock icon
it.layout(
ambientLeft,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
index 51988ef1ab78a..77bb12c2cbda9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
@@ -29,20 +29,20 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.util.mockito.any
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
-import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -51,8 +51,8 @@ import org.mockito.MockitoAnnotations
@RunWith(AndroidJUnit4::class)
class KeyguardLongPressInteractorTest : SysuiTestCase() {
- @Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var logger: UiEventLogger
+ @Mock private lateinit var accessibilityManager: AccessibilityManagerWrapper
private lateinit var underTest: KeyguardLongPressInteractor
@@ -63,6 +63,14 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ whenever(accessibilityManager.getRecommendedTimeoutMillis(anyInt(), anyInt())).thenAnswer {
+ it.arguments[0]
+ }
+
+ testScope = TestScope()
+ keyguardRepository = FakeKeyguardRepository()
+ keyguardTransitionRepository = FakeKeyguardTransitionRepository()
+
runBlocking { createUnderTest() }
}
@@ -98,60 +106,117 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() {
}
@Test
- fun `long-pressed - pop-up clicked - starts activity`() =
+ fun longPressed_menuClicked_showsSettings() =
testScope.runTest {
- val menu = collectLastValue(underTest.menu)
+ val isMenuVisible by collectLastValue(underTest.isMenuVisible)
+ val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings)
runCurrent()
- val x = 100
- val y = 123
- underTest.onLongPress(x, y)
- assertThat(menu()).isNotNull()
- assertThat(menu()?.position?.x).isEqualTo(x)
- assertThat(menu()?.position?.y).isEqualTo(y)
+ underTest.onLongPress()
+ assertThat(isMenuVisible).isTrue()
- menu()?.onClicked?.invoke()
+ underTest.onMenuTouchGestureEnded(/* isClick= */ true)
- assertThat(menu()).isNull()
- verify(activityStarter).dismissKeyguardThenExecute(any(), any(), anyBoolean())
+ assertThat(isMenuVisible).isFalse()
+ assertThat(shouldOpenSettings).isTrue()
}
@Test
- fun `long-pressed - pop-up dismissed - never starts activity`() =
+ fun onSettingsShown_consumesSettingsShowEvent() =
testScope.runTest {
- val menu = collectLastValue(underTest.menu)
+ val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings)
runCurrent()
- menu()?.onDismissed?.invoke()
+ underTest.onLongPress()
+ underTest.onMenuTouchGestureEnded(/* isClick= */ true)
+ assertThat(shouldOpenSettings).isTrue()
- assertThat(menu()).isNull()
- verify(activityStarter, never()).dismissKeyguardThenExecute(any(), any(), anyBoolean())
+ underTest.onSettingsShown()
+ assertThat(shouldOpenSettings).isFalse()
+ }
+
+ @Test
+ fun onTouchedOutside_neverShowsSettings() =
+ testScope.runTest {
+ val isMenuVisible by collectLastValue(underTest.isMenuVisible)
+ val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings)
+ runCurrent()
+
+ underTest.onTouchedOutside()
+
+ assertThat(isMenuVisible).isFalse()
+ assertThat(shouldOpenSettings).isFalse()
+ }
+
+ @Test
+ fun longPressed_openWppDirectlyEnabled_doesNotShowMenu_opensSettings() =
+ testScope.runTest {
+ createUnderTest(isOpenWppDirectlyEnabled = true)
+ val isMenuVisible by collectLastValue(underTest.isMenuVisible)
+ val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings)
+ runCurrent()
+
+ underTest.onLongPress()
+
+ assertThat(isMenuVisible).isFalse()
+ assertThat(shouldOpenSettings).isTrue()
}
- @Suppress("DEPRECATION") // We're okay using ACTION_CLOSE_SYSTEM_DIALOGS on system UI.
@Test
fun `long pressed - close dialogs broadcast received - popup dismissed`() =
testScope.runTest {
- val menu = collectLastValue(underTest.menu)
+ val isMenuVisible by collectLastValue(underTest.isMenuVisible)
runCurrent()
- underTest.onLongPress(123, 456)
- assertThat(menu()).isNotNull()
+ underTest.onLongPress()
+ assertThat(isMenuVisible).isTrue()
fakeBroadcastDispatcher.registeredReceivers.forEach { broadcastReceiver ->
broadcastReceiver.onReceive(context, Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
}
- assertThat(menu()).isNull()
+ assertThat(isMenuVisible).isFalse()
+ }
+
+ @Test
+ fun closesDialogAfterTimeout() =
+ testScope.runTest {
+ val isMenuVisible by collectLastValue(underTest.isMenuVisible)
+ runCurrent()
+
+ underTest.onLongPress()
+ assertThat(isMenuVisible).isTrue()
+
+ advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
+
+ assertThat(isMenuVisible).isFalse()
+ }
+
+ @Test
+ fun closesDialogAfterTimeout_onlyAfterTouchGestureEnded() =
+ testScope.runTest {
+ val isMenuVisible by collectLastValue(underTest.isMenuVisible)
+ runCurrent()
+
+ underTest.onLongPress()
+ assertThat(isMenuVisible).isTrue()
+ underTest.onMenuTouchGestureStarted()
+
+ advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
+ assertThat(isMenuVisible).isTrue()
+
+ underTest.onMenuTouchGestureEnded(/* isClick= */ false)
+ advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
+ assertThat(isMenuVisible).isFalse()
}
@Test
fun `logs when menu is shown`() =
testScope.runTest {
- collectLastValue(underTest.menu)
+ collectLastValue(underTest.isMenuVisible)
runCurrent()
- underTest.onLongPress(100, 123)
+ underTest.onLongPress()
verify(logger)
.log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
@@ -160,41 +225,61 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() {
@Test
fun `logs when menu is clicked`() =
testScope.runTest {
- val menu = collectLastValue(underTest.menu)
+ collectLastValue(underTest.isMenuVisible)
runCurrent()
- underTest.onLongPress(100, 123)
- menu()?.onClicked?.invoke()
+ underTest.onLongPress()
+ underTest.onMenuTouchGestureEnded(/* isClick= */ true)
verify(logger)
.log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
}
+ @Test
+ fun showMenu_leaveLockscreen_returnToLockscreen_menuNotVisible() =
+ testScope.runTest {
+ val isMenuVisible by collectLastValue(underTest.isMenuVisible)
+ runCurrent()
+ underTest.onLongPress()
+ assertThat(isMenuVisible).isTrue()
+
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ to = KeyguardState.GONE,
+ ),
+ )
+ assertThat(isMenuVisible).isFalse()
+
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ to = KeyguardState.LOCKSCREEN,
+ ),
+ )
+ assertThat(isMenuVisible).isFalse()
+ }
+
private suspend fun createUnderTest(
isLongPressFeatureEnabled: Boolean = true,
isRevampedWppFeatureEnabled: Boolean = true,
+ isOpenWppDirectlyEnabled: Boolean = false,
) {
- testScope = TestScope()
- keyguardRepository = FakeKeyguardRepository()
- keyguardTransitionRepository = FakeKeyguardTransitionRepository()
-
underTest =
KeyguardLongPressInteractor(
- unsafeContext = context,
scope = testScope.backgroundScope,
transitionInteractor =
KeyguardTransitionInteractor(
repository = keyguardTransitionRepository,
),
repository = keyguardRepository,
- activityStarter = activityStarter,
logger = logger,
featureFlags =
FakeFeatureFlags().apply {
set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, isLongPressFeatureEnabled)
set(Flags.REVAMPED_WALLPAPER_UI, isRevampedWppFeatureEnabled)
+ set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, isOpenWppDirectlyEnabled)
},
broadcastDispatcher = fakeBroadcastDispatcher,
+ accessibilityManager = accessibilityManager
)
setUpState()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index bfc09d7c03797..224eec1bf3ac7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -20,10 +20,12 @@ import android.app.admin.DevicePolicyManager
import android.content.Intent
import android.os.UserHandle
import androidx.test.filters.SmallTest
+import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.animation.Expandable
+import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.doze.util.BurnInHelperWrapper
@@ -38,10 +40,13 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
@@ -51,6 +56,7 @@ 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.CommandQueue
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.any
@@ -91,6 +97,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
@Mock private lateinit var commandQueue: CommandQueue
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
@Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger
+ @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+ @Mock private lateinit var accessibilityManager: AccessibilityManagerWrapper
private lateinit var underTest: KeyguardBottomAreaViewModel
@@ -134,6 +142,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
FakeFeatureFlags().apply {
set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
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 keyguardInteractor =
@@ -196,6 +206,19 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
dumpManager = mock(),
userHandle = UserHandle.SYSTEM,
)
+ val keyguardLongPressInteractor =
+ KeyguardLongPressInteractor(
+ scope = testScope.backgroundScope,
+ transitionInteractor =
+ KeyguardTransitionInteractor(
+ repository = FakeKeyguardTransitionRepository(),
+ ),
+ repository = repository,
+ logger = UiEventLoggerFake(),
+ featureFlags = featureFlags,
+ broadcastDispatcher = broadcastDispatcher,
+ accessibilityManager = accessibilityManager,
+ )
underTest =
KeyguardBottomAreaViewModel(
keyguardInteractor = keyguardInteractor,
@@ -216,6 +239,14 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
),
bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository),
burnInHelperWrapper = burnInHelperWrapper,
+ longPressViewModel =
+ KeyguardLongPressViewModel(
+ interactor = keyguardLongPressInteractor,
+ ),
+ settingsMenuViewModel =
+ KeyguardSettingsMenuViewModel(
+ interactor = keyguardLongPressInteractor,
+ ),
)
}
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 7b37ea0c9a1ac..99979976a1226 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -106,6 +106,7 @@ import com.android.systemui.model.SysUiState;
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.qs.QSFragment;
@@ -295,6 +296,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
@Captor
protected ArgumentCaptor
mEmptySpaceClickListenerCaptor;
+ @Mock protected ActivityStarter mActivityStarter;
protected KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
protected KeyguardInteractor mKeyguardInteractor;
@@ -575,7 +577,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
() -> mMultiShadeInteractor,
mDumpManager,
mKeyuardLongPressViewModel,
- mKeyguardInteractor);
+ mKeyguardInteractor,
+ mActivityStarter);
mNotificationPanelViewController.initDependencies(
mCentralSurfaces,
null,