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

Commit 9308ef59 authored by Josh's avatar Josh
Browse files

Added repository to expose app shortcuts for user visible apps

This repository takes all user visible apps on the device for the
current user and creates a Shortcut category for these apps. This
shortcut category is supplied to Shortcut helper categories interactor
and flows downstream to shortcut helper to be displayed when in
customization mode;

Flag: com.android.systemui.extended_apps_shortcut_category
Test: AppsShortcutCategoryRepositoryTest,
ShortcuthelperCategoriesInteractorTest
Fix: 403243867

Change-Id: Ibac3af87671bf0aca2d40ee5d39f18986c91c595
parent 0b85e498
Loading
Loading
Loading
Loading
+156 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.content.pm.UserInfo
import android.os.UserHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyboard.shortcut.appsShortcutCategoryRepository
import com.android.systemui.keyboard.shortcut.fakeLauncherApps
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
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.ShortcutIcon
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.res.R
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

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

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()

    private val repo = kosmos.appsShortcutCategoryRepository
    private val fakeLauncherApps = kosmos.fakeLauncherApps
    private val userTracker = kosmos.fakeUserTracker
    private val testScope = kosmos.testScope

    @Before
    fun setup() {
        userTracker.set(
            userInfos =
                listOf(
                    UserInfo(/* id= */ PRIMARY_USER_ID, /* name= */ "Primary User", /* flags= */ 0)
                ),
            selectedUserIndex = PRIMARY_USER_INDEX,
        )

        fakeLauncherApps.installPackageForUser(
            TEST_PACKAGE_1,
            TEST_CLASS_1,
            UserHandle(/* userId= */ PRIMARY_USER_ID),
            ICON_RES_ID_1,
            TEST_PACKAGE_LABEL_1,
        )

        fakeLauncherApps.installPackageForUser(
            TEST_PACKAGE_2,
            TEST_CLASS_2,
            UserHandle(/* userId= */ PRIMARY_USER_ID),
            ICON_RES_ID_2,
            TEST_PACKAGE_LABEL_2,
        )
    }

    @Test
    fun categories_emitsCorrectShortcutCategoryWithAllInstalledApps() {
        testScope.runTest {
            val categories by collectLastValue(repo.categories)

            assertThat(categories).containsExactly(expectedShortcutCategoryWithBothAppShortcuts)
        }
    }

    @Test
    fun categories_emitsEmptyListWhenAlUserVisibleAppsAreUninstalled() {
        testScope.runTest {
            val categories by collectLastValue(repo.categories)

            fakeLauncherApps.uninstallPackageForUser(
                TEST_PACKAGE_1,
                TEST_CLASS_1,
                UserHandle(/* userId= */ PRIMARY_USER_ID),
            )

            fakeLauncherApps.uninstallPackageForUser(
                TEST_PACKAGE_2,
                TEST_CLASS_2,
                UserHandle(/* userId= */ PRIMARY_USER_ID),
            )

            assertThat(categories).isEmpty()
        }
    }

    private val expectedShortcutCategoryWithBothAppShortcuts =
        ShortcutCategory(
            ShortcutCategoryType.AppCategories,
            ShortcutSubCategory(
                label = context.getString(R.string.keyboard_shortcut_group_applications),
                shortcuts =
                    listOf(
                        Shortcut(
                            label = TEST_PACKAGE_LABEL_1,
                            commands = emptyList(),
                            icon =
                                ShortcutIcon(
                                    packageName = TEST_PACKAGE_1,
                                    resourceId = ICON_RES_ID_1,
                                ),
                            pkgName = TEST_PACKAGE_1,
                            className = TEST_CLASS_1,
                        ),
                        Shortcut(
                            label = TEST_PACKAGE_LABEL_2,
                            commands = emptyList(),
                            icon =
                                ShortcutIcon(
                                    packageName = TEST_PACKAGE_2,
                                    resourceId = ICON_RES_ID_2,
                                ),
                            pkgName = TEST_PACKAGE_2,
                            className = TEST_CLASS_2,
                        ),
                    ),
            ),
        )

    private companion object {
        const val TEST_PACKAGE_1 = "test.package.one"
        const val TEST_PACKAGE_2 = "test.package.two"
        const val TEST_CLASS_1 = "TestClassOne"
        const val TEST_CLASS_2 = "TestClassTwo"
        const val PRIMARY_USER_ID = 10
        const val PRIMARY_USER_INDEX = 0
        const val ICON_RES_ID_1 = 1
        const val ICON_RES_ID_2 = 2
        const val TEST_PACKAGE_LABEL_1 = "ApplicationOne"
        const val TEST_PACKAGE_LABEL_2 = "ApplicationTwo"
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -145,7 +145,7 @@ class UserVisibleAppsRepositoryTest : SysuiTestCase() {
        )
    }

    companion object {
    private companion object {
        const val TEST_PACKAGE_1 = "test.package.one"
        const val TEST_PACKAGE_2 = "test.package.two"
        const val TEST_CLASS_1 = "TestClassOne"
+72 −5
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import androidx.test.filters.SmallTest
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyboard.shortcut.appsShortcutCategoryRepository
import com.android.systemui.keyboard.shortcut.data.repository.FakeAppsShortcutCategoryRepository
import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allCustomizableInputGesturesWithSimpleShortcutCombinations
@@ -34,24 +36,28 @@ import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.customIn
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.groupWithGoHomeShortcutInfo
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.systemCategoryWithCustomHomeShortcut
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.systemCategoryWithMergedGoHomeShortcut
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
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.ShortcutCategoryType.InputMethodEditor
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.keyboard.shortcut.shortcutHelperAccessibilityShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesInteractor
import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperCustomizationModeInteractor
import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
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 kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -69,8 +75,8 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() {
    private val multitaskingShortcutsSource = FakeKeyboardShortcutGroupsSource()

    private val kosmos =
        testKosmos().also {
            it.testDispatcher = UnconfinedTestDispatcher()
        testKosmos().useUnconfinedTestDispatcher().also {
            it.appsShortcutCategoryRepository = FakeAppsShortcutCategoryRepository()
            it.shortcutHelperSystemShortcutsSource = systemShortcutsSource
            it.shortcutHelperMultiTaskingShortcutsSource = multitaskingShortcutsSource
            it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
@@ -83,7 +89,9 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() {
    private val testScope = kosmos.testScope
    private lateinit var interactor: ShortcutHelperCategoriesInteractor
    private val helper = kosmos.shortcutHelperTestHelper
    private val inter by lazy { kosmos.shortcutHelperCategoriesInteractor }
    private val customizationModeInteractor = kosmos.shortcutHelperCustomizationModeInteractor
    private val fakeAppsShortcutCategoryRepo: FakeAppsShortcutCategoryRepository =
        kosmos.appsShortcutCategoryRepository as FakeAppsShortcutCategoryRepository

    @Before
    fun setShortcuts() {
@@ -383,8 +391,67 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() {
            assertThat(shortcutKeyCount).containsExactly(1, 2, 3).inOrder()
        }

    @Test
    @DisableFlags(Flags.FLAG_EXTENDED_APPS_SHORTCUT_CATEGORY)
    fun categories_extendedAppsCategoryFlagOff_allCustomizationModes_doesNotEmit3PAppsShortcut() =
        testScope.runTest {
            setExtendedAppsShortcutCategory()
            val categories by collectLastValue(interactor.shortcutCategories)
            helper.showFromActivity()

            customizationModeInteractor.toggleCustomizationMode(isCustomizing = false)
            assertThat(categories).doesNotContain(TestAppsShortcutCategory)

            customizationModeInteractor.toggleCustomizationMode(isCustomizing = true)
            assertThat(categories).doesNotContain(TestAppsShortcutCategory)
        }

    @Test
    @EnableFlags(Flags.FLAG_EXTENDED_APPS_SHORTCUT_CATEGORY)
    fun categories_extendedAppsCategoryFlagOn_customizationOn_emitsExtendedAppShortcutCategory() =
        testScope.runTest {
            setExtendedAppsShortcutCategory()
            val categories by collectLastValue(interactor.shortcutCategories)
            helper.showFromActivity()

            customizationModeInteractor.toggleCustomizationMode(isCustomizing = true)
            assertThat(categories).contains(TestAppsShortcutCategory)
        }

    @Test
    @EnableFlags(Flags.FLAG_EXTENDED_APPS_SHORTCUT_CATEGORY)
    fun categories_extendedAppsCategoryFlagOn_customizationOff_doesNotEmitExtendedAppShortcutCategory() =
        testScope.runTest {
            setExtendedAppsShortcutCategory()
            val categories by collectLastValue(interactor.shortcutCategories)
            helper.showFromActivity()

            customizationModeInteractor.toggleCustomizationMode(isCustomizing = false)
            assertThat(categories).doesNotContain(TestAppsShortcutCategory)
        }

    private fun setExtendedAppsShortcutCategory() {
        fakeAppsShortcutCategoryRepo.setFakeAppsShortcutCategories(listOf(TestAppsShortcutCategory))
    }

    private fun setCustomInputGestures(customInputGestures: List<InputGestureData>) {
        whenever(fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull()))
            .thenReturn(customInputGestures)
    }

    private val TestAppsShortcutCategory =
        ShortcutCategory(
            type = ShortcutCategoryType.AppCategories,
            ShortcutSubCategory(
                label = context.getString(R.string.keyboard_shortcut_group_applications),
                shortcuts =
                    listOf(
                        Shortcut(
                            label = "TestApp",
                            commands = emptyList(),
                            contentDescription = "TestApp, Press key",
                        )
                    ),
            ),
        )
}
+8 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.keyboard.shortcut

import com.android.systemui.CoreStartable
import com.android.systemui.Flags.keyboardShortcutHelperRewrite
import com.android.systemui.keyboard.shortcut.data.repository.AppsShortcutCategoryRepository
import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository
import com.android.systemui.keyboard.shortcut.data.repository.DefaultShortcutCategoriesRepository
import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesRepository
@@ -30,6 +31,7 @@ import com.android.systemui.keyboard.shortcut.data.source.MultitaskingShortcutsS
import com.android.systemui.keyboard.shortcut.data.source.SystemShortcutsSource
import com.android.systemui.keyboard.shortcut.qualifiers.AccessibilityShortcuts
import com.android.systemui.keyboard.shortcut.qualifiers.AppCategoriesShortcuts
import com.android.systemui.keyboard.shortcut.qualifiers.AppsShortcutCategories
import com.android.systemui.keyboard.shortcut.qualifiers.CurrentAppShortcuts
import com.android.systemui.keyboard.shortcut.qualifiers.CustomShortcutCategories
import com.android.systemui.keyboard.shortcut.qualifiers.DefaultShortcutCategories
@@ -87,6 +89,12 @@ interface ShortcutHelperModule {
        impl: CustomShortcutCategoriesRepository
    ): ShortcutCategoriesRepository

    @Binds
    @AppsShortcutCategories
    fun appsShortcutCategoriesRepository(
        impl: AppsShortcutCategoryRepository
    ): ShortcutCategoriesRepository

    companion object {
        @Provides
        @IntoMap
+73 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.content.Context
import android.content.pm.LauncherActivityInfo
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
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.ShortcutIcon
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.res.R
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

@SysUISingleton
class AppsShortcutCategoryRepository
@Inject
constructor(userVisibleAppsRepository: UserVisibleAppsRepository, context: Context) :
    ShortcutCategoriesRepository {

    override val categories: Flow<List<ShortcutCategory>> =
        userVisibleAppsRepository.userVisibleApps.map { userVisibleApps ->
            if (userVisibleApps.isEmpty()) {
                emptyList()
            } else {
                listOf(
                    ShortcutCategory(
                        ShortcutCategoryType.AppCategories,
                        ShortcutSubCategory(
                            label =
                                context.getString(R.string.keyboard_shortcut_group_applications),
                            shortcuts = convertLauncherActivityInfoToShortcutModel(userVisibleApps),
                        ),
                    )
                )
            }
        }

    private fun convertLauncherActivityInfoToShortcutModel(
        activityInfos: List<LauncherActivityInfo>
    ): List<Shortcut> {
        return activityInfos.map { activityInfo ->
            Shortcut(
                label = activityInfo.label.toString(),
                commands = emptyList(),
                icon =
                    ShortcutIcon(
                        packageName = activityInfo.applicationInfo.packageName,
                        resourceId = activityInfo.applicationInfo.iconRes,
                    ),
                pkgName = activityInfo.componentName.packageName,
                className = activityInfo.componentName.className,
            )
        }
    }
}
Loading