diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 08d18575da796..6bfe1a099c51d 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -17,10 +17,12 @@ package com.android.systemui.notetask import android.app.KeyguardManager +import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import android.os.UserManager +import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.util.kotlin.getOrNull @@ -57,7 +59,7 @@ constructor( * If the keyguard is locked, notes will open as a full screen experience. A locked device has * no contextual information which let us use the whole screen space available. * - * If no in multi-window or the keyguard is unlocked, notes will open as a bubble OR it will be + * If not in multi-window or the keyguard is unlocked, notes will open as a bubble OR it will be * collapsed if the notes bubble is already opened. * * That will let users open other apps in full screen, and take contextual notes. @@ -68,16 +70,23 @@ constructor( val bubbles = optionalBubbles.getOrNull() ?: return val keyguardManager = optionalKeyguardManager.getOrNull() ?: return val userManager = optionalUserManager.getOrNull() ?: return - val intent = intentResolver.resolveIntent() ?: return // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing. if (!userManager.isUserUnlocked) return - if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) { - context.startActivity(intent) - } else { - // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter. - bubbles.showOrHideAppBubble(intent) + val intent = intentResolver.resolveIntent() ?: return + + // TODO(b/266686199): We should handle when app not available. For now, we log. + try { + if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) { + context.startActivity(intent) + } else { + bubbles.showOrHideAppBubble(intent) + } + } catch (e: ActivityNotFoundException) { + val message = + "Activity not found for action: ${NoteTaskIntentResolver.ACTION_CREATE_NOTE}." + Log.e(TAG, message, e) } } @@ -106,6 +115,8 @@ constructor( } companion object { + private val TAG = NoteTaskController::class.simpleName.orEmpty() + // TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead. const val NOTE_TASK_KEY_EVENT = 311 } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt index 4b10d69a2b800..11dc1d7eb8043 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt @@ -16,70 +16,39 @@ package com.android.systemui.notetask -import android.content.ComponentName +import android.app.role.RoleManager +import android.content.Context import android.content.Intent -import android.content.pm.ActivityInfo -import android.content.pm.PackageManager -import android.content.pm.PackageManager.ResolveInfoFlags -import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE import javax.inject.Inject -/** - * Class responsible to query all apps and find one that can handle the [ACTION_CREATE_NOTE]. If - * found, an [Intent] ready for be launched will be returned. Otherwise, returns null. - * - * TODO(b/248274123): should be revisited once the notes role is implemented. - */ internal class NoteTaskIntentResolver @Inject constructor( - private val packageManager: PackageManager, + private val context: Context, + private val roleManager: RoleManager, ) { fun resolveIntent(): Intent? { - val intent = Intent(ACTION_CREATE_NOTE) - val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) - val infoList = packageManager.queryIntentActivities(intent, flags) + val packageName = roleManager.getRoleHoldersAsUser(ROLE_NOTES, context.user).firstOrNull() - for (info in infoList) { - val packageName = info.activityInfo.applicationInfo.packageName ?: continue - val activityName = resolveActivityNameForNotesAction(packageName) ?: continue + if (packageName.isNullOrEmpty()) return null - return Intent(ACTION_CREATE_NOTE) - .setPackage(packageName) - .setComponent(ComponentName(packageName, activityName)) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - - return null - } - - private fun resolveActivityNameForNotesAction(packageName: String): String? { - val intent = Intent(ACTION_CREATE_NOTE).setPackage(packageName) - val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) - val resolveInfo = packageManager.resolveActivity(intent, flags) - - val activityInfo = resolveInfo?.activityInfo ?: return null - if (activityInfo.name.isNullOrBlank()) return null - if (!activityInfo.exported) return null - if (!activityInfo.enabled) return null - if (!activityInfo.showWhenLocked) return null - if (!activityInfo.turnScreenOn) return null - - return activityInfo.name + return Intent(ACTION_CREATE_NOTE) + .setPackage(packageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint was + // used to start it. + .putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true) } companion object { - // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead. + // TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead. const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE" // TODO(b/265912743): Use RoleManager.NOTES_ROLE instead. - const val NOTE_ROLE = "android.app.role.NOTES" + const val ROLE_NOTES = "android.app.role.NOTES" + + // TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead. + const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE" } } - -private val ActivityInfo.showWhenLocked: Boolean - get() = flags and ActivityInfo.FLAG_SHOW_WHEN_LOCKED != 0 - -private val ActivityInfo.turnScreenOn: Boolean - get() = flags and ActivityInfo.FLAG_TURN_SCREEN_ON != 0 diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt index 22ce121321247..ec6a16accc4de 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -51,7 +51,7 @@ internal interface NoteTaskModule { featureFlags: FeatureFlags, roleManager: RoleManager, ): Boolean { - val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskIntentResolver.NOTE_ROLE) + val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskIntentResolver.ROLE_NOTES) val isFeatureEnabled = featureFlags.isEnabled(Flags.NOTE_TASKS) return isRoleAvailable && isFeatureEnabled } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt index bbe60f4ba4935..18be92ba27cf4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt @@ -16,17 +16,14 @@ package com.android.systemui.notetask -import android.content.ComponentName +import android.app.role.RoleManager import android.content.Intent -import android.content.pm.ActivityInfo -import android.content.pm.ApplicationInfo import android.content.pm.PackageManager -import android.content.pm.PackageManager.ResolveInfoFlags -import android.content.pm.ResolveInfo import android.test.suitebuilder.annotation.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -47,172 +44,39 @@ import org.mockito.MockitoAnnotations internal class NoteTaskIntentResolverTest : SysuiTestCase() { @Mock lateinit var packageManager: PackageManager + @Mock lateinit var roleManager: RoleManager - private lateinit var resolver: NoteTaskIntentResolver + private lateinit var underTest: NoteTaskIntentResolver @Before fun setUp() { MockitoAnnotations.initMocks(this) - resolver = NoteTaskIntentResolver(packageManager) - } - - private fun createResolveInfo( - activityInfo: ActivityInfo? = createActivityInfo(), - ): ResolveInfo { - return ResolveInfo().apply { this.activityInfo = activityInfo } - } - - private fun createActivityInfo( - packageName: String = "PackageName", - name: String? = "ActivityName", - exported: Boolean = true, - enabled: Boolean = true, - showWhenLocked: Boolean = true, - turnScreenOn: Boolean = true, - ): ActivityInfo { - return ActivityInfo().apply { - this.name = name - this.exported = exported - this.enabled = enabled - if (showWhenLocked) { - flags = flags or ActivityInfo.FLAG_SHOW_WHEN_LOCKED - } - if (turnScreenOn) { - flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON - } - this.applicationInfo = ApplicationInfo().apply { this.packageName = packageName } - } - } - - private fun givenQueryIntentActivities(block: () -> List) { - whenever(packageManager.queryIntentActivities(any(), any())) - .thenReturn(block()) - } - - private fun givenResolveActivity(block: () -> ResolveInfo?) { - whenever(packageManager.resolveActivity(any(), any())).thenReturn(block()) + underTest = NoteTaskIntentResolver(context, roleManager) } @Test - fun resolveIntent_shouldReturnNotesIntent() { - givenQueryIntentActivities { listOf(createResolveInfo()) } - givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo()) } + fun resolveIntent_shouldReturnIntentInStylusMode() { + val packageName = "com.android.note.app" + whenever(roleManager.getRoleHoldersAsUser(NoteTaskIntentResolver.ROLE_NOTES, context.user)) + .then { listOf(packageName) } - val actual = resolver.resolveIntent() + val actual = underTest.resolveIntent() - val expected = - Intent(ACTION_CREATE_NOTE) - .setPackage("PackageName") - .setComponent(ComponentName("PackageName", "ActivityName")) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - // Compares the string representation of both intents, as they are different instances. - assertThat(actual.toString()).isEqualTo(expected.toString()) + requireNotNull(actual) { "Intent must not be null" } + assertThat(actual.action).isEqualTo(ACTION_CREATE_NOTE) + assertThat(actual.`package`).isEqualTo(packageName) + val expectedExtra = actual.getExtra(NoteTaskIntentResolver.INTENT_EXTRA_USE_STYLUS_MODE) + assertThat(expectedExtra).isEqualTo(true) + val expectedFlag = actual.flags and Intent.FLAG_ACTIVITY_NEW_TASK + assertThat(expectedFlag).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK) } @Test - fun resolveIntent_activityInfoEnabledIsFalse_shouldReturnNull() { - givenQueryIntentActivities { listOf(createResolveInfo()) } - givenResolveActivity { - createResolveInfo(activityInfo = createActivityInfo(enabled = false)) - } + fun resolveIntent_noRoleHolderIsSet_shouldReturnNull() { + whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskIntentResolver.ROLE_NOTES), any())) + .then { listOf() } - val actual = resolver.resolveIntent() - - assertThat(actual).isNull() - } - - @Test - fun resolveIntent_activityInfoExportedIsFalse_shouldReturnNull() { - givenQueryIntentActivities { listOf(createResolveInfo()) } - givenResolveActivity { - createResolveInfo(activityInfo = createActivityInfo(exported = false)) - } - - val actual = resolver.resolveIntent() - - assertThat(actual).isNull() - } - - @Test - fun resolveIntent_activityInfoShowWhenLockedIsFalse_shouldReturnNull() { - givenQueryIntentActivities { listOf(createResolveInfo()) } - givenResolveActivity { - createResolveInfo(activityInfo = createActivityInfo(showWhenLocked = false)) - } - - val actual = resolver.resolveIntent() - - assertThat(actual).isNull() - } - - @Test - fun resolveIntent_activityInfoTurnScreenOnIsFalse_shouldReturnNull() { - givenQueryIntentActivities { listOf(createResolveInfo()) } - givenResolveActivity { - createResolveInfo(activityInfo = createActivityInfo(turnScreenOn = false)) - } - - val actual = resolver.resolveIntent() - - assertThat(actual).isNull() - } - - @Test - fun resolveIntent_activityInfoNameIsBlank_shouldReturnNull() { - givenQueryIntentActivities { listOf(createResolveInfo()) } - givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = "")) } - - val actual = resolver.resolveIntent() - - assertThat(actual).isNull() - } - - @Test - fun resolveIntent_activityInfoNameIsNull_shouldReturnNull() { - givenQueryIntentActivities { listOf(createResolveInfo()) } - givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = null)) } - - val actual = resolver.resolveIntent() - - assertThat(actual).isNull() - } - - @Test - fun resolveIntent_activityInfoIsNull_shouldReturnNull() { - givenQueryIntentActivities { listOf(createResolveInfo()) } - givenResolveActivity { createResolveInfo(activityInfo = null) } - - val actual = resolver.resolveIntent() - - assertThat(actual).isNull() - } - - @Test - fun resolveIntent_resolveActivityIsNull_shouldReturnNull() { - givenQueryIntentActivities { listOf(createResolveInfo()) } - givenResolveActivity { null } - - val actual = resolver.resolveIntent() - - assertThat(actual).isNull() - } - - @Test - fun resolveIntent_packageNameIsBlank_shouldReturnNull() { - givenQueryIntentActivities { - listOf(createResolveInfo(createActivityInfo(packageName = ""))) - } - - val actual = resolver.resolveIntent() - - assertThat(actual).isNull() - } - - @Test - fun resolveIntent_activityNotFoundForAction_shouldReturnNull() { - givenQueryIntentActivities { emptyList() } - - val actual = resolver.resolveIntent() + val actual = underTest.resolveIntent() assertThat(actual).isNull() }