Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bee90c26 authored by Marcello Galhardo's avatar Marcello Galhardo
Browse files

Use Note Role Holder when opening Note Task

Test: atest SystemUITests:NoteTaskIntentResolverTest

Fixes: b/266058122
Fixes: b/254606432

Change-Id: I74c85b543e9d0f750a2ed1f1b12ed4f94594be1e
parent 5c0efdcd
Loading
Loading
Loading
Loading
+18 −7
Original line number Diff line number Diff line
@@ -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,17 +70,24 @@ 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

        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 {
            // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter.
                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
    }
+17 −48
Original line number Diff line number Diff line
@@ -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
            // 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
+1 −1
Original line number Diff line number Diff line
@@ -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
        }
+21 −157
Original line number Diff line number Diff line
@@ -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<ResolveInfo>) {
        whenever(packageManager.queryIntentActivities(any(), any<ResolveInfoFlags>()))
            .thenReturn(block())
    }

    private fun givenResolveActivity(block: () -> ResolveInfo?) {
        whenever(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(block())
    }

    @Test
    fun resolveIntent_shouldReturnNotesIntent() {
        givenQueryIntentActivities { listOf(createResolveInfo()) }
        givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo()) }

        val actual = resolver.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())
    }

    @Test
    fun resolveIntent_activityInfoEnabledIsFalse_shouldReturnNull() {
        givenQueryIntentActivities { listOf(createResolveInfo()) }
        givenResolveActivity {
            createResolveInfo(activityInfo = createActivityInfo(enabled = false))
        }

        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()
        underTest = NoteTaskIntentResolver(context, roleManager)
    }

    @Test
    fun resolveIntent_activityInfoNameIsBlank_shouldReturnNull() {
        givenQueryIntentActivities { listOf(createResolveInfo()) }
        givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = "")) }
    fun resolveIntent_shouldReturnIntentInStylusMode() {
        val packageName = "com.android.note.app"
        whenever(roleManager.getRoleHoldersAsUser(NoteTaskIntentResolver.ROLE_NOTES, context.user))
            .then { listOf(packageName) }

        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()
    }
        val actual = underTest.resolveIntent()

    @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()
        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_activityNotFoundForAction_shouldReturnNull() {
        givenQueryIntentActivities { emptyList() }
    fun resolveIntent_noRoleHolderIsSet_shouldReturnNull() {
        whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskIntentResolver.ROLE_NOTES), any()))
            .then { listOf<String>() }

        val actual = resolver.resolveIntent()
        val actual = underTest.resolveIntent()

        assertThat(actual).isNull()
    }