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

Commit d4bdf695 authored by Chris Göllner's avatar Chris Göllner Committed by Chris Göllner
Browse files

Shortcut Helper - Only start Activity after shortcuts have been fetched

+ Only starts activity when we have fetched all shortcuts. This is
  because for the "current app" shortcuts, WindowManager returns
  the shortcuts for the currently focused app. We want the shortcuts
  of the focused app, before shortcut helper started. If we fetch
  shortcuts after shortcut helper started, WindowManager will return
  shortcuts of the shortcut helper instead of the app.
+ Changes repository to return a list of all shortcut categories instead
  of returning each category individually. This ensures we can ensure
  processing only continues when all have been fetched
+ Change categories flow to be a StateFlow, to make sure an existing
  value is replayed to new subscribers. This ensures we don't fetch
  new shortcuts after the helper has been shown

Flag: com.android.systemui.keyboard_shortcut_helper_rewrite
Test: Unit tests in this CL
Test: Manually
Fixes: 352500619
Change-Id: Id32a4248a732c26399539fd9c9dd66717d0acec4
parent 3368b44b
Loading
Loading
Loading
Loading
+73 −77
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.content.Context
import android.graphics.drawable.Icon
import android.hardware.input.InputManager
import android.util.Log
import android.view.InputDevice
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.KeyboardShortcutGroup
@@ -47,8 +48,11 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
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
@@ -56,6 +60,7 @@ class ShortcutHelperCategoriesRepository
@Inject
constructor(
    private val context: Context,
    @Background private val backgroundScope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    @SystemShortcuts private val systemShortcutsSource: KeyboardShortcutGroupsSource,
    @MultitaskingShortcuts private val multitaskingShortcutsSource: KeyboardShortcutGroupsSource,
@@ -75,79 +80,80 @@ constructor(
            }
        }

    val systemShortcutsCategory =
        activeInputDevice.map {
            if (it != null) {
    val categories: Flow<List<ShortcutCategory>> =
        activeInputDevice
            .map {
                if (it == null) {
                    return@map emptyList()
                }
                return@map listOfNotNull(
                    fetchSystemShortcuts(it),
                    fetchMultiTaskingShortcuts(it),
                    fetchAppCategoriesShortcuts(it),
                    fetchImeShortcuts(it),
                    fetchCurrentAppShortcuts(it),
                )
            }
            .stateIn(
                scope = backgroundScope,
                started = SharingStarted.Lazily,
                initialValue = emptyList(),
            )

    private suspend fun fetchSystemShortcuts(inputDevice: InputDevice) =
        toShortcutCategory(
                    it.keyCharacterMap,
            inputDevice.keyCharacterMap,
            System,
                    systemShortcutsSource.shortcutGroups(it.id),
            systemShortcutsSource.shortcutGroups(inputDevice.id),
            keepIcons = true,
        )
            } else {
                null
            }
        }

    val multitaskingShortcutsCategory =
        activeInputDevice.map {
            if (it != null) {
    private suspend fun fetchMultiTaskingShortcuts(inputDevice: InputDevice) =
        toShortcutCategory(
                    it.keyCharacterMap,
            inputDevice.keyCharacterMap,
            MultiTasking,
                    multitaskingShortcutsSource.shortcutGroups(it.id),
            multitaskingShortcutsSource.shortcutGroups(inputDevice.id),
            keepIcons = true,
        )
            } else {
                null
            }
        }

    val appCategoriesShortcutsCategory =
        activeInputDevice.map {
            if (it != null) {
    private suspend fun fetchAppCategoriesShortcuts(inputDevice: InputDevice) =
        toShortcutCategory(
                    it.keyCharacterMap,
            inputDevice.keyCharacterMap,
            AppCategories,
                    appCategoriesShortcutsSource.shortcutGroups(it.id),
            appCategoriesShortcutsSource.shortcutGroups(inputDevice.id),
            keepIcons = true,
        )
            } else {
                null
            }
        }

    val imeShortcutsCategory =
        activeInputDevice.map {
            if (it != null) {
    private suspend fun fetchImeShortcuts(inputDevice: InputDevice) =
        toShortcutCategory(
                    it.keyCharacterMap,
            inputDevice.keyCharacterMap,
            InputMethodEditor,
                    inputShortcutsSource.shortcutGroups(it.id),
            inputShortcutsSource.shortcutGroups(inputDevice.id),
            keepIcons = false,
        )
            } else {
                null
            }
        }

    val currentAppShortcutsCategory: Flow<ShortcutCategory?> =
        activeInputDevice.map {
            if (it != null) {
                val shortcutGroups = currentAppShortcutsSource.shortcutGroups(it.id)
    private suspend fun fetchCurrentAppShortcuts(inputDevice: InputDevice): ShortcutCategory? {
        val shortcutGroups = currentAppShortcutsSource.shortcutGroups(inputDevice.id)
        val categoryType = getCurrentAppShortcutCategoryType(shortcutGroups)
                if (categoryType == null) {
        return if (categoryType == null) {
            null
        } else {
            toShortcutCategory(
                        it.keyCharacterMap,
                inputDevice.keyCharacterMap,
                categoryType,
                shortcutGroups,
                keepIcons = false
            )
        }
            } else {
    }

    private fun getCurrentAppShortcutCategoryType(
        shortcutGroups: List<KeyboardShortcutGroup>
    ): ShortcutCategoryType? {
        return if (shortcutGroups.isEmpty()) {
            null
        } else {
            CurrentApp(packageName = shortcutGroups[0].packageName.toString())
        }
    }

@@ -174,16 +180,6 @@ constructor(
        }
    }

    private fun getCurrentAppShortcutCategoryType(
        shortcutGroups: List<KeyboardShortcutGroup>
    ): ShortcutCategoryType? {
        return if (shortcutGroups.isEmpty()) {
            null
        } else {
            CurrentApp(packageName = shortcutGroups[0].packageName.toString())
        }
    }

    private fun toShortcuts(
        keyCharacterMap: KeyCharacterMap,
        infoList: List<KeyboardShortcutInfo>,
+3 −9
Original line number Diff line number Diff line
@@ -23,7 +23,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map

@SysUISingleton
class ShortcutHelperCategoriesInteractor
@@ -33,14 +33,8 @@ constructor(
) {

    val shortcutCategories: Flow<List<ShortcutCategory>> =
        combine(
            categoriesRepository.systemShortcutsCategory,
            categoriesRepository.multitaskingShortcutsCategory,
            categoriesRepository.imeShortcutsCategory,
            categoriesRepository.appCategoriesShortcutsCategory,
            categoriesRepository.currentAppShortcutsCategory
        ) { shortcutCategories ->
            shortcutCategories.filterNotNull().map { groupSubCategoriesInCategory(it) }
        categoriesRepository.categories.map { categories ->
            categories.map { category -> groupSubCategoriesInCategory(category) }
        }

    private fun groupSubCategoriesInCategory(shortcutCategory: ShortcutCategory): ShortcutCategory {
+2 −3
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.systemui.keyboard.shortcut.ui.viewmodel
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor
import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -40,8 +39,8 @@ constructor(
) {

    val shouldShow =
        stateInteractor.state
            .map { it is ShortcutHelperState.Active }
        categoriesInteractor.shortcutCategories
            .map { it.isNotEmpty() }
            .distinctUntilChanged()
            .flowOn(backgroundDispatcher)

+90 −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 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.data.source.FakeKeyboardShortcutGroupsSource
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
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
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.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() {

    private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
    private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()

    private val kosmos =
        testKosmos().also {
            it.testDispatcher = UnconfinedTestDispatcher()
            it.shortcutHelperSystemShortcutsSource = fakeSystemSource
            it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource
            it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
            it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource()
            it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
        }

    private val repo = kosmos.shortcutHelperCategoriesRepository
    private val helper = kosmos.shortcutHelperTestHelper
    private val testScope = kosmos.testScope

    @Before
    fun setUp() {
        fakeSystemSource.setGroups(TestShortcuts.systemGroups)
        fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
    }

    @Test
    fun categories_multipleSubscribers_replaysExistingValueToNewSubscribers() =
        testScope.runTest {
            fakeSystemSource.setGroups(TestShortcuts.systemGroups)
            fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
            helper.showFromActivity()
            val firstCategories by collectLastValue(repo.categories)

            // Intentionally change shortcuts now. This simulates "current app" shortcuts changing
            // when our helper is shown.
            // We still want to return the shortcuts that were returned before our helper was
            // showing.
            fakeSystemSource.setGroups(emptyList())

            val secondCategories by collectLastValue(repo.categories)
            // Make sure the second subscriber receives the same value as the first subscriber, even
            // though fetching shortcuts again would have returned a new result.
            assertThat(secondCategories).isEqualTo(firstCategories)
        }
}
+3 −2
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
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.shortcutHelperMultiTaskingShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
@@ -48,14 +49,14 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() {

    private val systemShortcutsSource = FakeKeyboardShortcutGroupsSource()
    private val multitaskingShortcutsSource = FakeKeyboardShortcutGroupsSource()
    private val defaultAppsShortcutsSource = FakeKeyboardShortcutGroupsSource()
    @OptIn(ExperimentalCoroutinesApi::class)
    private val kosmos =
        testKosmos().also {
            it.testDispatcher = UnconfinedTestDispatcher()
            it.shortcutHelperSystemShortcutsSource = systemShortcutsSource
            it.shortcutHelperMultiTaskingShortcutsSource = multitaskingShortcutsSource
            it.shortcutHelperAppCategoriesShortcutsSource = defaultAppsShortcutsSource
            it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
            it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
        }

    private val testScope = kosmos.testScope
Loading