diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 650d5fabeb85a..904f36a9ad00b 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -235,7 +235,10 @@ + + + diff --git a/packages/SystemUI/src/com/android/systemui/notetask/InternalNoteTaskApi.kt b/packages/SystemUI/src/com/android/systemui/notetask/InternalNoteTaskApi.kt new file mode 100644 index 0000000000000..5d03218406d00 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/InternalNoteTaskApi.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +/** + * Marks declarations that are **internal** in note task API, which means that should not be used + * outside of `com.android.systemui.notetask`. + */ +@Retention(value = AnnotationRetention.BINARY) +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.PROPERTY +) +@RequiresOptIn( + level = RequiresOptIn.Level.ERROR, + message = "This is an internal API, do not it outside `com.android.systemui.notetask`", +) +internal annotation class InternalNoteTaskApi diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 58ac5b30972fe..93ed8591e738d 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -14,18 +14,21 @@ * limitations under the License. */ +@file:OptIn(InternalNoteTaskApi::class) + package com.android.systemui.notetask import android.app.KeyguardManager import android.app.admin.DevicePolicyManager +import android.app.role.OnRoleHoldersChangedListener +import android.app.role.RoleManager +import android.app.role.RoleManager.ROLE_NOTES import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK -import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.PackageManager +import android.content.pm.ShortcutManager import android.os.Build import android.os.UserHandle import android.os.UserManager @@ -33,6 +36,8 @@ import android.util.Log import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled +import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser +import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.settings.UserTracker import com.android.systemui.util.kotlin.getOrNull @@ -55,6 +60,8 @@ class NoteTaskController @Inject constructor( private val context: Context, + private val roleManager: RoleManager, + private val shortcutManager: ShortcutManager, private val resolver: NoteTaskInfoResolver, private val eventLogger: NoteTaskEventLogger, private val optionalBubbles: Optional, @@ -133,7 +140,7 @@ constructor( infoReference.set(info) // TODO(b/266686199): We should handle when app not available. For now, we log. - val intent = createNoteIntent(info) + val intent = createNoteTaskIntent(info) try { logDebug { "onShowNoteTask - start: $info on user#${user.identifier}" } when (info.launchMode) { @@ -182,30 +189,71 @@ constructor( logDebug { "setNoteTaskShortcutEnabled - completed: $isEnabled" } } + /** + * Updates all [NoteTaskController] related information, including but not exclusively the + * widget shortcut created by the [user] - by default it will use the current user. + * + * Keep in mind the shortcut API has a + * [rate limiting](https://developer.android.com/develop/ui/views/launch/shortcuts/managing-shortcuts#rate-limiting) + * and may not be updated in real-time. To reduce the chance of stale shortcuts, we run the + * function during System UI initialization. + */ + fun updateNoteTaskAsUser(user: UserHandle) { + val packageName = roleManager.getDefaultRoleHolderAsUser(ROLE_NOTES, user) + val hasNotesRoleHolder = isEnabled && !packageName.isNullOrEmpty() + + setNoteTaskShortcutEnabled(hasNotesRoleHolder) + + if (hasNotesRoleHolder) { + shortcutManager.enableShortcuts(listOf(SHORTCUT_ID)) + val updatedShortcut = roleManager.createNoteShortcutInfoAsUser(context, user) + shortcutManager.updateShortcuts(listOf(updatedShortcut)) + } else { + shortcutManager.disableShortcuts(listOf(SHORTCUT_ID)) + } + } + + /** @see OnRoleHoldersChangedListener */ + fun onRoleHoldersChanged(roleName: String, user: UserHandle) { + if (roleName == ROLE_NOTES) updateNoteTaskAsUser(user) + } + companion object { val TAG = NoteTaskController::class.simpleName.orEmpty() + + const val SHORTCUT_ID = "note_task_shortcut_id" + + /** + * Shortcut extra which can point to a package name and can be used to indicate an alternate + * badge info. Launcher only reads this if the shortcut comes from a system app. + * + * Duplicated from [com.android.launcher3.icons.IconCache]. + * + * @see com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE + */ + const val EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE = "extra_shortcut_badge_override_package" } } -private fun createNoteIntent(info: NoteTaskInfo): Intent = +/** Creates an [Intent] for [ROLE_NOTES]. */ +private fun createNoteTaskIntent(info: NoteTaskInfo): Intent = Intent(Intent.ACTION_CREATE_NOTE).apply { setPackage(info.packageName) // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint - // was used to start it. + // was used to start the note task. putExtra(Intent.EXTRA_USE_STYLUS_MODE, true) - addFlags(FLAG_ACTIVITY_NEW_TASK) - // We should ensure the note experience can be open both as a full screen (lock screen) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // We should ensure the note experience can be opened both as a full screen (lockscreen) // and inside the app bubble (contextual). These additional flags will do that. if (info.launchMode == NoteTaskLaunchMode.Activity) { - addFlags(FLAG_ACTIVITY_MULTIPLE_TASK) - addFlags(FLAG_ACTIVITY_NEW_DOCUMENT) + addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) } } -private inline fun logDebug(message: () -> String) { - if (Build.IS_DEBUGGABLE) { - Log.d(NoteTaskController.TAG, message()) - } +/** [Log.println] a [Log.DEBUG] message, only when [Build.IS_DEBUGGABLE]. */ +private inline fun Any.logDebug(message: () -> String) { + if (Build.IS_DEBUGGABLE) Log.d(this::class.java.simpleName.orEmpty(), message()) } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt index 8ecf08192e293..616f9b5261561 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt @@ -14,13 +14,17 @@ * limitations under the License. */ +@file:OptIn(InternalNoteTaskApi::class) + package com.android.systemui.notetask import android.app.role.RoleManager +import android.app.role.RoleManager.ROLE_NOTES import android.content.pm.PackageManager import android.content.pm.PackageManager.ApplicationInfoFlags import android.os.UserHandle import android.util.Log +import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser import com.android.systemui.settings.UserTracker import javax.inject.Inject @@ -36,10 +40,9 @@ constructor( entryPoint: NoteTaskEntryPoint? = null, isKeyguardLocked: Boolean = false, ): NoteTaskInfo? { - // TODO(b/267634412): Select UserHandle depending on where the user initiated note-taking. val user = userTracker.userHandle - val packageName = - roleManager.getRoleHoldersAsUser(RoleManager.ROLE_NOTES, user).firstOrNull() + + val packageName = roleManager.getDefaultRoleHolderAsUser(ROLE_NOTES, user) if (packageName.isNullOrEmpty()) return null diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt index fb3c0cb54f843..04ed08b6fc209 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -15,11 +15,15 @@ */ package com.android.systemui.notetask +import android.app.role.RoleManager +import android.os.UserHandle import android.view.KeyEvent import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.CommandQueue import com.android.wm.shell.bubbles.Bubbles import java.util.Optional +import java.util.concurrent.Executor import javax.inject.Inject /** Class responsible to "glue" all note task dependencies. */ @@ -27,8 +31,10 @@ internal class NoteTaskInitializer @Inject constructor( private val controller: NoteTaskController, + private val roleManager: RoleManager, private val commandQueue: CommandQueue, private val optionalBubbles: Optional, + @Background private val backgroundExecutor: Executor, @NoteTaskEnabledKey private val isEnabled: Boolean, ) { @@ -43,11 +49,15 @@ constructor( } fun initialize() { - controller.setNoteTaskShortcutEnabled(isEnabled) - // Guard against feature not being enabled or mandatory dependencies aren't available. if (!isEnabled || optionalBubbles.isEmpty) return + controller.setNoteTaskShortcutEnabled(true) commandQueue.addCallback(callbacks) + roleManager.addOnRoleHoldersChangedListenerAsUser( + backgroundExecutor, + controller::onRoleHoldersChanged, + UserHandle.ALL, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt new file mode 100644 index 0000000000000..441b9f5d01819 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import android.app.role.RoleManager +import android.app.role.RoleManager.ROLE_NOTES +import android.content.Context +import android.content.pm.ShortcutInfo +import android.graphics.drawable.Icon +import android.os.PersistableBundle +import android.os.UserHandle +import com.android.systemui.R +import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity + +/** Extension functions for [RoleManager] used **internally** by note task. */ +@InternalNoteTaskApi +internal object NoteTaskRoleManagerExt { + + /** + * Gets package name of the default (first) app holding the [role]. If none, returns either an + * empty string or null. + */ + fun RoleManager.getDefaultRoleHolderAsUser(role: String, user: UserHandle): String? = + getRoleHoldersAsUser(role, user).firstOrNull() + + /** Creates a [ShortcutInfo] for [ROLE_NOTES]. */ + fun RoleManager.createNoteShortcutInfoAsUser( + context: Context, + user: UserHandle, + ): ShortcutInfo { + val extras = PersistableBundle() + getDefaultRoleHolderAsUser(ROLE_NOTES, user)?.let { packageName -> + // Set custom app badge using the icon from ROLES_NOTES default app. + extras.putString(NoteTaskController.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE, packageName) + } + + val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) + + return ShortcutInfo.Builder(context, NoteTaskController.SHORTCUT_ID) + .setIntent(LaunchNoteTaskActivity.newIntent(context = context)) + .setShortLabel(context.getString(R.string.note_task_button_label)) + .setLongLived(true) + .setIcon(icon) + .setExtras(extras) + .build() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt index 5c59532e0c2e9..0cfb0a5218200 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt @@ -14,19 +14,17 @@ * limitations under the License. */ +@file:OptIn(InternalNoteTaskApi::class) + package com.android.systemui.notetask.shortcut import android.app.Activity import android.app.role.RoleManager -import android.content.Intent +import android.content.pm.ShortcutManager import android.os.Bundle -import android.os.PersistableBundle import androidx.activity.ComponentActivity -import androidx.annotation.DrawableRes -import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.graphics.drawable.IconCompat -import com.android.systemui.R +import com.android.systemui.notetask.InternalNoteTaskApi +import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser import javax.inject.Inject /** @@ -42,62 +40,16 @@ class CreateNoteTaskShortcutActivity @Inject constructor( private val roleManager: RoleManager, + private val shortcutManager: ShortcutManager, ) : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val intent = - createShortcutIntent( - id = SHORTCUT_ID, - shortLabel = getString(R.string.note_task_button_label), - intent = LaunchNoteTaskActivity.newIntent(context = this), - iconResource = R.drawable.ic_note_task_shortcut_widget, - ) - setResult(Activity.RESULT_OK, intent) + val shortcutInfo = roleManager.createNoteShortcutInfoAsUser(context = this, user) + val shortcutIntent = shortcutManager.createShortcutResultIntent(shortcutInfo) + setResult(Activity.RESULT_OK, shortcutIntent) finish() } - - private fun createShortcutIntent( - id: String, - shortLabel: String, - intent: Intent, - @DrawableRes iconResource: Int, - ): Intent { - val extras = PersistableBundle() - - roleManager.getRoleHoldersAsUser(RoleManager.ROLE_NOTES, user).firstOrNull()?.let { name -> - extras.putString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE, name) - } - - val shortcutInfo = - ShortcutInfoCompat.Builder(this, id) - .setIntent(intent) - .setShortLabel(shortLabel) - .setLongLived(true) - .setIcon(IconCompat.createWithResource(this, iconResource)) - .setExtras(extras) - .build() - - return ShortcutManagerCompat.createShortcutResultIntent( - this, - shortcutInfo, - ) - } - - private companion object { - private const val SHORTCUT_ID = "note-task-shortcut-id" - - /** - * Shortcut extra which can point to a package name and can be used to indicate an alternate - * badge info. Launcher only reads this if the shortcut comes from a system app. - * - * Duplicated from [com.android.launcher3.icons.IconCache]. - * - * @see com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE - */ - private const val EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE = - "extra_shortcut_badge_override_package" - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index 0ee52ea7838aa..e64094675ff5a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -13,10 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(InternalNoteTaskApi::class) + package com.android.systemui.notetask import android.app.KeyguardManager import android.app.admin.DevicePolicyManager +import android.app.role.RoleManager +import android.app.role.RoleManager.ROLE_NOTES import android.content.ComponentName import android.content.Context import android.content.Intent @@ -24,12 +28,20 @@ import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.PackageManager +import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED +import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager import android.os.UserHandle import android.os.UserManager import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.NoteTaskController.Companion.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE +import com.android.systemui.notetask.NoteTaskController.Companion.SHORTCUT_ID import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity +import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserTracker import com.android.systemui.util.mockito.any @@ -47,6 +59,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.isNull +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @@ -63,15 +76,17 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Mock lateinit var keyguardManager: KeyguardManager @Mock lateinit var userManager: UserManager @Mock lateinit var eventLogger: NoteTaskEventLogger + @Mock lateinit var roleManager: RoleManager + @Mock lateinit var shortcutManager: ShortcutManager @Mock private lateinit var devicePolicyManager: DevicePolicyManager private val userTracker: UserTracker = FakeUserTracker() - private val noteTaskInfo = NoteTaskInfo(packageName = NOTES_PACKAGE_NAME, uid = NOTES_UID) @Before fun setUp() { MockitoAnnotations.initMocks(this) + whenever(context.getString(R.string.note_task_button_label)).thenReturn(NOTES_SHORT_LABEL) whenever(context.packageManager).thenReturn(packageManager) whenever(resolver.resolveInfo(any(), any())).thenReturn(noteTaskInfo) whenever(userManager.isUserUnlocked).thenReturn(true) @@ -82,6 +97,8 @@ internal class NoteTaskControllerTest : SysuiTestCase() { ) ) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE) + whenever(roleManager.getRoleHoldersAsUser(ROLE_NOTES, userTracker.userHandle)) + .thenReturn(listOf(NOTES_PACKAGE_NAME)) } private fun createNoteTaskController( @@ -98,6 +115,8 @@ internal class NoteTaskControllerTest : SysuiTestCase() { isEnabled = isEnabled, devicePolicyManager = devicePolicyManager, userTracker = userTracker, + roleManager = roleManager, + shortcutManager = shortcutManager, ) // region onBubbleExpandChanged @@ -132,7 +151,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - fun onBubbleExpandChanged_expandingAndKeyguardLocked_doNothing() { + fun onBubbleExpandChanged_expandingAndKeyguardLocked_shouldDoNothing() { val expectedInfo = noteTaskInfo.copy(isKeyguardLocked = true) createNoteTaskController() @@ -146,7 +165,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - fun onBubbleExpandChanged_notExpandingAndKeyguardLocked_doNothing() { + fun onBubbleExpandChanged_notExpandingAndKeyguardLocked_shouldDoNothing() { val expectedInfo = noteTaskInfo.copy(isKeyguardLocked = true) createNoteTaskController() @@ -268,8 +287,8 @@ internal class NoteTaskControllerTest : SysuiTestCase() { verifyZeroInteractions(context) val intentCaptor = argumentCaptor() - verify(bubbles).showOrHideAppBubble(capture(intentCaptor), eq(userTracker.userHandle), - isNull()) + verify(bubbles) + .showOrHideAppBubble(capture(intentCaptor), eq(userTracker.userHandle), isNull()) intentCaptor.value.let { intent -> assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME) @@ -333,11 +352,11 @@ internal class NoteTaskControllerTest : SysuiTestCase() { verify(context.packageManager) .setComponentEnabledSetting( argument.capture(), - eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), + eq(COMPONENT_ENABLED_STATE_ENABLED), eq(PackageManager.DONT_KILL_APP), ) - val expected = ComponentName(context, CreateNoteTaskShortcutActivity::class.java) - assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString()) + assertThat(argument.value.className) + .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) } @Test @@ -348,11 +367,11 @@ internal class NoteTaskControllerTest : SysuiTestCase() { verify(context.packageManager) .setComponentEnabledSetting( argument.capture(), - eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), + eq(COMPONENT_ENABLED_STATE_DISABLED), eq(PackageManager.DONT_KILL_APP), ) - val expected = ComponentName(context, CreateNoteTaskShortcutActivity::class.java) - assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString()) + assertThat(argument.value.className) + .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) } // endregion @@ -403,8 +422,8 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) val intentCaptor = argumentCaptor() - verify(bubbles).showOrHideAppBubble(capture(intentCaptor), eq(userTracker.userHandle), - isNull()) + verify(bubbles) + .showOrHideAppBubble(capture(intentCaptor), eq(userTracker.userHandle), isNull()) intentCaptor.value.let { intent -> assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME) @@ -427,8 +446,8 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.QUICK_AFFORDANCE) val intentCaptor = argumentCaptor() - verify(bubbles).showOrHideAppBubble(capture(intentCaptor), eq(userTracker.userHandle), - isNull()) + verify(bubbles) + .showOrHideAppBubble(capture(intentCaptor), eq(userTracker.userHandle), isNull()) intentCaptor.value.let { intent -> assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME) @@ -438,7 +457,78 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } // endregion + // region updateNoteTaskAsUser + @Test + fun updateNoteTaskAsUser_withNotesRole_withShortcuts_shouldUpdateShortcuts() { + createNoteTaskController(isEnabled = true).updateNoteTaskAsUser(userTracker.userHandle) + + val actualComponent = argumentCaptor() + verify(context.packageManager) + .setComponentEnabledSetting( + actualComponent.capture(), + eq(COMPONENT_ENABLED_STATE_ENABLED), + eq(PackageManager.DONT_KILL_APP), + ) + assertThat(actualComponent.value.className) + .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) + verify(shortcutManager, never()).disableShortcuts(any()) + verify(shortcutManager).enableShortcuts(listOf(SHORTCUT_ID)) + val actualShortcuts = argumentCaptor>() + verify(shortcutManager).updateShortcuts(actualShortcuts.capture()) + val actualShortcut = actualShortcuts.value.first() + assertThat(actualShortcut.id).isEqualTo(SHORTCUT_ID) + assertThat(actualShortcut.intent?.component?.className) + .isEqualTo(LaunchNoteTaskActivity::class.java.name) + assertThat(actualShortcut.intent?.action).isEqualTo(Intent.ACTION_CREATE_NOTE) + assertThat(actualShortcut.shortLabel).isEqualTo(NOTES_SHORT_LABEL) + assertThat(actualShortcut.isLongLived).isEqualTo(true) + assertThat(actualShortcut.icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget) + assertThat(actualShortcut.extras?.getString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE)) + .isEqualTo(NOTES_PACKAGE_NAME) + } + + @Test + fun updateNoteTaskAsUser_noNotesRole_shouldDisableShortcuts() { + whenever(roleManager.getRoleHoldersAsUser(ROLE_NOTES, userTracker.userHandle)) + .thenReturn(emptyList()) + + createNoteTaskController(isEnabled = true).updateNoteTaskAsUser(userTracker.userHandle) + + val argument = argumentCaptor() + verify(context.packageManager) + .setComponentEnabledSetting( + argument.capture(), + eq(COMPONENT_ENABLED_STATE_DISABLED), + eq(PackageManager.DONT_KILL_APP), + ) + assertThat(argument.value.className) + .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) + verify(shortcutManager).disableShortcuts(listOf(SHORTCUT_ID)) + verify(shortcutManager, never()).enableShortcuts(any()) + verify(shortcutManager, never()).updateShortcuts(any()) + } + + @Test + fun updateNoteTaskAsUser_flagDisabled_shouldDisableShortcuts() { + createNoteTaskController(isEnabled = false).updateNoteTaskAsUser(userTracker.userHandle) + + val argument = argumentCaptor() + verify(context.packageManager) + .setComponentEnabledSetting( + argument.capture(), + eq(COMPONENT_ENABLED_STATE_DISABLED), + eq(PackageManager.DONT_KILL_APP), + ) + assertThat(argument.value.className) + .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name) + verify(shortcutManager).disableShortcuts(listOf(SHORTCUT_ID)) + verify(shortcutManager, never()).enableShortcuts(any()) + verify(shortcutManager, never()).updateShortcuts(any()) + } + // endregion + private companion object { + const val NOTES_SHORT_LABEL = "Notetaking" const val NOTES_PACKAGE_NAME = "com.android.note.app" const val NOTES_UID = 123456 } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt index 46e02788b2df0..cd67e8d0a4c27 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -15,36 +15,37 @@ */ package com.android.systemui.notetask +import android.app.role.RoleManager import android.test.suitebuilder.annotation.SmallTest import android.view.KeyEvent import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.time.FakeSystemClock import com.android.wm.shell.bubbles.Bubbles import java.util.Optional import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations -/** - * Tests for [NoteTaskController]. - * - * Build/Install/Run: - * - atest SystemUITests:NoteTaskInitializerTest - */ +/** atest SystemUITests:NoteTaskInitializerTest */ @SmallTest @RunWith(AndroidJUnit4::class) internal class NoteTaskInitializerTest : SysuiTestCase() { @Mock lateinit var commandQueue: CommandQueue @Mock lateinit var bubbles: Bubbles - @Mock lateinit var noteTaskController: NoteTaskController + @Mock lateinit var controller: NoteTaskController + @Mock lateinit var roleManager: RoleManager + private val clock = FakeSystemClock() + private val executor = FakeExecutor(clock) @Before fun setUp() { @@ -56,47 +57,41 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { bubbles: Bubbles? = this.bubbles, ): NoteTaskInitializer { return NoteTaskInitializer( - controller = noteTaskController, + controller = controller, commandQueue = commandQueue, optionalBubbles = Optional.ofNullable(bubbles), isEnabled = isEnabled, + roleManager = roleManager, + backgroundExecutor = executor, ) } // region initializer @Test - fun initialize_shouldAddCallbacks() { + fun initialize() { createNoteTaskInitializer().initialize() + verify(controller).setNoteTaskShortcutEnabled(true) verify(commandQueue).addCallback(any()) + verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any()) } @Test - fun initialize_flagDisabled_shouldDoNothing() { + fun initialize_flagDisabled() { createNoteTaskInitializer(isEnabled = false).initialize() + verify(controller, never()).setNoteTaskShortcutEnabled(any()) verify(commandQueue, never()).addCallback(any()) + verify(roleManager, never()).addOnRoleHoldersChangedListenerAsUser(any(), any(), any()) } @Test - fun initialize_bubblesNotPresent_shouldDoNothing() { + fun initialize_bubblesNotPresent() { createNoteTaskInitializer(bubbles = null).initialize() + verify(controller, never()).setNoteTaskShortcutEnabled(any()) verify(commandQueue, never()).addCallback(any()) - } - - @Test - fun initialize_flagEnabled_shouldEnableShortcut() { - createNoteTaskInitializer().initialize() - - verify(noteTaskController).setNoteTaskShortcutEnabled(true) - } - - @Test - fun initialize_flagDisabled_shouldDisableShortcut() { - createNoteTaskInitializer(isEnabled = false).initialize() - - verify(noteTaskController).setNoteTaskShortcutEnabled(false) + verify(roleManager, never()).addOnRoleHoldersChangedListenerAsUser(any(), any(), any()) } // endregion @@ -105,14 +100,14 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() { createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL) - verify(noteTaskController).showNoteTask(entryPoint = NoteTaskEntryPoint.TAIL_BUTTON) + verify(controller).showNoteTask(entryPoint = NoteTaskEntryPoint.TAIL_BUTTON) } @Test fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() { createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN) - verifyZeroInteractions(noteTaskController) + verifyZeroInteractions(controller) } // endregion }