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