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.view.accessibility.AccessibilityManager 24 import androidx.annotation.VisibleForTesting 25 import com.android.internal.logging.UiEvent 26 import com.android.internal.logging.UiEventLogger 27 import com.android.systemui.broadcast.BroadcastDispatcher 28 import com.android.systemui.dagger.SysUISingleton 29 import com.android.systemui.dagger.qualifiers.Application 30 import com.android.systemui.flags.FeatureFlags 31 import com.android.systemui.flags.Flags 32 import com.android.systemui.keyguard.data.repository.KeyguardRepository 33 import com.android.systemui.keyguard.shared.model.KeyguardState 34 import com.android.systemui.res.R 35 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper 36 import javax.inject.Inject 37 import kotlinx.coroutines.CoroutineScope 38 import kotlinx.coroutines.ExperimentalCoroutinesApi 39 import kotlinx.coroutines.Job 40 import kotlinx.coroutines.delay 41 import kotlinx.coroutines.flow.MutableStateFlow 42 import kotlinx.coroutines.flow.SharingStarted 43 import kotlinx.coroutines.flow.StateFlow 44 import kotlinx.coroutines.flow.asStateFlow 45 import kotlinx.coroutines.flow.combine 46 import kotlinx.coroutines.flow.flatMapLatest 47 import kotlinx.coroutines.flow.flowOf 48 import kotlinx.coroutines.flow.launchIn 49 import kotlinx.coroutines.flow.map 50 import kotlinx.coroutines.flow.onEach 51 import kotlinx.coroutines.flow.stateIn 52 import kotlinx.coroutines.launch 53 54 /** Business logic for use-cases related to the keyguard long-press feature. */ 55 @OptIn(ExperimentalCoroutinesApi::class) 56 @SysUISingleton 57 class KeyguardLongPressInteractor 58 @Inject 59 constructor( 60 @Application private val appContext: Context, 61 @Application private val scope: CoroutineScope, 62 transitionInteractor: KeyguardTransitionInteractor, 63 repository: KeyguardRepository, 64 private val logger: UiEventLogger, 65 private val featureFlags: FeatureFlags, 66 broadcastDispatcher: BroadcastDispatcher, 67 private val accessibilityManager: AccessibilityManagerWrapper, 68 ) { 69 /** Whether the long-press handling feature should be enabled. */ 70 val isLongPressHandlingEnabled: StateFlow<Boolean> = 71 if (isFeatureEnabled()) { 72 combine( 73 transitionInteractor.finishedKeyguardState.map { 74 it == KeyguardState.LOCKSCREEN 75 }, 76 repository.isQuickSettingsVisible, 77 ) { isFullyTransitionedToLockScreen, isQuickSettingsVisible -> 78 isFullyTransitionedToLockScreen && !isQuickSettingsVisible 79 } 80 } else { 81 flowOf(false) 82 } 83 .stateIn( 84 scope = scope, 85 started = SharingStarted.WhileSubscribed(), 86 initialValue = false, 87 ) 88 89 private val _isMenuVisible = MutableStateFlow(false) 90 /** Model for whether the menu should be shown. */ 91 val isMenuVisible: StateFlow<Boolean> = 92 isLongPressHandlingEnabled 93 .flatMapLatest { isEnabled -> 94 if (isEnabled) { 95 _isMenuVisible.asStateFlow() 96 } else { 97 // Reset the state so we don't see a menu when long-press handling is enabled 98 // again in the future. 99 _isMenuVisible.value = false 100 flowOf(false) 101 } 102 } 103 .stateIn( 104 scope = scope, 105 started = SharingStarted.WhileSubscribed(), 106 initialValue = false, 107 ) 108 109 private val _shouldOpenSettings = MutableStateFlow(false) 110 /** 111 * Whether the long-press accessible "settings" flow should be opened. 112 * 113 * Note that [onSettingsShown] must be invoked to consume this, once the settings are opened. 114 */ 115 val shouldOpenSettings = _shouldOpenSettings.asStateFlow() 116 117 private var delayedHideMenuJob: Job? = null 118 119 init { 120 if (isFeatureEnabled()) { 121 broadcastDispatcher 122 .broadcastFlow( 123 IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), 124 ) 125 .onEach { hideMenu() } 126 .launchIn(scope) 127 } 128 } 129 130 /** Notifies that the user has long-pressed on the lock screen. */ 131 fun onLongPress() { 132 if (!isLongPressHandlingEnabled.value) { 133 return 134 } 135 136 if (featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP)) { 137 showSettings() 138 } else { 139 showMenu() 140 } 141 } 142 143 /** Notifies that the user has touched outside of the pop-up. */ 144 fun onTouchedOutside() { 145 hideMenu() 146 } 147 148 /** Notifies that the user has started a touch gesture on the menu. */ 149 fun onMenuTouchGestureStarted() { 150 cancelAutomaticMenuHiding() 151 } 152 153 /** Notifies that the user has started a touch gesture on the menu. */ 154 fun onMenuTouchGestureEnded(isClick: Boolean) { 155 if (isClick) { 156 hideMenu() 157 logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED) 158 showSettings() 159 } else { 160 scheduleAutomaticMenuHiding() 161 } 162 } 163 164 /** Notifies that the settings UI has been shown, consuming the event to show it. */ 165 fun onSettingsShown() { 166 _shouldOpenSettings.value = false 167 } 168 169 private fun showSettings() { 170 _shouldOpenSettings.value = true 171 } 172 173 private fun isFeatureEnabled(): Boolean { 174 return featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED) && 175 appContext.resources.getBoolean(R.bool.long_press_keyguard_customize_lockscreen_enabled) 176 } 177 178 /** Updates application state to ask to show the menu. */ 179 private fun showMenu() { 180 _isMenuVisible.value = true 181 scheduleAutomaticMenuHiding() 182 logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN) 183 } 184 185 private fun scheduleAutomaticMenuHiding() { 186 cancelAutomaticMenuHiding() 187 delayedHideMenuJob = 188 scope.launch { 189 delay(timeOutMs()) 190 hideMenu() 191 } 192 } 193 194 /** Updates application state to ask to hide the menu. */ 195 private fun hideMenu() { 196 cancelAutomaticMenuHiding() 197 _isMenuVisible.value = false 198 } 199 200 private fun cancelAutomaticMenuHiding() { 201 delayedHideMenuJob?.cancel() 202 delayedHideMenuJob = null 203 } 204 205 private fun timeOutMs(): Long { 206 return accessibilityManager 207 .getRecommendedTimeoutMillis( 208 DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS.toInt(), 209 AccessibilityManager.FLAG_CONTENT_ICONS or 210 AccessibilityManager.FLAG_CONTENT_TEXT or 211 AccessibilityManager.FLAG_CONTENT_CONTROLS, 212 ) 213 .toLong() 214 } 215 216 enum class LogEvents( 217 private val _id: Int, 218 ) : UiEventLogger.UiEventEnum { 219 @UiEvent(doc = "The lock screen was long-pressed and we showed the settings popup menu.") 220 LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN(1292), 221 @UiEvent(doc = "The lock screen long-press popup menu was clicked.") 222 LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED(1293), 223 ; 224 225 override fun getId() = _id 226 } 227 228 companion object { 229 @VisibleForTesting const val DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS = 5000L 230 } 231 } 232