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