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