diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index f3d2638c4ee7c..eabcba469a9fe 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -758,6 +758,8 @@ 48dp 48dp 24dp + + 8dp 32dp 32dp diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 084a0e0cb62db..beb3e195abf6e 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2743,6 +2743,12 @@ --> • At least one device is available + + Press and hold to activate + Cancel 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 0ec2a287732ca..57668c795d1c9 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 @@ -61,6 +61,14 @@ constructor( private val isUsingRepository: Boolean get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) + /** + * Whether the UI should use the long press gesture to activate quick affordances. + * + * If `false`, the UI goes back to using single taps. + */ + val useLongPress: Boolean + get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) + /** Returns an observable for the quick affordance at the given position. */ fun quickAffordance( position: KeyguardQuickAffordancePosition 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 348564601c935..ae8edfece4cb8 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 @@ -16,14 +16,19 @@ package com.android.systemui.keyguard.ui.binder +import android.annotation.SuppressLint import android.graphics.drawable.Animatable2 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.isVisible import androidx.core.view.updateLayoutParams import androidx.lifecycle.Lifecycle @@ -38,6 +43,9 @@ 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.FalsingManager +import kotlin.math.pow +import kotlin.math.sqrt +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest @@ -51,6 +59,7 @@ import kotlinx.coroutines.launch * view-binding, binding each view only once. It is okay and expected for the same instance of the * view-model to be reused for multiple view/view-binder bindings. */ +@OptIn(ExperimentalCoroutinesApi::class) object KeyguardBottomAreaViewBinder { private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L @@ -84,6 +93,7 @@ object KeyguardBottomAreaViewBinder { view: ViewGroup, viewModel: KeyguardBottomAreaViewModel, falsingManager: FalsingManager?, + messageDisplayer: (Int) -> Unit, ): Binding { val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area) val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container) @@ -107,6 +117,7 @@ object KeyguardBottomAreaViewBinder { view = startButton, viewModel = buttonModel, falsingManager = falsingManager, + messageDisplayer = messageDisplayer, ) } } @@ -117,6 +128,7 @@ object KeyguardBottomAreaViewBinder { view = endButton, viewModel = buttonModel, falsingManager = falsingManager, + messageDisplayer = messageDisplayer, ) } } @@ -221,10 +233,12 @@ object KeyguardBottomAreaViewBinder { } } + @SuppressLint("ClickableViewAccessibility") private fun updateButton( view: ImageView, viewModel: KeyguardQuickAffordanceViewModel, falsingManager: FalsingManager?, + messageDisplayer: (Int) -> Unit, ) { if (!viewModel.isVisible) { view.isVisible = false @@ -297,14 +311,112 @@ object KeyguardBottomAreaViewBinder { view.isClickable = viewModel.isClickable if (viewModel.isClickable) { - view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager))) + if (viewModel.useLongPress) { + view.setOnTouchListener(OnTouchListener(view, viewModel, messageDisplayer)) + } else { + view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager))) + } } else { view.setOnClickListener(null) + view.setOnTouchListener(null) } view.isSelected = viewModel.isSelected } + private class OnTouchListener( + private val view: View, + private val viewModel: KeyguardQuickAffordanceViewModel, + private val messageDisplayer: (Int) -> Unit, + ) : View.OnTouchListener { + + private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong() + private var longPressAnimator: ViewPropertyAnimator? = null + private var downTimestamp = 0L + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + return when (event?.actionMasked) { + MotionEvent.ACTION_DOWN -> + if (viewModel.configKey != null) { + downTimestamp = System.currentTimeMillis() + longPressAnimator = + view + .animate() + .scaleX(PRESSED_SCALE) + .scaleY(PRESSED_SCALE) + .setDuration(longPressDurationMs) + .withEndAction { + view.setOnClickListener { + viewModel.onClicked( + KeyguardQuickAffordanceViewModel.OnClickedParameters( + configKey = viewModel.configKey, + expandable = Expandable.fromView(view), + ) + ) + } + view.performClick() + view.setOnClickListener(null) + } + true + } else { + false + } + MotionEvent.ACTION_MOVE -> { + if (event.historySize > 0) { + val distance = + sqrt( + (event.y - event.getHistoricalY(0)).pow(2) + + (event.x - event.getHistoricalX(0)).pow(2) + ) + if (distance > ViewConfiguration.getTouchSlop()) { + cancel() + } + } + true + } + MotionEvent.ACTION_UP -> { + if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) { + messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short) + val shakeAnimator = + ObjectAnimator.ofFloat( + view, + "translationX", + 0f, + view.context.resources + .getDimensionPixelSize( + R.dimen.keyguard_affordance_shake_amplitude + ) + .toFloat(), + 0f, + ) + shakeAnimator.duration = 300 + shakeAnimator.interpolator = CycleInterpolator(5f) + shakeAnimator.start() + } + cancel() + true + } + MotionEvent.ACTION_CANCEL -> { + cancel() + true + } + else -> false + } + } + + private fun cancel() { + downTimestamp = 0L + longPressAnimator?.cancel() + longPressAnimator = null + view.animate().scaleX(1f).scaleY(1f) + } + + companion object { + private const val PRESSED_SCALE = 1.5f + } + } + private class OnClickListener( private val viewModel: KeyguardQuickAffordanceViewModel, private val falsingManager: FalsingManager, 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 780728448e18c..5d85680efcf46 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 @@ -193,6 +193,7 @@ constructor( isClickable = isClickable, isActivated = activationState is ActivationState.Active, isSelected = isSelected, + useLongPress = quickAffordanceInteractor.useLongPress, ) is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt index e47559c671cd7..cf3a6daa40bb6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt @@ -30,6 +30,7 @@ data class KeyguardQuickAffordanceViewModel( val isClickable: Boolean = false, val isActivated: Boolean = false, val isSelected: Boolean = false, + val useLongPress: Boolean = false, ) { data class OnClickedParameters( val configKey: String, diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index e33248cc5a2b4..bd2317c54a7e9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1327,7 +1327,9 @@ public final class NotificationPanelViewController implements Dumpable { mKeyguardBottomArea.init( mKeyguardBottomAreaViewModel, mFalsingManager, - mLockIconViewController + mLockIconViewController, + stringResourceId -> + mKeyguardIndicationController.showTransientIndication(stringResourceId) ); } 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 90d2ef38448ed..2ce116394236b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt @@ -23,6 +23,7 @@ import android.view.ViewGroup import android.view.ViewPropertyAnimator import android.view.WindowInsets import android.widget.FrameLayout +import androidx.annotation.StringRes import com.android.keyguard.LockIconViewController import com.android.systemui.R import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder @@ -50,6 +51,10 @@ constructor( defStyleRes, ) { + interface MessageDisplayer { + fun display(@StringRes stringResourceId: Int) + } + private var ambientIndicationArea: View? = null private lateinit var binding: KeyguardBottomAreaViewBinder.Binding private var lockIconViewController: LockIconViewController? = null @@ -59,13 +64,16 @@ constructor( viewModel: KeyguardBottomAreaViewModel, falsingManager: FalsingManager? = null, lockIconViewController: LockIconViewController? = null, + messageDisplayer: MessageDisplayer? = null, ) { binding = bind( this, viewModel, falsingManager, - ) + ) { + messageDisplayer?.display(it) + } this.lockIconViewController = lockIconViewController }