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,