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 package com.android.systemui.notetask.quickaffordance
18 
19 import android.app.role.OnRoleHoldersChangedListener
20 import android.app.role.RoleManager
21 import android.content.Context
22 import android.content.Intent
23 import android.hardware.input.InputSettings
24 import android.os.Build
25 import android.os.UserHandle
26 import android.os.UserManager
27 import android.util.Log
28 import com.android.keyguard.KeyguardUpdateMonitor
29 import com.android.keyguard.KeyguardUpdateMonitorCallback
30 import com.android.systemui.animation.Expandable
31 import com.android.systemui.common.shared.model.ContentDescription
32 import com.android.systemui.common.shared.model.Icon
33 import com.android.systemui.dagger.qualifiers.Background
34 import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
35 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
36 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState
37 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
38 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.PickerScreenState
39 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
40 import com.android.systemui.notetask.LaunchNotesRoleSettingsTrampolineActivity.Companion.ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE
41 import com.android.systemui.notetask.NoteTaskController
42 import com.android.systemui.notetask.NoteTaskEnabledKey
43 import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE
44 import com.android.systemui.notetask.NoteTaskInfoResolver
45 import com.android.systemui.res.R
46 import com.android.systemui.stylus.StylusManager
47 import dagger.Lazy
48 import java.util.concurrent.Executor
49 import javax.inject.Inject
50 import kotlinx.coroutines.channels.awaitClose
51 import kotlinx.coroutines.channels.trySendBlocking
52 import kotlinx.coroutines.flow.callbackFlow
53 import kotlinx.coroutines.flow.combine
54 import kotlinx.coroutines.flow.map
55 import kotlinx.coroutines.flow.onEach
56 
57 class NoteTaskQuickAffordanceConfig
58 @Inject
59 constructor(
60     private val context: Context,
61     private val controller: NoteTaskController,
62     private val noteTaskInfoResolver: NoteTaskInfoResolver,
63     private val stylusManager: StylusManager,
64     private val roleManager: RoleManager,
65     private val keyguardMonitor: KeyguardUpdateMonitor,
66     private val userManager: UserManager,
67     private val lazyRepository: Lazy<KeyguardQuickAffordanceRepository>,
68     @NoteTaskEnabledKey private val isEnabled: Boolean,
69     @Background private val backgroundExecutor: Executor,
70 ) : KeyguardQuickAffordanceConfig {
71 
72     override val key = BuiltInKeyguardQuickAffordanceKeys.CREATE_NOTE
73 
74     private val pickerNameResourceId = R.string.note_task_button_label
75 
76     override fun pickerName(): String = context.getString(pickerNameResourceId)
77 
78     override val pickerIconResourceId = R.drawable.ic_note_task_shortcut_keyguard
79 
80     // Due to a dependency cycle with KeyguardQuickAffordanceRepository, we need to lazily access
81     // the repository when lockScreenState is accessed for the first time.
82     override val lockScreenState by lazy {
83         val repository = lazyRepository.get()
84         val configSelectedFlow = repository.createConfigSelectedFlow(key)
85         val stylusEverUsedFlow = stylusManager.createStylusEverUsedFlow(context)
86         val userUnlockedFlow = userManager.createUserUnlockedFlow(keyguardMonitor)
87         val defaultNotesAppFlow =
88             roleManager.createNotesRoleFlow(backgroundExecutor, controller, noteTaskInfoResolver)
89         combine(userUnlockedFlow, stylusEverUsedFlow, configSelectedFlow, defaultNotesAppFlow) {
90                 isUserUnlocked,
91                 isStylusEverUsed,
92                 isConfigSelected,
93                 isDefaultNotesAppSet ->
94                 logDebug { "lockScreenState:isUserUnlocked=$isUserUnlocked" }
95                 logDebug { "lockScreenState:isStylusEverUsed=$isStylusEverUsed" }
96                 logDebug { "lockScreenState:isConfigSelected=$isConfigSelected" }
97                 logDebug { "lockScreenState:isDefaultNotesAppSet=$isDefaultNotesAppSet" }
98 
99                 if (
100                     isEnabled &&
101                         isUserUnlocked &&
102                         isDefaultNotesAppSet &&
103                         isConfigSelected &&
104                         isStylusEverUsed
105                 ) {
106                     val contentDescription = ContentDescription.Resource(pickerNameResourceId)
107                     val icon = Icon.Resource(pickerIconResourceId, contentDescription)
108                     LockScreenState.Visible(icon)
109                 } else {
110                     LockScreenState.Hidden
111                 }
112             }
113             .onEach { state -> logDebug { "lockScreenState=$state" } }
114     }
115 
116     override suspend fun getPickerScreenState(): PickerScreenState {
117         val isDefaultNotesAppSet =
118             noteTaskInfoResolver.resolveInfo(
119                 QUICK_AFFORDANCE,
120                 user = controller.getUserForHandlingNotesTaking(QUICK_AFFORDANCE)
121             ) != null
122         return when {
123             isEnabled && isDefaultNotesAppSet -> PickerScreenState.Default()
124             isEnabled -> {
125                 PickerScreenState.Disabled(
126                     explanation =
127                         context.getString(
128                             R.string.notes_app_quick_affordance_unavailable_explanation
129                         ),
130                     actionText =
131                         context.getString(
132                             R.string.keyguard_affordance_enablement_dialog_notes_app_action
133                         ),
134                     actionIntent =
135                         Intent(ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE).apply {
136                             setPackage(context.packageName)
137                         },
138                 )
139             }
140             else -> PickerScreenState.UnavailableOnDevice
141         }
142     }
143 
144     override fun onTriggered(expandable: Expandable?): OnTriggeredResult {
145         controller.showNoteTask(entryPoint = QUICK_AFFORDANCE)
146         return OnTriggeredResult.Handled
147     }
148 }
149 
createUserUnlockedFlownull150 private fun UserManager.createUserUnlockedFlow(monitor: KeyguardUpdateMonitor) = callbackFlow {
151     trySendBlocking(isUserUnlocked)
152     val callback =
153         object : KeyguardUpdateMonitorCallback() {
154             override fun onUserUnlocked() {
155                 trySendBlocking(isUserUnlocked)
156             }
157         }
158     monitor.registerCallback(callback)
159     awaitClose { monitor.removeCallback(callback) }
160 }
161 
<lambda>null162 private fun StylusManager.createStylusEverUsedFlow(context: Context) = callbackFlow {
163     trySendBlocking(InputSettings.isStylusEverUsed(context))
164     val callback =
165         object : StylusManager.StylusCallback {
166             override fun onStylusFirstUsed() {
167                 trySendBlocking(InputSettings.isStylusEverUsed(context))
168             }
169         }
170     registerCallback(callback)
171     awaitClose { unregisterCallback(callback) }
172 }
173 
createNotesRoleFlownull174 private fun RoleManager.createNotesRoleFlow(
175     executor: Executor,
176     noteTaskController: NoteTaskController,
177     noteTaskInfoResolver: NoteTaskInfoResolver,
178 ) = callbackFlow {
179     fun isDefaultNotesAppSetForUser() =
180         noteTaskInfoResolver.resolveInfo(
181             QUICK_AFFORDANCE,
182             user = noteTaskController.getUserForHandlingNotesTaking(QUICK_AFFORDANCE)
183         ) != null
184 
185     trySendBlocking(isDefaultNotesAppSetForUser())
186     val callback = OnRoleHoldersChangedListener { roleName, _ ->
187         if (roleName == RoleManager.ROLE_NOTES) {
188             trySendBlocking(isDefaultNotesAppSetForUser())
189         }
190     }
191     addOnRoleHoldersChangedListenerAsUser(executor, callback, UserHandle.ALL)
192     awaitClose { removeOnRoleHoldersChangedListenerAsUser(callback, UserHandle.ALL) }
193 }
194 
createConfigSelectedFlownull195 private fun KeyguardQuickAffordanceRepository.createConfigSelectedFlow(key: String) =
196     selections.map { selected ->
197         selected.values.flatten().any { selectedConfig -> selectedConfig.key == key }
198     }
199 
logDebugnull200 private inline fun Any.logDebug(message: () -> String) {
201     if (Build.IS_DEBUGGABLE) Log.d(this::class.java.simpleName, message())
202 }
203