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

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

Merge "Added data layer for retrieval of custom shortcuts" into main

parents 38404c43 d1fc3b35
Loading
Loading
Loading
Loading
+291 −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.content.Context
import android.content.Context.INPUT_SERVICE
import android.hardware.input.InputGestureData
import android.hardware.input.InputGestureData.createKeyTrigger
import android.hardware.input.KeyGestureEvent
import android.hardware.input.fakeInputManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.view.KeyEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.hardware.input.Flags.FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES
import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyboard.shortcut.customShortcutCategoriesRepository
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.AppCategories
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.ShortcutCommand
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
import com.android.systemui.kosmos.testScope
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.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

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

    private val mockUserContext: Context = mock()
    private val kosmos =
        testKosmos().also {
            it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
        }

    private val fakeInputManager = kosmos.fakeInputManager
    private val testScope = kosmos.testScope
    private val helper = kosmos.shortcutHelperTestHelper
    private val repo = kosmos.customShortcutCategoriesRepository

    @Before
    fun setup() {
        whenever(mockUserContext.getSystemService(INPUT_SERVICE))
            .thenReturn(fakeInputManager.inputManager)
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    fun categories_emitsCorrectlyConvertedShortcutCategories() {
        testScope.runTest {
            whenever(
                    fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
                )
                .thenReturn(customizableInputGesturesWithSimpleShortcutCombinations)

            helper.toggle(deviceId = 123)
            val categories by collectLastValue(repo.categories)

            assertThat(categories)
                .containsExactlyElementsIn(expectedShortcutCategoriesWithSimpleShortcutCombination)
        }
    }

    @Test
    @DisableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    fun categories_emitsEmptyListWhenFlagIsDisabled() {
        testScope.runTest {
            whenever(
                    fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
                )
                .thenReturn(customizableInputGesturesWithSimpleShortcutCombinations)

            helper.toggle(deviceId = 123)
            val categories by collectLastValue(repo.categories)

            assertThat(categories).isEmpty()
        }
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    fun categories_ignoresUnknownKeyGestureTypes() {
        testScope.runTest {
            whenever(
                    fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
                )
                .thenReturn(customizableInputGestureWithUnknownKeyGestureType)

            helper.toggle(deviceId = 123)
            val categories by collectLastValue(repo.categories)

            assertThat(categories).isEmpty()
        }
    }

    private fun simpleInputGestureData(
        keyCode: Int = KeyEvent.KEYCODE_A,
        modifiers: Int = KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
        keyGestureType: Int,
    ): InputGestureData {
        val builder = InputGestureData.Builder()
        builder.setKeyGestureType(keyGestureType)
        builder.setTrigger(createKeyTrigger(keyCode, modifiers))
        return builder.build()
    }

    private fun simpleShortcutCategory(
        category: ShortcutCategoryType,
        subcategoryLabel: String,
        shortcutLabel: String,
    ): ShortcutCategory {
        return ShortcutCategory(
            type = category,
            subCategories =
                listOf(
                    ShortcutSubCategory(
                        label = subcategoryLabel,
                        shortcuts = listOf(simpleShortcut(shortcutLabel)),
                    )
                ),
        )
    }

    private fun simpleShortcut(label: String) =
        Shortcut(
            label = label,
            commands =
                listOf(
                    ShortcutCommand(
                        isCustom = true,
                        keys =
                            listOf(
                                ShortcutKey.Text("Ctrl"),
                                ShortcutKey.Text("Alt"),
                                ShortcutKey.Text("A"),
                            ),
                    )
                ),
        )

    private val customizableInputGestureWithUnknownKeyGestureType =
        // These key gesture events are currently not supported by shortcut helper customizer
        listOf(
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS
            ),
            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_MEDIA_KEY),
        )

    private val expectedShortcutCategoriesWithSimpleShortcutCombination =
        listOf(
            simpleShortcutCategory(System, "System apps", "Open assistant"),
            simpleShortcutCategory(System, "System controls", "Go to home screen"),
            simpleShortcutCategory(System, "System apps", "Open settings"),
            simpleShortcutCategory(System, "System controls", "Lock screen"),
            simpleShortcutCategory(System, "System controls", "View notifications"),
            simpleShortcutCategory(System, "System apps", "Take a note"),
            simpleShortcutCategory(System, "System controls", "Take screenshot"),
            simpleShortcutCategory(System, "System controls", "Go back"),
            simpleShortcutCategory(
                MultiTasking,
                "Split screen",
                "Switch from split screen to full screen",
            ),
            simpleShortcutCategory(
                MultiTasking,
                "Split screen",
                "Use split screen with current app on the left",
            ),
            simpleShortcutCategory(
                MultiTasking,
                "Split screen",
                "Switch to app on left or above while using split screen",
            ),
            simpleShortcutCategory(
                MultiTasking,
                "Split screen",
                "Use split screen with current app on the right",
            ),
            simpleShortcutCategory(
                MultiTasking,
                "Split screen",
                "Switch to app on right or below while using split screen",
            ),
            simpleShortcutCategory(System, "System controls", "Show shortcuts"),
            simpleShortcutCategory(System, "System controls", "View recent apps"),
            simpleShortcutCategory(AppCategories, "Applications", "Calculator"),
            simpleShortcutCategory(AppCategories, "Applications", "Calendar"),
            simpleShortcutCategory(AppCategories, "Applications", "Chrome"),
            simpleShortcutCategory(AppCategories, "Applications", "Contacts"),
            simpleShortcutCategory(AppCategories, "Applications", "Gmail"),
            simpleShortcutCategory(AppCategories, "Applications", "Maps"),
            simpleShortcutCategory(AppCategories, "Applications", "Messages"),
            simpleShortcutCategory(MultiTasking, "Recent apps", "Cycle forward through recent apps"),
        )

    private val customizableInputGesturesWithSimpleShortcutCombinations =
        listOf(
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT
            ),
            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS
            ),
            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL
            ),
            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT
            ),
            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION
            ),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT
            ),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT
            ),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT
            ),
            simpleInputGestureData(
                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT
            ),
            simpleInputGestureData(
                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
            ),
        )
}
+3 −3
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
import com.android.systemui.keyboard.shortcut.defaultShortcutCategoriesRepository
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
@@ -47,7 +48,6 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesRepository
import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
@@ -71,7 +71,7 @@ import org.mockito.kotlin.whenever
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() {
class DefaultShortcutCategoriesRepositoryTest : SysuiTestCase() {

    private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
    private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
@@ -87,7 +87,7 @@ class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() {
            it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
        }

    private val repo = kosmos.shortcutHelperCategoriesRepository
    private val repo = kosmos.defaultShortcutCategoriesRepository
    private val helper = kosmos.shortcutHelperTestHelper
    private val testScope = kosmos.testScope
    private val fakeInputManager = kosmos.fakeInputManager
+57 −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.model

import android.graphics.drawable.Icon
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

/**
 * Internal Keyboard Shortcut models to use with [ShortcutCategoriesUtils.fetchShortcutCategory]
 * when converting API models to Shortcut Helper Model [ShortcutCategory]. These Internal Models
 * bridge the Gap between [InputGestureData] from custom shortcuts API and [KeyboardShortcutGroup]
 * from default shortcuts API allowing us to have a single Utility Class that converts API models to
 * Shortcut Helper models
 *
 * @param label Equivalent to shortcut helper's subcategory label
 * @param items Keyboard Shortcuts received from API
 * @param packageName package name of current app shortcut if available.
 */
data class InternalKeyboardShortcutGroup(
    val label: String,
    val items: List<InternalKeyboardShortcutInfo>,
    val packageName: String? = null,
)

/**
 * @param label Shortcut label
 * @param keycode Key to trigger shortcut
 * @param modifiers Mask of shortcut modifiers
 * @param baseCharacter Key to trigger shortcut if is a character
 * @param icon Shortcut icon if available - often used for app launch shortcuts
 * @param isCustomShortcut If Shortcut is user customized or system defined.
 */
data class InternalKeyboardShortcutInfo(
    val label: String,
    val keycode: Int,
    val modifiers: Int,
    val baseCharacter: Char = Char.MIN_VALUE,
    val icon: Icon? = null,
    val isCustomShortcut: Boolean = false,
)
+175 −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.content.Context
import android.content.Context.INPUT_SERVICE
import android.hardware.input.InputGestureData
import android.hardware.input.InputGestureData.KeyTrigger
import android.hardware.input.InputManager
import android.hardware.input.InputSettings
import android.hardware.input.KeyGestureEvent
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
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.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
import com.android.systemui.settings.UserTracker
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext

@SysUISingleton
class CustomShortcutCategoriesRepository
@Inject
constructor(
    stateRepository: ShortcutHelperStateRepository,
    private val userTracker: UserTracker,
    @Background private val backgroundScope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val shortcutCategoriesUtils: ShortcutCategoriesUtils,
) : ShortcutCategoriesRepository {

    private val userContext: Context
        get() = userTracker.createCurrentUserContext(userTracker.userContext)

    // Input manager created with user context to provide correct user id when requesting custom
    // shortcut
    private val inputManager: InputManager
        get() = userContext.getSystemService(INPUT_SERVICE) as InputManager

    private val activeInputDevice =
        stateRepository.state.map {
            if (it is Active) {
                withContext(backgroundDispatcher) { inputManager.getInputDevice(it.deviceId) }
            } else {
                null
            }
        }

    override val categories: Flow<List<ShortcutCategory>> =
        activeInputDevice
            .map { inputDevice ->
                if (inputDevice == null) {
                    emptyList()
                } else {
                    val customInputGesturesForUser: List<InputGestureData> =
                        if (InputSettings.isCustomizableInputGesturesFeatureFlagEnabled()) {
                            inputManager.getCustomInputGestures(/* filter= */ null)
                        } else emptyList()
                    val sources = toInternalGroupSources(customInputGesturesForUser)
                    val supportedKeyCodes =
                        shortcutCategoriesUtils.fetchSupportedKeyCodes(
                            inputDevice.id,
                            sources.map { it.groups },
                        )

                    val result =
                        sources.mapNotNull { source ->
                            shortcutCategoriesUtils.fetchShortcutCategory(
                                type = source.type,
                                groups = source.groups,
                                inputDevice = inputDevice,
                                supportedKeyCodes = supportedKeyCodes,
                            )
                        }
                    result
                }
            }
            .stateIn(
                scope = backgroundScope,
                initialValue = emptyList(),
                started = SharingStarted.Lazily,
            )

    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(
        @KeyGestureEvent.KeyGestureType keyGestureType: Int
    ): String? {
        return InputGestures.gestureToInternalKeyboardShortcutGroupLabelMap.getOrDefault(
            keyGestureType,
            null,
        )
    }

    private fun fetchShortcutInfoLabelByGestureType(
        @KeyGestureEvent.KeyGestureType keyGestureType: Int
    ): String? {
        return InputGestures.gestureToInternalKeyboardShortcutInfoLabelMap.getOrDefault(
            keyGestureType,
            null,
        )
    }

    private fun fetchShortcutCategoryTypeByGestureType(
        @KeyGestureEvent.KeyGestureType keyGestureType: Int
    ): ShortcutCategoryType? {
        return InputGestures.gestureToShortcutCategoryTypeMap.getOrDefault(keyGestureType, null)
    }

    private data class InternalGroupsSource(
        val groups: List<InternalKeyboardShortcutGroup>,
        val type: ShortcutCategoryType,
    )
}
+160 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading