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