• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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