1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package com.android.systemui.keyguard.domain.interactor 19 20 import android.content.Context 21 import android.content.Intent 22 import android.content.IntentFilter 23 import android.os.PowerManager 24 import android.provider.Settings 25 import android.view.accessibility.AccessibilityManager 26 import androidx.annotation.VisibleForTesting 27 import com.android.app.tracing.coroutines.launchTraced as launch 28 import com.android.internal.logging.UiEvent 29 import com.android.internal.logging.UiEventLogger 30 import com.android.systemui.Flags.doubleTapToSleep 31 import com.android.systemui.broadcast.BroadcastDispatcher 32 import com.android.systemui.dagger.SysUISingleton 33 import com.android.systemui.dagger.qualifiers.Application 34 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor 35 import com.android.systemui.flags.FeatureFlags 36 import com.android.systemui.keyguard.data.repository.KeyguardRepository 37 import com.android.systemui.keyguard.shared.model.KeyguardState 38 import com.android.systemui.res.R 39 import com.android.systemui.shade.PulsingGestureListener 40 import com.android.systemui.shade.ShadeDisplayAware 41 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper 42 import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository 43 import com.android.systemui.util.time.SystemClock 44 import javax.inject.Inject 45 import kotlinx.coroutines.CoroutineScope 46 import kotlinx.coroutines.Job 47 import kotlinx.coroutines.delay 48 import kotlinx.coroutines.flow.Flow 49 import kotlinx.coroutines.flow.MutableStateFlow 50 import kotlinx.coroutines.flow.SharingStarted 51 import kotlinx.coroutines.flow.StateFlow 52 import kotlinx.coroutines.flow.asStateFlow 53 import kotlinx.coroutines.flow.combine 54 import kotlinx.coroutines.flow.flatMapLatest 55 import kotlinx.coroutines.flow.flowOf 56 import kotlinx.coroutines.flow.launchIn 57 import kotlinx.coroutines.flow.onEach 58 import kotlinx.coroutines.flow.stateIn 59 60 /** Business logic for use-cases related to top-level touch handling in the lock screen. */ 61 @SysUISingleton 62 class KeyguardTouchHandlingInteractor 63 @Inject 64 constructor( 65 @ShadeDisplayAware private val context: Context, 66 @Application private val scope: CoroutineScope, 67 transitionInteractor: KeyguardTransitionInteractor, 68 repository: KeyguardRepository, 69 private val logger: UiEventLogger, 70 private val featureFlags: FeatureFlags, 71 broadcastDispatcher: BroadcastDispatcher, 72 private val accessibilityManager: AccessibilityManagerWrapper, 73 private val pulsingGestureListener: PulsingGestureListener, 74 private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, 75 private val secureSettingsRepository: UserAwareSecureSettingsRepository, 76 private val powerManager: PowerManager, 77 private val systemClock: SystemClock, 78 ) { 79 /** Whether the long-press handling feature should be enabled. */ 80 val isLongPressHandlingEnabled: StateFlow<Boolean> = 81 if (isLongPressFeatureEnabled()) { 82 combine( 83 transitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN), 84 repository.isQuickSettingsVisible, 85 ) { isFullyTransitionedToLockScreen, isQuickSettingsVisible -> 86 isFullyTransitionedToLockScreen && !isQuickSettingsVisible 87 } 88 } else { 89 flowOf(false) 90 } 91 .stateIn( 92 scope = scope, 93 started = SharingStarted.WhileSubscribed(), 94 initialValue = false, 95 ) 96 97 /** Whether the double tap handling handling feature should be enabled. */ 98 val isDoubleTapHandlingEnabled: StateFlow<Boolean> = 99 if (isDoubleTapFeatureEnabled()) { 100 combine( 101 transitionInteractor.transitionValue(KeyguardState.LOCKSCREEN), 102 repository.isQuickSettingsVisible, 103 isDoubleTapSettingEnabled(), 104 ) { 105 isFullyTransitionedToLockScreen, 106 isQuickSettingsVisible, 107 isDoubleTapSettingEnabled -> 108 isFullyTransitionedToLockScreen == 1f && 109 !isQuickSettingsVisible && 110 isDoubleTapSettingEnabled 111 } 112 } else { 113 flowOf(false) 114 } 115 .stateIn( 116 scope = scope, 117 started = SharingStarted.WhileSubscribed(), 118 initialValue = false, 119 ) 120 121 private val _isMenuVisible = MutableStateFlow(false) 122 /** Model for whether the menu should be shown. */ 123 val isMenuVisible: StateFlow<Boolean> = 124 isLongPressHandlingEnabled 125 .flatMapLatest { isEnabled -> 126 if (isEnabled) { 127 _isMenuVisible.asStateFlow() 128 } else { 129 // Reset the state so we don't see a menu when long-press handling is enabled 130 // again in the future. 131 _isMenuVisible.value = false 132 flowOf(false) 133 } 134 } 135 .stateIn( 136 scope = scope, 137 started = SharingStarted.WhileSubscribed(), 138 initialValue = false, 139 ) 140 141 private val _shouldOpenSettings = MutableStateFlow(false) 142 /** 143 * Whether the long-press accessible "settings" flow should be opened. 144 * 145 * Note that [onSettingsShown] must be invoked to consume this, once the settings are opened. 146 */ 147 val shouldOpenSettings = _shouldOpenSettings.asStateFlow() 148 149 private var delayedHideMenuJob: Job? = null 150 151 init { 152 if (isLongPressFeatureEnabled()) { 153 broadcastDispatcher 154 .broadcastFlow(IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) 155 .onEach { hideMenu() } 156 .launchIn(scope) 157 } 158 } 159 160 /** 161 * Notifies that the user has long-pressed on the lock screen. 162 * 163 * @param isA11yAction: Whether the action was performed as an a11y action 164 */ 165 fun onLongPress(isA11yAction: Boolean = false) { 166 if (!isLongPressHandlingEnabled.value) { 167 return 168 } 169 170 if (isA11yAction) { 171 showSettings() 172 } else { 173 showMenu() 174 } 175 } 176 177 /** Notifies that the user has touched outside of the pop-up. */ 178 fun onTouchedOutside() { 179 hideMenu() 180 } 181 182 /** Notifies that the user has started a touch gesture on the menu. */ 183 fun onMenuTouchGestureStarted() { 184 cancelAutomaticMenuHiding() 185 } 186 187 /** Notifies that the user has started a touch gesture on the menu. */ 188 fun onMenuTouchGestureEnded(isClick: Boolean) { 189 if (isClick) { 190 hideMenu() 191 logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED) 192 showSettings() 193 } else { 194 scheduleAutomaticMenuHiding() 195 } 196 } 197 198 /** Notifies that the settings UI has been shown, consuming the event to show it. */ 199 fun onSettingsShown() { 200 _shouldOpenSettings.value = false 201 } 202 203 /** Notifies that the lockscreen has been clicked at position [x], [y]. */ 204 fun onClick(x: Float, y: Float) { 205 pulsingGestureListener.onSingleTapUp(x, y) 206 faceAuthInteractor.onNotificationPanelClicked() 207 } 208 209 /** Notifies that the lockscreen has been double clicked. */ 210 fun onDoubleClick() { 211 if (isDoubleTapHandlingEnabled.value) { 212 powerManager.goToSleep(systemClock.uptimeMillis()) 213 } else { 214 pulsingGestureListener.onDoubleTapEvent() 215 } 216 } 217 218 private fun isDoubleTapSettingEnabled(): Flow<Boolean> { 219 return secureSettingsRepository.boolSetting(Settings.Secure.DOUBLE_TAP_TO_SLEEP) 220 } 221 222 private fun showSettings() { 223 _shouldOpenSettings.value = true 224 } 225 226 private fun isLongPressFeatureEnabled(): Boolean { 227 return context.resources.getBoolean(R.bool.long_press_keyguard_customize_lockscreen_enabled) 228 } 229 230 private fun isDoubleTapFeatureEnabled(): Boolean { 231 return doubleTapToSleep() && 232 context.resources.getBoolean(com.android.internal.R.bool.config_supportDoubleTapSleep) 233 } 234 235 /** Updates application state to ask to show the menu. */ 236 private fun showMenu() { 237 _isMenuVisible.value = true 238 scheduleAutomaticMenuHiding() 239 logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN) 240 } 241 242 private fun scheduleAutomaticMenuHiding() { 243 cancelAutomaticMenuHiding() 244 delayedHideMenuJob = 245 scope.launch { 246 delay(timeOutMs()) 247 hideMenu() 248 } 249 } 250 251 /** Updates application state to ask to hide the menu. */ 252 private fun hideMenu() { 253 cancelAutomaticMenuHiding() 254 _isMenuVisible.value = false 255 } 256 257 private fun cancelAutomaticMenuHiding() { 258 delayedHideMenuJob?.cancel() 259 delayedHideMenuJob = null 260 } 261 262 private fun timeOutMs(): Long { 263 return accessibilityManager 264 .getRecommendedTimeoutMillis( 265 DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS.toInt(), 266 AccessibilityManager.FLAG_CONTENT_ICONS or 267 AccessibilityManager.FLAG_CONTENT_TEXT or 268 AccessibilityManager.FLAG_CONTENT_CONTROLS, 269 ) 270 .toLong() 271 } 272 273 enum class LogEvents(private val _id: Int) : UiEventLogger.UiEventEnum { 274 @UiEvent(doc = "The lock screen was long-pressed and we showed the settings popup menu.") 275 LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN(1292), 276 @UiEvent(doc = "The lock screen long-press popup menu was clicked.") 277 LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED(1293); 278 279 override fun getId() = _id 280 } 281 282 companion object { 283 @VisibleForTesting const val DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS = 5000L 284 } 285 } 286