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