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

Commit 7a262235 authored by Joshua Mokut's avatar Joshua Mokut Committed by Android (Google) Code Review
Browse files

Merge "dynamic shortcut label/icon retrieval for app launch shortcuts" into main

parents e6378dbf 51d9b10c
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -385,6 +385,9 @@
         is ready -->
    <uses-permission android:name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" />

    <!-- To be able to decipher default applications for certain roles in shortcut helper -->
    <uses-permission android:name="android.permission.MANAGE_DEFAULT_APPLICATIONS" />

    <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" />
    <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" />
    <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />
+195 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.keyboard.shortcut.data.repository

import android.app.role.RoleManager
import android.app.role.roleManager
import android.content.Context
import android.content.Intent
import android.content.mockedContext
import android.content.packageManager
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.hardware.input.AppLaunchData
import android.hardware.input.AppLaunchData.RoleData
import android.hardware.input.InputGestureData
import android.hardware.input.InputGestureData.createKeyTrigger
import android.view.KeyEvent.KEYCODE_A
import android.view.KeyEvent.META_ALT_ON
import android.view.KeyEvent.META_CTRL_ON
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.app.ResolverActivity
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyboard.shortcut.data.model.InternalGroupsSource
import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup
import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo
import com.android.systemui.keyboard.shortcut.inputGestureDataAdapter
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
import com.android.systemui.kosmos.runTest
import com.android.systemui.res.R
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.settings.userTracker
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever


@SmallTest
@RunWith(AndroidJUnit4::class)
class InputGestureDataAdapterTest : SysuiTestCase() {

    private val kosmos = testKosmos().also { kosmos ->
        kosmos.userTracker = FakeUserTracker(onCreateCurrentUserContext = { kosmos.mockedContext })
    }
    private val adapter = kosmos.inputGestureDataAdapter
    private val roleManager = kosmos.roleManager
    private val packageManager: PackageManager = kosmos.packageManager
    private val mockUserContext: Context = kosmos.mockedContext
    private val intent: Intent = mock()
    private val fakeResolverActivityInfo =
        ActivityInfo().apply { name = ResolverActivity::class.qualifiedName }
    private val fakeActivityInfo: ActivityInfo =
        ActivityInfo().apply {
            name = FAKE_ACTIVITY_NAME
            icon = 0x1
            nonLocalizedLabel = TEST_SHORTCUT_LABEL
        }
    private val mockSelectorIntent: Intent = mock()

    @Before
    fun setup() {
        whenever(mockUserContext.packageManager).thenReturn(packageManager)
        whenever(mockUserContext.getSystemService(RoleManager::class.java)).thenReturn(roleManager)
        whenever(roleManager.isRoleAvailable(TEST_ROLE)).thenReturn(true)
        whenever(roleManager.getDefaultApplication(TEST_ROLE)).thenReturn(TEST_ROLE_PACKAGE)
        whenever(packageManager.getActivityInfo(any(), anyInt())).thenReturn(mock())
        whenever(packageManager.getLaunchIntentForPackage(TEST_ROLE_PACKAGE)).thenReturn(intent)
        whenever(intent.selector).thenReturn(mockSelectorIntent)
        whenever(mockSelectorIntent.categories).thenReturn(setOf(TEST_ACTIVITY_CATEGORY))
    }

    @Test
    fun shortcutLabel_whenDefaultAppForCategoryIsNotSet_loadsLabelFromFirstAppMatchingIntent() =
        kosmos.runTest {
            setApiToRetrieveResolverActivity()

            val inputGestureData = buildInputGestureDataForAppLaunchShortcut()
            val internalGroups = adapter.toInternalGroupSources(listOf(inputGestureData))
            val label =
                internalGroups.firstOrNull()?.groups?.firstOrNull()?.items?.firstOrNull()?.label

            assertThat(label).isEqualTo(expectedShortcutLabelForFirstAppMatchingIntent)
        }

    @Test
    fun shortcutLabel_whenDefaultAppForCategoryIsSet_loadsLabelOfDefaultApp() {
        kosmos.runTest {
            setApiToRetrieveSpecificActivity()

            val inputGestureData = buildInputGestureDataForAppLaunchShortcut()
            val internalGroups = adapter.toInternalGroupSources(listOf(inputGestureData))
            val label =
                internalGroups.firstOrNull()?.groups?.firstOrNull()?.items?.firstOrNull()?.label

            assertThat(label).isEqualTo(TEST_SHORTCUT_LABEL)
        }
    }

