• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 @file:OptIn(InternalNoteTaskApi::class)
18 
19 package com.android.systemui.notetask
20 
21 import android.app.ActivityManager
22 import android.app.KeyguardManager
23 import android.app.admin.DevicePolicyManager
24 import android.app.role.OnRoleHoldersChangedListener
25 import android.app.role.RoleManager
26 import android.app.role.RoleManager.ROLE_NOTES
27 import android.content.ActivityNotFoundException
28 import android.content.ComponentName
29 import android.content.Context
30 import android.content.Intent
31 import android.content.pm.PackageManager
32 import android.content.pm.ShortcutManager
33 import android.graphics.drawable.Icon
34 import android.os.Process
35 import android.os.UserHandle
36 import android.os.UserManager
37 import android.provider.Settings
38 import android.widget.Toast
39 import androidx.annotation.VisibleForTesting
40 import com.android.app.tracing.coroutines.launchTraced as launch
41 import com.android.systemui.dagger.SysUISingleton
42 import com.android.systemui.dagger.qualifiers.Application
43 import com.android.systemui.dagger.qualifiers.Background
44 import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled
45 import com.android.systemui.log.DebugLogger.debugLog
46 import com.android.systemui.notetask.NoteTaskEntryPoint.KEYBOARD_SHORTCUT
47 import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE
48 import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON
49 import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser
50 import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser
51 import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
52 import com.android.systemui.res.R
53 import com.android.systemui.settings.UserTracker
54 import com.android.systemui.shared.system.ActivityManagerKt.isInForeground
55 import com.android.systemui.util.settings.SecureSettings
56 import com.android.wm.shell.bubbles.Bubble
57 import com.android.wm.shell.bubbles.Bubbles.BubbleExpandListener
58 import java.util.concurrent.atomic.AtomicReference
59 import javax.inject.Inject
60 import kotlin.coroutines.CoroutineContext
61 import kotlinx.coroutines.CoroutineScope
62 
63 /**
64  * Entry point for creating and managing note.
65  *
66  * The controller decides how a note is launched based in the device state: locked or unlocked.
67  *
68  * Currently, we only support a single task per time.
69  */
70 @SysUISingleton
71 class NoteTaskController
72 @Inject
73 constructor(
74     private val context: Context,
75     private val roleManager: RoleManager,
76     private val shortcutManager: ShortcutManager,
77     private val resolver: NoteTaskInfoResolver,
78     private val eventLogger: NoteTaskEventLogger,
79     private val noteTaskBubblesController: NoteTaskBubblesController,
80     private val userManager: UserManager,
81     private val keyguardManager: KeyguardManager,
82     private val activityManager: ActivityManager,
83     @NoteTaskEnabledKey private val isEnabled: Boolean,
84     private val devicePolicyManager: DevicePolicyManager,
85     private val userTracker: UserTracker,
86     private val secureSettings: SecureSettings,
87     @Application private val applicationScope: CoroutineScope,
88     @Background private val bgCoroutineContext: CoroutineContext,
89 ) {
90 
91     @VisibleForTesting val infoReference = AtomicReference<NoteTaskInfo?>()
92 
93     /** @see BubbleExpandListener */
onBubbleExpandChangednull94     fun onBubbleExpandChanged(isExpanding: Boolean, key: String?) {
95         if (!isEnabled) return
96 
97         val info = infoReference.getAndSet(null) ?: return
98 
99         if (key != Bubble.getNoteBubbleKeyForApp(info.packageName, info.user)) return
100 
101         // Safe guard mechanism, this callback should only be called for app bubbles.
102         if (info.launchMode !is NoteTaskLaunchMode.AppBubble) return
103 
104         if (isExpanding) {
105             debugLog { "onBubbleExpandChanged - expanding: $info" }
106             eventLogger.logNoteTaskOpened(info)
107         } else {
108             debugLog { "onBubbleExpandChanged - collapsing: $info" }
109             eventLogger.logNoteTaskClosed(info)
110         }
111     }
112 
113     /** Starts the notes role setting. */
startNotesRoleSettingnull114     fun startNotesRoleSetting(activityContext: Context, entryPoint: NoteTaskEntryPoint?) {
115         val user =
116             if (entryPoint == null) {
117                 userTracker.userHandle
118             } else {
119                 getUserForHandlingNotesTaking(entryPoint)
120             }
121         activityContext.startActivityAsUser(createNotesRoleHolderSettingsIntent(), user)
122     }
123 
124     /**
125      * Returns the [UserHandle] of an android user that should handle the notes taking [entryPoint].
126      * 1. tail button entry point: In COPE or work profile devices, the user can select whether the
127      *    work or main profile notes app should be launched in the Settings app. In non-management
128      *    or device owner devices, the user can only select main profile notes app.
129      * 2. lock screen quick affordance: since there is no user setting, the main profile notes app
130      *    is used as default for work profile devices while the work profile notes app is used for
131      *    COPE devices.
132      * 3. Other entry point: the current user from [UserTracker.userHandle].
133      */
getUserForHandlingNotesTakingnull134     fun getUserForHandlingNotesTaking(entryPoint: NoteTaskEntryPoint): UserHandle =
135         when {
136             entryPoint == TAIL_BUTTON -> secureSettings.preferredUser
137             devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile &&
138                 entryPoint == QUICK_AFFORDANCE -> {
139                 userTracker.userProfiles
140                     .firstOrNull { userManager.isManagedProfile(it.id) }
141                     ?.userHandle ?: userTracker.userHandle
142             }
143             // On work profile devices, SysUI always run in the main user.
144             else -> userTracker.userHandle
145         }
146 
147     /**
148      * Shows a note task. How the task is shown will depend on when the method is invoked.
149      *
150      * If the keyguard is locked, notes will open as a full screen experience. A locked device has
151      * no contextual information which let us use the whole screen space available.
152      *
153      * If the keyguard is unlocked, notes will open as a bubble OR it will be collapsed if the notes
154      * bubble is already opened.
155      *
156      * That will let users open other apps in full screen, and take contextual notes.
157      */
showNoteTasknull158     fun showNoteTask(entryPoint: NoteTaskEntryPoint) {
159         if (!isEnabled) return
160 
161         showNoteTaskAsUser(entryPoint, getUserForHandlingNotesTaking(entryPoint))
162     }
163 
164     /** A variant of [showNoteTask] which launches note task in the given [user]. */
showNoteTaskAsUsernull165     fun showNoteTaskAsUser(entryPoint: NoteTaskEntryPoint, user: UserHandle) {
166         if (!isEnabled) return
167 
168         applicationScope.launch("$TAG#showNoteTaskAsUser") {
169             awaitShowNoteTaskAsUser(entryPoint, user)
170         }
171     }
172 
awaitShowNoteTaskAsUsernull173     private suspend fun awaitShowNoteTaskAsUser(entryPoint: NoteTaskEntryPoint, user: UserHandle) {
174         if (!isEnabled) return
175 
176         if (!noteTaskBubblesController.areBubblesAvailable()) {
177             debugLog { "Bubbles not available in the system user SysUI instance" }
178             return
179         }
180 
181         // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
182         if (!userManager.isUserUnlocked) return
183 
184         val isKeyguardLocked = keyguardManager.isKeyguardLocked
185         // KeyguardQuickAffordanceInteractor blocks the quick affordance from showing in the
186         // keyguard if it is not allowed by the admin policy. Here we block any other way to show
187         // note task when the screen is locked.
188         if (
189             isKeyguardLocked &&
190                 devicePolicyManager.areKeyguardShortcutsDisabled(userId = user.identifier)
191         ) {
192             debugLog { "Enterprise policy disallows launching note app when the screen is locked." }
193             return
194         }
195 
196         val info = resolver.resolveInfo(entryPoint, isKeyguardLocked, user)
197 
198         if (info == null) {
199             debugLog { "Default notes app isn't set" }
200             showNoDefaultNotesAppToast()
201             return
202         }
203 
204         infoReference.set(info)
205 
206         try {
207             // TODO(b/266686199): We should handle when app not available. For now, we log.
208             debugLog { "onShowNoteTask - start: $info on user#${user.identifier}" }
209             val useStylusMode =
210                 when {
211                     info.entryPoint == TAIL_BUTTON -> true
212                     info.entryPoint == KEYBOARD_SHORTCUT -> false
213                     else ->
214                         context.resources.getInteger(R.integer.config_preferredNotesMode) ==
215                             PREFERRED_NOTES_MODE_STYLUS
216                 }
217             when (info.launchMode) {
218                 is NoteTaskLaunchMode.AppBubble -> {
219                     val intent = createNoteTaskIntent(info, useStylusMode)
220                     val icon =
221                         Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
222                     noteTaskBubblesController.showOrHideNoteBubble(
223                         intent,
224                         user,
225                         icon,
226                         info.launchMode.bubbleExpandBehavior,
227                     )
228 
229                     // App bubble logging happens on `onBubbleExpandChanged`.
230                     debugLog { "onShowNoteTask - opened as app bubble: $info" }
231                 }
232                 is NoteTaskLaunchMode.Activity -> {
233                     if (info.isKeyguardLocked && activityManager.isInForeground(info.packageName)) {
234                         // Force note task into background by calling home.
235                         val intent = createHomeIntent()
236                         context.startActivityAsUser(intent, user)
237                         eventLogger.logNoteTaskClosed(info)
238                         debugLog { "onShowNoteTask - closed as activity: $info" }
239                     } else {
240                         val intent = createNoteTaskIntent(info, useStylusMode)
241                         context.startActivityAsUser(intent, user)
242                         eventLogger.logNoteTaskOpened(info)
243                         debugLog { "onShowNoteTask - opened as activity: $info" }
244                     }
245                 }
246             }
247             debugLog { "onShowNoteTask - success: $info" }
248         } catch (e: ActivityNotFoundException) {
249             debugLog { "onShowNoteTask - failed: $info" }
250         }
251         debugLog { "onShowNoteTask - completed: $info" }
252     }
253 
254     @VisibleForTesting
showNoDefaultNotesAppToastnull255     fun showNoDefaultNotesAppToast() {
256         Toast.makeText(context, R.string.set_default_notes_app_toast_content, Toast.LENGTH_SHORT)
257             .show()
258     }
259 
260     /**
261      * Set `android:enabled` property in the `AndroidManifest` associated with the Shortcut
262      * component to [value].
263      *
264      * If the shortcut entry `android:enabled` is set to `true`, the shortcut will be visible in the
265      * Widget Picker to all users.
266      */
setNoteTaskShortcutEnablednull267     fun setNoteTaskShortcutEnabled(value: Boolean, user: UserHandle) {
268         if (!userManager.isUserUnlocked(user)) {
269             debugLog { "setNoteTaskShortcutEnabled call but user locked: user=$user" }
270             return
271         }
272 
273         val componentName = ComponentName(context, CreateNoteTaskShortcutActivity::class.java)
274 
275         val enabledState =
276             if (value) {
277                 PackageManager.COMPONENT_ENABLED_STATE_ENABLED
278             } else {
279                 PackageManager.COMPONENT_ENABLED_STATE_DISABLED
280             }
281 
282         val userContext = context.createContextAsUser(user, /* flags= */ 0)
283 
284         userContext.packageManager.setComponentEnabledSetting(
285             componentName,
286             enabledState,
287             PackageManager.DONT_KILL_APP,
288         )
289 
290         debugLog { "setNoteTaskShortcutEnabled for user $user- completed: $enabledState" }
291     }
292 
293     /**
294      * Like [updateNoteTaskAsUser] but automatically apply to the current user and all its work
295      * profiles.
296      *
297      * @see updateNoteTaskAsUser
298      * @see UserTracker.userHandle
299      * @see UserTracker.userProfiles
300      */
updateNoteTaskForCurrentUserAndManagedProfilesnull301     fun updateNoteTaskForCurrentUserAndManagedProfiles() {
302         updateNoteTaskAsUser(userTracker.userHandle)
303         for (profile in userTracker.userProfiles) {
304             if (userManager.isManagedProfile(profile.id)) {
305                 updateNoteTaskAsUser(profile.userHandle)
306             }
307         }
308     }
309 
310     /**
311      * Updates all [NoteTaskController] related information, including but not exclusively the
312      * widget shortcut created by the [user] - by default it will use the current user.
313      *
314      * If the user is not current user, the update will be dispatched to run in that user's process.
315      *
316      * Keep in mind the shortcut API has a
317      * [rate limiting](https://developer.android.com/develop/ui/views/launch/shortcuts/managing-shortcuts#rate-limiting)
318      * and may not be updated in real-time. To reduce the chance of stale shortcuts, we run the
319      * function during System UI initialization.
320      */
updateNoteTaskAsUsernull321     fun updateNoteTaskAsUser(user: UserHandle) {
322         if (!userManager.isUserUnlocked(user)) {
323             debugLog { "updateNoteTaskAsUser call but user locked: user=$user" }
324             return
325         }
326 
327         // When switched to a secondary user, the sysUI is still running in the main user, we will
328         // need to update the shortcut in the secondary user.
329         if (user == getCurrentRunningUser()) {
330             launchUpdateNoteTaskAsUser(user)
331         } else {
332             // TODO(b/278729185): Replace fire and forget service with a bounded service.
333             val intent = NoteTaskControllerUpdateService.createIntent(context)
334             try {
335                 // If the user is stopped before 'startServiceAsUser' kicks-in, a
336                 // 'SecurityException' will be thrown.
337                 context.startServiceAsUser(intent, user)
338             } catch (e: SecurityException) {
339                 debugLog(error = e) { "Unable to start 'NoteTaskControllerUpdateService'." }
340             }
341         }
342     }
343 
344     @InternalNoteTaskApi
launchUpdateNoteTaskAsUsernull345     fun launchUpdateNoteTaskAsUser(user: UserHandle) {
346         applicationScope.launch("$TAG#launchUpdateNoteTaskAsUser", bgCoroutineContext) {
347             if (!userManager.isUserUnlocked(user)) {
348                 debugLog { "updateNoteTaskAsUserInternal call but user locked: user=$user" }
349                 return@launch
350             }
351 
352             val packageName = roleManager.getDefaultRoleHolderAsUser(ROLE_NOTES, user)
353             val hasNotesRoleHolder = isEnabled && !packageName.isNullOrEmpty()
354 
355             setNoteTaskShortcutEnabled(hasNotesRoleHolder, user)
356 
357             if (hasNotesRoleHolder) {
358                 shortcutManager.enableShortcuts(listOf(SHORTCUT_ID))
359                 val updatedShortcut = roleManager.createNoteShortcutInfoAsUser(context, user)
360                 shortcutManager.updateShortcuts(listOf(updatedShortcut))
361             } else {
362                 shortcutManager.disableShortcuts(listOf(SHORTCUT_ID))
363             }
364         }
365     }
366 
367     /** @see OnRoleHoldersChangedListener */
onRoleHoldersChangednull368     fun onRoleHoldersChanged(roleName: String, user: UserHandle) {
369         if (roleName != ROLE_NOTES) return
370 
371         updateNoteTaskAsUser(user)
372     }
373 
374     // Returns the [UserHandle] that this class is running on.
getCurrentRunningUsernull375     @VisibleForTesting internal fun getCurrentRunningUser(): UserHandle = Process.myUserHandle()
376 
377     private val SecureSettings.preferredUser: UserHandle
378         get() {
379             val trackingUserId = userTracker.userHandle.identifier
380             val userId =
381                 secureSettings.getIntForUser(
382                     /* name= */ Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
383                     /* def= */ trackingUserId,
384                     /* userHandle= */ trackingUserId,
385                 )
386             return UserHandle.of(userId)
387         }
388 
389     companion object {
390         val TAG = NoteTaskController::class.simpleName.orEmpty()
391 
392         const val SHORTCUT_ID = "note_task_shortcut_id"
393 
394         /**
395          * Shortcut extra which can point to a package name and can be used to indicate an alternate
396          * badge info. Launcher only reads this if the shortcut comes from a system app.
397          *
398          * Duplicated from [com.android.launcher3.icons.IconCache].
399          *
400          * @see com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE
401          */
402         const val EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE = "extra_shortcut_badge_override_package"
403 
404         const val PREFERRED_NOTES_MODE_STYLUS = 1
405 
406         /** Returns notes role holder settings intent. */
createNotesRoleHolderSettingsIntentnull407         fun createNotesRoleHolderSettingsIntent() =
408             Intent(Intent.ACTION_MANAGE_DEFAULT_APP).putExtra(Intent.EXTRA_ROLE_NAME, ROLE_NOTES)
409     }
410 }
411 
412 /** Creates an [Intent] for [ROLE_NOTES]. */
413 private fun createNoteTaskIntent(info: NoteTaskInfo, useStylusMode: Boolean): Intent =
414     Intent(Intent.ACTION_CREATE_NOTE).apply {
415         setPackage(info.packageName)
416 
417         // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint
418         // was used to start the note task.
419         putExtra(Intent.EXTRA_USE_STYLUS_MODE, useStylusMode)
420 
421         addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
422         // We should ensure the note experience can be opened both as a full screen (lockscreen)
423         // and inside the app bubble (contextual). These additional flags will do that.
424         if (info.launchMode == NoteTaskLaunchMode.Activity) {
425             addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
426             addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
427         }
428     }
429 
430 /** Creates an [Intent] which forces the current app to background by calling home. */
createHomeIntentnull431 private fun createHomeIntent(): Intent =
432     Intent(Intent.ACTION_MAIN).apply {
433         addCategory(Intent.CATEGORY_HOME)
434         flags = Intent.FLAG_ACTIVITY_NEW_TASK
435     }
436