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

Commit 332aff48 authored by Chris Göllner's avatar Chris Göllner
Browse files

Shortcut Helper - Retrieve app categories shortcuts from WindowManager

Before, we were hard coding the shortcuts. Now there is a new API from
WindowManager to retrieve these shortcuts, which are defined in a
config file.

Fixes: 341045436
Test: Manual
Test: AppCategoriesShortcutsSourceTest.kt
Flag: com.android.systemui.keyboard_shortcut_helper_rewrite
Change-Id: I2d3234129c37b777e4e18e213f7f9af0ee8d781a
parent 606145ec
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -233,7 +233,7 @@ constructor(
            Log.wtf(TAG, "Unsupported modifiers remaining: $remainingModifiers")
            return null
        }
        if (info.keycode != 0) {
        if (info.keycode != 0 || info.baseCharacter > Char.MIN_VALUE) {
            keys += toShortcutKey(keyCharacterMap, info.keycode, info.baseCharacter) ?: return null
        }
        if (keys.isEmpty()) {
@@ -253,7 +253,7 @@ constructor(
            return ShortcutKey.Icon(iconResId)
        }
        if (baseCharacter > Char.MIN_VALUE) {
            return ShortcutKey.Text(baseCharacter.toString())
            return ShortcutKey.Text(baseCharacter.uppercase())
        }
        val specialKeyLabel = ShortcutHelperKeys.specialKeyLabels[keyCode]
        if (specialKeyLabel != null) {
+17 −80
Original line number Diff line number Diff line
@@ -16,92 +16,29 @@

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

import android.content.Intent
import android.content.res.Resources
import android.view.KeyEvent
import android.view.KeyboardShortcutGroup
import android.view.KeyboardShortcutInfo
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
import com.android.systemui.util.icons.AppCategoryIconProvider
import android.view.WindowManager
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.shortcut.extensions.copy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext

class AppCategoriesShortcutsSource
@Inject
constructor(
    private val appCategoryIconProvider: AppCategoryIconProvider,
    @Main private val resources: Resources,
    private val windowManager: WindowManager,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
) : KeyboardShortcutGroupsSource {

    override suspend fun shortcutGroups(deviceId: Int) =
        listOf(
            KeyboardShortcutGroup(
                /* label = */ resources.getString(R.string.keyboard_shortcut_group_applications),
                /* items = */ shortcuts()
            )
        )

    private suspend fun shortcuts(): List<KeyboardShortcutInfo> =
        listOfNotNull(
                assistantAppShortcutInfo(),
                appCategoryShortcutInfo(
                    Intent.CATEGORY_APP_BROWSER,
                    R.string.keyboard_shortcut_group_applications_browser,
                    KeyEvent.KEYCODE_B
                ),
                appCategoryShortcutInfo(
                    Intent.CATEGORY_APP_CONTACTS,
                    R.string.keyboard_shortcut_group_applications_contacts,
                    KeyEvent.KEYCODE_C
                ),
                appCategoryShortcutInfo(
                    Intent.CATEGORY_APP_EMAIL,
                    R.string.keyboard_shortcut_group_applications_email,
                    KeyEvent.KEYCODE_E
                ),
                appCategoryShortcutInfo(
                    Intent.CATEGORY_APP_CALENDAR,
                    R.string.keyboard_shortcut_group_applications_calendar,
                    KeyEvent.KEYCODE_K
                ),
                appCategoryShortcutInfo(
                    Intent.CATEGORY_APP_MAPS,
                    R.string.keyboard_shortcut_group_applications_maps,
                    KeyEvent.KEYCODE_M
                ),
                appCategoryShortcutInfo(
                    Intent.CATEGORY_APP_MUSIC,
                    R.string.keyboard_shortcut_group_applications_music,
                    KeyEvent.KEYCODE_P
                ),
                appCategoryShortcutInfo(
                    Intent.CATEGORY_APP_MESSAGING,
                    R.string.keyboard_shortcut_group_applications_sms,
                    KeyEvent.KEYCODE_S
                ),
                appCategoryShortcutInfo(
                    Intent.CATEGORY_APP_CALCULATOR,
                    R.string.keyboard_shortcut_group_applications_calculator,
                    KeyEvent.KEYCODE_U
                ),
            )
            .sortedBy { it.label!!.toString().lowercase() }

    private suspend fun assistantAppShortcutInfo(): KeyboardShortcutInfo? {
        val assistantIcon = appCategoryIconProvider.assistantAppIcon() ?: return null
        return KeyboardShortcutInfo(
            /* label = */ resources.getString(R.string.keyboard_shortcut_group_applications_assist),
            /* icon = */ assistantIcon,
            /* keycode = */ KeyEvent.KEYCODE_A,
            /* modifiers = */ KeyEvent.META_META_ON,
        )
    override suspend fun shortcutGroups(deviceId: Int): List<KeyboardShortcutGroup> =
        withContext(backgroundDispatcher) {
            val group = windowManager.getApplicationLaunchKeyboardShortcuts(deviceId)
            return@withContext if (group == null) {
                emptyList()
            } else {
                val sortedShortcutItems = group.items.sortedBy { it.label!!.toString().lowercase() }
                listOf(group.copy(items = sortedShortcutItems))
            }
        }

    private suspend fun appCategoryShortcutInfo(category: String, labelResId: Int, keycode: Int) =
        KeyboardShortcutInfo(
            /* label = */ resources.getString(labelResId),
            /* icon = */ appCategoryIconProvider.categoryAppIcon(category),
            /* keycode = */ keycode,
            /* modifiers = */ KeyEvent.META_META_ON,
        )
}
+27 −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.extensions

import android.view.KeyboardShortcutGroup
import android.view.KeyboardShortcutInfo

fun KeyboardShortcutGroup.copy(
    label: CharSequence = getLabel(),
    items: List<KeyboardShortcutInfo> = getItems(),
    isSystemGroup: Boolean = isSystemGroup(),
    packageName: CharSequence? = getPackageName(),
) = KeyboardShortcutGroup(label, items, isSystemGroup).also { it.packageName = packageName }
+27 −173
Original line number Diff line number Diff line
@@ -16,21 +16,17 @@

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

import android.content.Intent.CATEGORY_APP_BROWSER
import android.content.Intent.CATEGORY_APP_CALCULATOR
import android.content.Intent.CATEGORY_APP_CALENDAR
import android.content.Intent.CATEGORY_APP_CONTACTS
import android.content.Intent.CATEGORY_APP_EMAIL
import android.content.Intent.CATEGORY_APP_MAPS
import android.content.Intent.CATEGORY_APP_MESSAGING
import android.content.Intent.CATEGORY_APP_MUSIC
import android.view.KeyEvent
import android.view.KeyboardShortcutGroup
import android.view.KeyboardShortcutInfo
import android.view.mockWindowManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.util.icons.fakeAppCategoryIconProvider
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -43,185 +39,43 @@ class AppCategoriesShortcutsSourceTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val defaultAppIconsProvider = kosmos.fakeAppCategoryIconProvider
    private val source = kosmos.shortcutHelperAppCategoriesShortcutsSource
    private val mockWindowManager = kosmos.mockWindowManager
    private val source =
        AppCategoriesShortcutsSource(kosmos.mockWindowManager, kosmos.testDispatcher)

    private var appCategoriesGroup: KeyboardShortcutGroup? = null

    @Before
    fun setUp() {
        categoryApps.forEach { categoryAppIcon ->
            defaultAppIconsProvider.installCategoryApp(
                categoryAppIcon.category,
                categoryAppIcon.packageName,
                categoryAppIcon.iconResId
            )
        }
    }

    @Test
    fun shortcutGroups_returnsSingleGroup() =
        testScope.runTest { assertThat(source.shortcutGroups(TEST_DEVICE_ID)).hasSize(1) }

    @Test
    fun shortcutGroups_hasAssistantIcon() =
        testScope.runTest {
            defaultAppIconsProvider.installAssistantApp(ASSISTANT_PACKAGE, ASSISTANT_ICON_RES_ID)

            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items

            val shortcutInfo = shortcuts.first { it.label == "Assistant" }

            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(ASSISTANT_PACKAGE)
            assertThat(shortcutInfo.icon!!.resId).isEqualTo(ASSISTANT_ICON_RES_ID)
        }

    @Test
    fun shortcutGroups_hasBrowserIcon() =
        testScope.runTest {
            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items

            val shortcutInfo = shortcuts.first { it.label == "Browser" }

            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(BROWSER_PACKAGE)
            assertThat(shortcutInfo.icon!!.resId).isEqualTo(BROWSER_ICON_RES_ID)
        }

    @Test
    fun shortcutGroups_hasContactsIcon() =
        testScope.runTest {
            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items

            val shortcutInfo = shortcuts.first { it.label == "Contacts" }

            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(CONTACTS_PACKAGE)
            assertThat(shortcutInfo.icon!!.resId).isEqualTo(CONTACTS_ICON_RES_ID)
        }

    @Test
    fun shortcutGroups_hasEmailIcon() =
        testScope.runTest {
            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items

            val shortcutInfo = shortcuts.first { it.label == "Email" }

            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(EMAIL_PACKAGE)
            assertThat(shortcutInfo.icon!!.resId).isEqualTo(EMAIL_ICON_RES_ID)
        }

    @Test
    fun shortcutGroups_hasCalendarIcon() =
        testScope.runTest {
            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items

            val shortcutInfo = shortcuts.first { it.label == "Calendar" }

            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(CALENDAR_PACKAGE)
            assertThat(shortcutInfo.icon!!.resId).isEqualTo(CALENDAR_ICON_RES_ID)
        }

    @Test
    fun shortcutGroups_hasMapsIcon() =
        testScope.runTest {
            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items

            val shortcutInfo = shortcuts.first { it.label == "Maps" }

            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(MAPS_PACKAGE)
            assertThat(shortcutInfo.icon!!.resId).isEqualTo(MAPS_ICON_RES_ID)
        whenever(mockWindowManager.getApplicationLaunchKeyboardShortcuts(TEST_DEVICE_ID))
            .thenAnswer { appCategoriesGroup }
    }

    @Test
    fun shortcutGroups_hasMessagingIcon() =
    fun shortcutGroups_nullResult_returnsEmptyList() =
        testScope.runTest {
            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items

            val shortcutInfo = shortcuts.first { it.label == "SMS" }
            appCategoriesGroup = null

            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(MESSAGING_PACKAGE)
            assertThat(shortcutInfo.icon!!.resId).isEqualTo(MESSAGING_ICON_RES_ID)
            assertThat(source.shortcutGroups(TEST_DEVICE_ID)).isEmpty()
        }

    @Test
    fun shortcutGroups_hasMusicIcon() =
    fun shortcutGroups_returnsSortedList() =
        testScope.runTest {
            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items

            val shortcutInfo = shortcuts.first { it.label == "Music" }

            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(MUSIC_PACKAGE)
            assertThat(shortcutInfo.icon!!.resId).isEqualTo(MUSIC_ICON_RES_ID)
        }

    @Test
    fun shortcutGroups_hasCalculatorIcon() =
        testScope.runTest {
            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items

            val shortcutInfo = shortcuts.first { it.label == "Calculator" }

            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(CALCULATOR_PACKAGE)
            assertThat(shortcutInfo.icon!!.resId).isEqualTo(CALCULATOR_ICON_RES_ID)
        }
            val testItems =
                listOf(
                    KeyboardShortcutInfo("Info 2", KeyEvent.KEYCODE_E, KeyEvent.META_META_ON),
                    KeyboardShortcutInfo("Info 1", KeyEvent.KEYCODE_E, KeyEvent.META_META_ON),
                    KeyboardShortcutInfo("Info 3", KeyEvent.KEYCODE_E, KeyEvent.META_META_ON),
                )
            appCategoriesGroup = KeyboardShortcutGroup("Test Group", testItems)

    @Test
    fun shortcutGroups_shortcutsSortedByLabelIgnoringCase() =
        testScope.runTest {
            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items

            val shortcutLabels = shortcuts.map { it.label!!.toString() }
            assertThat(shortcutLabels).isEqualTo(shortcutLabels.sortedBy { it.lowercase() })
            val shortcutLabels = shortcuts.map { it.label.toString() }
            assertThat(shortcutLabels).containsExactly("Info 1", "Info 2", "Info 3").inOrder()
        }

    @Test
    fun shortcutGroups_noAssistantApp_excludesAssistantFromShortcuts() =
        testScope.runTest {
            val shortcutLabels =
                source.shortcutGroups(TEST_DEVICE_ID).first().items.map { it.label!!.toString() }

            assertThat(shortcutLabels).doesNotContain("Assistant")
        }

    private companion object {
        private const val ASSISTANT_PACKAGE = "the.assistant.app"
        private const val ASSISTANT_ICON_RES_ID = 123

        private const val BROWSER_PACKAGE = "com.test.browser"
        private const val BROWSER_ICON_RES_ID = 1

        private const val CONTACTS_PACKAGE = "app.test.contacts"
        private const val CONTACTS_ICON_RES_ID = 234

        private const val EMAIL_PACKAGE = "email.app.test"
        private const val EMAIL_ICON_RES_ID = 351

        private const val CALENDAR_PACKAGE = "app.test.calendar"
        private const val CALENDAR_ICON_RES_ID = 411

        private const val MAPS_PACKAGE = "maps.app.package"
        private const val MAPS_ICON_RES_ID = 999

        private const val MUSIC_PACKAGE = "com.android.music"
        private const val MUSIC_ICON_RES_ID = 101

        private const val MESSAGING_PACKAGE = "my.sms.app"
        private const val MESSAGING_ICON_RES_ID = 9191

        private const val CALCULATOR_PACKAGE = "that.calculator.app"
        private const val CALCULATOR_ICON_RES_ID = 314

        private val categoryApps =
            listOf(
                CategoryApp(CATEGORY_APP_BROWSER, BROWSER_PACKAGE, BROWSER_ICON_RES_ID),
                CategoryApp(CATEGORY_APP_CONTACTS, CONTACTS_PACKAGE, CONTACTS_ICON_RES_ID),
                CategoryApp(CATEGORY_APP_EMAIL, EMAIL_PACKAGE, EMAIL_ICON_RES_ID),
                CategoryApp(CATEGORY_APP_CALENDAR, CALENDAR_PACKAGE, CALENDAR_ICON_RES_ID),
                CategoryApp(CATEGORY_APP_MAPS, MAPS_PACKAGE, MAPS_ICON_RES_ID),
                CategoryApp(CATEGORY_APP_MUSIC, MUSIC_PACKAGE, MUSIC_ICON_RES_ID),
                CategoryApp(CATEGORY_APP_MESSAGING, MESSAGING_PACKAGE, MESSAGING_ICON_RES_ID),
                CategoryApp(CATEGORY_APP_CALCULATOR, CALCULATOR_PACKAGE, CALCULATOR_ICON_RES_ID),
            )

    companion object {
        private const val TEST_DEVICE_ID = 123
    }

    private class CategoryApp(val category: String, val packageName: String, val iconResId: Int)
}
+3 −1
Original line number Diff line number Diff line
@@ -19,4 +19,6 @@ package android.view
import com.android.systemui.kosmos.Kosmos
import org.mockito.Mockito.mock

val Kosmos.windowManager by Kosmos.Fixture<WindowManager> { mock(WindowManager::class.java) }
val Kosmos.mockWindowManager: WindowManager by Kosmos.Fixture { mock(WindowManager::class.java) }

var Kosmos.windowManager: WindowManager by Kosmos.Fixture { mockWindowManager }
Loading