    @Test
    fun shortcutIcon_whenDefaultAppForCategoryIsSet_loadsIconOfDefaultApp() {
        kosmos.runTest {
            setApiToRetrieveSpecificActivity()

            val inputGestureData = buildInputGestureDataForAppLaunchShortcut()
            val internalGroups = adapter.toInternalGroupSources(listOf(inputGestureData))
            val icon =
                internalGroups.firstOrNull()?.groups?.firstOrNull()?.items?.firstOrNull()?.icon

            assertThat(icon).isNotNull()
        }
    }

    @Test
    fun internalGroupSource_isCorrectlyConvertedWithSimpleInputGestureData() =
        kosmos.runTest {
            setApiToRetrieveResolverActivity()

            val inputGestureData = buildInputGestureDataForAppLaunchShortcut()
            val internalGroups = adapter.toInternalGroupSources(listOf(inputGestureData))

            assertThat(internalGroups).containsExactly(
                InternalGroupsSource(
                    type = ShortcutCategoryType.AppCategories,
                    groups = listOf(
                        InternalKeyboardShortcutGroup(
                            label = APPLICATION_SHORTCUT_GROUP_LABEL,
                            items = listOf(
                                InternalKeyboardShortcutInfo(
                                    label = expectedShortcutLabelForFirstAppMatchingIntent,
                                    keycode = KEYCODE_A,
                                    modifiers = META_CTRL_ON or META_ALT_ON,
                                    isCustomShortcut = true
                                )
                            )
                        )
                    )
                )
            )
        }

    private fun setApiToRetrieveResolverActivity() {
        whenever(intent.resolveActivityInfo(eq(packageManager), anyInt()))
            .thenReturn(fakeResolverActivityInfo)
    }

    private fun setApiToRetrieveSpecificActivity() {
        whenever(intent.resolveActivityInfo(eq(packageManager), anyInt()))
            .thenReturn(fakeActivityInfo)
    }


    private fun buildInputGestureDataForAppLaunchShortcut(
        keyCode: Int = KEYCODE_A,
        modifiers: Int = META_CTRL_ON or META_ALT_ON,
        appLaunchData: AppLaunchData = RoleData(TEST_ROLE)
    ): InputGestureData {
        return InputGestureData.Builder()
            .setTrigger(createKeyTrigger(keyCode, modifiers))
            .setAppLaunchData(appLaunchData)
            .build()
    }

    private val expectedShortcutLabelForFirstAppMatchingIntent =
        context.getString(R.string.keyboard_shortcut_group_applications_browser)

    private companion object {
        private const val TEST_ROLE = "Test Browser Role"
        private const val TEST_ROLE_PACKAGE = "test.browser.package"
        private const val APPLICATION_SHORTCUT_GROUP_LABEL = "Applications"
        private const val FAKE_ACTIVITY_NAME = "Fake activity"
        private const val TEST_SHORTCUT_LABEL = "Test shortcut label"
        private const val TEST_ACTIVITY_CATEGORY = Intent.CATEGORY_APP_BROWSER
    }
}
+0 −28
Original line number Diff line number Diff line
@@ -567,13 +567,6 @@ object TestShortcuts {
            ),
            simpleShortcutCategory(System, "System controls", "Show shortcuts"),
            simpleShortcutCategory(System, "System controls", "View recent apps"),
            simpleShortcutCategory(AppCategories, "Applications", "Calculator"),
            simpleShortcutCategory(AppCategories, "Applications", "Calendar"),
            simpleShortcutCategory(AppCategories, "Applications", "Browser"),
            simpleShortcutCategory(AppCategories, "Applications", "Contacts"),
            simpleShortcutCategory(AppCategories, "Applications", "Email"),
            simpleShortcutCategory(AppCategories, "Applications", "Maps"),
            simpleShortcutCategory(AppCategories, "Applications", "SMS"),
        )
    val customInputGestureTypeHome = simpleInputGestureData(keyGestureType = KEY_GESTURE_TYPE_HOME)

@@ -614,27 +607,6 @@ object TestShortcuts {
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER
            ),
            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR
            ),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR
            ),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER
            ),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS
            ),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL
            ),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS
            ),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING
            ),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER
            ),
+6 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.hardware.input.InputGestureData
import android.view.KeyboardShortcutGroup
import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesUtils
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType

