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