1 /*
2  * Copyright (C) 2022 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 package com.android.systemui.notetask
17 
18 import android.app.role.OnRoleHoldersChangedListener
19 import android.app.role.RoleManager
20 import android.content.Context
21 import android.content.pm.UserInfo
22 import android.os.UserHandle
23 import android.view.KeyEvent
24 import android.view.KeyEvent.KEYCODE_N
25 import android.view.KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL
26 import android.view.ViewConfiguration
27 import com.android.keyguard.KeyguardUpdateMonitor
28 import com.android.keyguard.KeyguardUpdateMonitorCallback
29 import com.android.systemui.dagger.qualifiers.Background
30 import com.android.systemui.log.DebugLogger.debugLog
31 import com.android.systemui.notetask.NoteTaskEntryPoint.KEYBOARD_SHORTCUT
32 import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON
33 import com.android.systemui.settings.UserTracker
34 import com.android.systemui.statusbar.CommandQueue
35 import com.android.wm.shell.bubbles.Bubbles
36 import java.util.Optional
37 import java.util.concurrent.Executor
38 import javax.inject.Inject
39 
40 /** Class responsible to "glue" all note task dependencies. */
41 class NoteTaskInitializer
42 @Inject
43 constructor(
44     private val controller: NoteTaskController,
45     private val roleManager: RoleManager,
46     private val commandQueue: CommandQueue,
47     private val optionalBubbles: Optional<Bubbles>,
48     private val userTracker: UserTracker,
49     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
50     @Background private val backgroundExecutor: Executor,
51     @NoteTaskEnabledKey private val isEnabled: Boolean,
52 ) {
53 
54     /** Initializes note task related features and glue it with other parts of the SystemUI. */
initializenull55     fun initialize() {
56         debugLog { "initialize: isEnabled=$isEnabled, hasBubbles=${optionalBubbles.isEmpty}" }
57 
58         // Guard against feature not being enabled or mandatory dependencies aren't available.
59         if (!isEnabled || optionalBubbles.isEmpty) return
60 
61         initializeHandleSystemKey()
62         initializeOnRoleHoldersChanged()
63         initializeOnUserUnlocked()
64         initializeUserTracker()
65     }
66 
67     /**
68      * Initializes a callback for [CommandQueue] which will redirect [KeyEvent] from a Stylus to
69      * [NoteTaskController], ensure custom actions can be triggered (i.e., keyboard shortcut).
70      */
initializeHandleSystemKeynull71     private fun initializeHandleSystemKey() {
72         commandQueue.addCallback(callbacks)
73     }
74 
75     /**
76      * Initializes the [RoleManager] role holder changed listener to ensure [NoteTaskController]
77      * will always update whenever the role holder app changes. Keep in mind that a role may change
78      * by direct user interaction (i.e., user goes to settings and change it) or by indirect
79      * interaction (i.e., the current role holder app is uninstalled).
80      */
initializeOnRoleHoldersChangednull81     private fun initializeOnRoleHoldersChanged() {
82         roleManager.addOnRoleHoldersChangedListenerAsUser(
83             backgroundExecutor,
84             callbacks,
85             UserHandle.ALL,
86         )
87     }
88 
89     /**
90      * Initializes a [KeyguardUpdateMonitor] listener that will ensure [NoteTaskController] is in
91      * correct state during system initialization (after a direct boot user unlocked event).
92      *
93      * Once the system is unlocked, we will force trigger [NoteTaskController.onRoleHoldersChanged]
94      * with a hardcoded [RoleManager.ROLE_NOTES] for the current user.
95      */
initializeOnUserUnlockednull96     private fun initializeOnUserUnlocked() {
97         if (keyguardUpdateMonitor.isUserUnlocked(userTracker.userId)) {
98             controller.updateNoteTaskForCurrentUserAndManagedProfiles()
99         }
100         keyguardUpdateMonitor.registerCallback(callbacks)
101     }
102 
initializeUserTrackernull103     private fun initializeUserTracker() {
104         userTracker.addCallback(callbacks, backgroundExecutor)
105     }
106 
107     // Some callbacks use a weak reference, so we play safe and keep a hard reference to them all.
108     private val callbacks =
109         object :
110             KeyguardUpdateMonitorCallback(),
111             CommandQueue.Callbacks,
112             UserTracker.Callback,
113             OnRoleHoldersChangedListener {
114 
handleSystemKeynull115             override fun handleSystemKey(key: KeyEvent) {
116                 key.toNoteTaskEntryPointOrNull()?.let(controller::showNoteTask)
117             }
118 
onRoleHoldersChangednull119             override fun onRoleHoldersChanged(roleName: String, user: UserHandle) {
120                 controller.onRoleHoldersChanged(roleName, user)
121             }
122 
onUserUnlockednull123             override fun onUserUnlocked() {
124                 controller.updateNoteTaskForCurrentUserAndManagedProfiles()
125             }
126 
onUserChangednull127             override fun onUserChanged(newUser: Int, userContext: Context) {
128                 controller.updateNoteTaskForCurrentUserAndManagedProfiles()
129             }
130 
onProfilesChangednull131             override fun onProfilesChanged(profiles: List<UserInfo>) {
132                 controller.updateNoteTaskForCurrentUserAndManagedProfiles()
133             }
134         }
135 
136     /**
137      * Tracks a [KeyEvent], and determines if it should trigger an action to show the note task.
138      * Returns a [NoteTaskEntryPoint] if an action should be taken, and null otherwise.
139      */
KeyEventnull140     private fun KeyEvent.toNoteTaskEntryPointOrNull(): NoteTaskEntryPoint? {
141         val entryPoint =
142             when {
143                 keyCode == KEYCODE_STYLUS_BUTTON_TAIL && isTailButtonNotesGesture() -> TAIL_BUTTON
144                 keyCode == KEYCODE_N && isMetaPressed && isCtrlPressed -> KEYBOARD_SHORTCUT
145                 else -> null
146             }
147         debugLog { "toNoteTaskEntryPointOrNull: entryPoint=$entryPoint" }
148         return entryPoint
149     }
150 
151     private var lastStylusButtonTailUpEventTime: Long = -MULTI_PRESS_TIMEOUT
152 
153     /**
154      * Perform gesture detection for the stylus tail button to make sure we only show the note task
155      * when there is a single press. Long presses and multi-presses are ignored for now.
156      */
KeyEventnull157     private fun KeyEvent.isTailButtonNotesGesture(): Boolean {
158         if (keyCode != KEYCODE_STYLUS_BUTTON_TAIL || action != KeyEvent.ACTION_UP) {
159             return false
160         }
161 
162         val isMultiPress = (downTime - lastStylusButtonTailUpEventTime) < MULTI_PRESS_TIMEOUT
163         val isLongPress = (eventTime - downTime) >= LONG_PRESS_TIMEOUT
164         lastStylusButtonTailUpEventTime = eventTime
165 
166         // For now, trigger action immediately on UP of a single press, without waiting for
167         // the multi-press timeout to expire.
168         debugLog {
169             "isTailButtonNotesGesture: isMultiPress=$isMultiPress, isLongPress=$isLongPress"
170         }
171         return !isMultiPress && !isLongPress
172     }
173 
174     companion object {
175         val MULTI_PRESS_TIMEOUT = ViewConfiguration.getMultiPressTimeout().toLong()
176         val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong()
177     }
178 }
179