/**
 * Internal Keyboard Shortcut models to use with [ShortcutCategoriesUtils.fetchShortcutCategory]
@@ -55,3 +56,8 @@ data class InternalKeyboardShortcutInfo(
    val icon: Icon? = null,
    val isCustomShortcut: Boolean = false,
)

data class InternalGroupsSource(
    val groups: List<InternalKeyboardShortcutGroup>,
    val type: ShortcutCategoryType,
)
 No newline at end of file
+6 −74
Original line number Diff line number Diff line
@@ -16,10 +16,8 @@

package com.android.systemui.keyboard.shortcut.data.repository

import android.content.Context
import android.hardware.input.InputGestureData
import android.hardware.input.InputGestureData.Builder
import android.hardware.input.InputGestureData.KeyTrigger
import android.hardware.input.InputGestureData.createKeyTrigger
import android.hardware.input.InputManager
import android.hardware.input.KeyGestureEvent.KeyGestureType
@@ -30,11 +28,8 @@ import com.android.systemui.Flags.shortcutHelperKeyGlyph
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult
import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup
import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo
import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
@@ -57,8 +52,7 @@ constructor(
    @Background private val backgroundScope: CoroutineScope,
    @Background private val bgCoroutineContext: CoroutineContext,
    private val shortcutCategoriesUtils: ShortcutCategoriesUtils,
    private val context: Context,
    private val inputGestureMaps: InputGestureMaps,
    private val inputGestureDataAdapter: InputGestureDataAdapter,
    private val customInputGesturesRepository: CustomInputGesturesRepository,
    private val inputManager: InputManager
) : ShortcutCategoriesRepository {
@@ -116,7 +110,7 @@ constructor(
                if (inputDevice == null) {
                    emptyList()
                } else {
                    val sources = toInternalGroupSources(inputGestures)
                    val sources = inputGestureDataAdapter.toInternalGroupSources(inputGestures)
                    val supportedKeyCodes =
                        shortcutCategoriesUtils.fetchSupportedKeyCodes(
                            inputDevice.id,
@@ -216,7 +210,8 @@ constructor(
            return null
        }

        return inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutBeingCustomized.label]
        return inputGestureDataAdapter
            .getKeyGestureTypeFromShortcutLabel(shortcutBeingCustomized.label)
    }

    @KeyGestureType
@@ -232,7 +227,8 @@ constructor(
            return null
        }

        return inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutBeingCustomized.label]
        return inputGestureDataAdapter
            .getKeyGestureTypeFromShortcutLabel(shortcutBeingCustomized.label)
    }

    private fun Builder.addTriggerFromSelectedKeyCombination(): Builder {
@@ -261,70 +257,6 @@ constructor(
        return _shortcutBeingCustomized.value
    }

    private fun toInternalGroupSources(
        inputGestures: List<InputGestureData>
    ): List<InternalGroupsSource> {
        val ungroupedInternalGroupSources =
            inputGestures.mapNotNull { gestureData ->
                val keyTrigger = gestureData.trigger as KeyTrigger
                val keyGestureType = gestureData.action.keyGestureType()
                fetchGroupLabelByGestureType(keyGestureType)?.let { groupLabel ->
                    toInternalKeyboardShortcutInfo(keyGestureType, keyTrigger)?.let {
                        internalKeyboardShortcutInfo ->
                        val group =
                            InternalKeyboardShortcutGroup(
                                label = groupLabel,
                                items = listOf(internalKeyboardShortcutInfo),
                            )

                        fetchShortcutCategoryTypeByGestureType(keyGestureType)?.let {
                            InternalGroupsSource(groups = listOf(group), type = it)
                        }
                    }
                }
            }

        return ungroupedInternalGroupSources
    }

    private fun toInternalKeyboardShortcutInfo(
        keyGestureType: Int,
        keyTrigger: KeyTrigger,
    ): InternalKeyboardShortcutInfo? {
        fetchShortcutInfoLabelByGestureType(keyGestureType)?.let {
            return InternalKeyboardShortcutInfo(
                label = it,
                keycode = keyTrigger.keycode,
                modifiers = keyTrigger.modifierState,
                isCustomShortcut = true,
            )
        }
        return null
    }

    private fun fetchGroupLabelByGestureType(@KeyGestureType keyGestureType: Int): String? {
        inputGestureMaps.gestureToInternalKeyboardShortcutGroupLabelResIdMap[keyGestureType]?.let {
            return context.getString(it)
        } ?: return null
    }

    private fun fetchShortcutInfoLabelByGestureType(@KeyGestureType keyGestureType: Int): String? {
        inputGestureMaps.gestureToInternalKeyboardShortcutInfoLabelResIdMap[keyGestureType]?.let {
            return context.getString(it)
        } ?: return null
    }

    private fun fetchShortcutCategoryTypeByGestureType(
        @KeyGestureType keyGestureType: Int
    ): ShortcutCategoryType? {
        return inputGestureMaps.gestureToShortcutCategoryTypeMap[keyGestureType]
    }

    private data class InternalGroupsSource(
        val groups: List<InternalKeyboardShortcutGroup>,
        val type: ShortcutCategoryType,
    )

    private companion object {
        private const val TAG = "CustomShortcutCategoriesRepository"
    }
Loading