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

Commit f8554bbe authored by Chris Göllner's avatar Chris Göllner Committed by Android (Google) Code Review
Browse files

Merge "Shortcut Helper - Use physical keyboard id when no id is specified" into main

parents 8db4979f ddd3066a
Loading
Loading
Loading
Loading
+21 −1
Original line number Diff line number Diff line
@@ -20,16 +20,24 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.input.InputManager
import android.os.UserHandle
import android.view.KeyCharacterMap.VIRTUAL_KEYBOARD
import com.android.systemui.CoreStartable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Inactive
import com.android.systemui.shared.hardware.findInputDevice
import com.android.systemui.statusbar.CommandQueue
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

@SysUISingleton
class ShortcutHelperRepository
@@ -37,6 +45,9 @@ class ShortcutHelperRepository
constructor(
    private val commandQueue: CommandQueue,
    private val broadcastDispatcher: BroadcastDispatcher,
    private val inputManager: InputManager,
    @Background private val backgroundScope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
) : CoreStartable {

    val state = MutableStateFlow<ShortcutHelperState>(Inactive)
@@ -44,7 +55,9 @@ constructor(
    override fun start() {
        registerBroadcastReceiver(
            action = Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS,
            onReceive = { state.value = Active() }
            onReceive = {
                backgroundScope.launch { state.value = Active(findPhysicalKeyboardId()) }
            }
        )
        registerBroadcastReceiver(
            action = Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS,
@@ -72,6 +85,13 @@ constructor(
        )
    }

    private suspend fun findPhysicalKeyboardId() =
        withContext(backgroundDispatcher) {
            val firstEnabledPhysicalKeyboard =
                inputManager.findInputDevice { it.isEnabled && it.isFullKeyboard && !it.isVirtual }
            return@withContext firstEnabledPhysicalKeyboard?.id ?: VIRTUAL_KEYBOARD
        }

    fun hide() {
        state.value = Inactive
    }
+1 −1
Original line number Diff line number Diff line
@@ -19,5 +19,5 @@ package com.android.systemui.keyboard.shortcut.shared.model
sealed interface ShortcutHelperState {
    data object Inactive : ShortcutHelperState

    data class Active(val deviceId: Int? = null) : ShortcutHelperState
    data class Active(val deviceId: Int) : ShortcutHelperState
}
+102 −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.hardware.input.fakeInputManager
import android.view.KeyCharacterMap.VIRTUAL_KEYBOARD
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.shared.model.ShortcutHelperState
import com.android.systemui.keyboard.shortcut.shortcutHelperRepository
import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

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

    private val kosmos = testKosmos()

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

    @Test
    fun state_activeThroughToggle_emitsActiveWithDeviceIdFromEvent() =
        testScope.runTest {
            val deviceId = 123
            val state by collectLastValue(repo.state)

            helper.toggle(deviceId)

            assertThat(state).isEqualTo(ShortcutHelperState.Active(deviceId))
        }

    @Test
    fun state_activeThroughActivity_noKeyboardActive_emitsActiveWithVirtualDeviceId() =
        testScope.runTest {
            val state by collectLastValue(repo.state)

            helper.showFromActivity()

            assertThat(state).isEqualTo(ShortcutHelperState.Active(VIRTUAL_KEYBOARD))
        }

    @Test
    fun state_activeThroughActivity_virtualKeyboardActive_emitsActiveWithVirtualDeviceId() =
        testScope.runTest {
            val state by collectLastValue(repo.state)

            fakeInputManager.addVirtualKeyboard()
            helper.showFromActivity()

            assertThat(state).isEqualTo(ShortcutHelperState.Active(VIRTUAL_KEYBOARD))
        }

    @Test
    fun state_activeThroughActivity_physicalKeyboardActive_emitsActiveWithDeviceId() =
        testScope.runTest {
            val deviceId = 456
            val state by collectLastValue(repo.state)

            fakeInputManager.addPhysicalKeyboard(deviceId)
            helper.showFromActivity()

            assertThat(state).isEqualTo(ShortcutHelperState.Active(deviceId))
        }

    @Test
    fun state_activeThroughActivity_physicalKeyboardDisabled_emitsActiveWithVirtualDeviceId() =
        testScope.runTest {
            val deviceId = 456
            val state by collectLastValue(repo.state)

            fakeInputManager.addPhysicalKeyboard(deviceId)
            fakeInputManager.inputManager.disableInputDevice(deviceId)
            helper.showFromActivity()

            assertThat(state).isEqualTo(ShortcutHelperState.Active(VIRTUAL_KEYBOARD))
        }
}
+85 −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 android.hardware.input

import android.view.InputDevice
import android.view.KeyCharacterMap
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.invocation.InvocationOnMock

class FakeInputManager {

    private val devices = mutableMapOf<Int, InputDevice>()

    val inputManager =
        mock<InputManager> {
            whenever(getInputDevice(anyInt())).thenAnswer { invocation ->
                val deviceId = invocation.arguments[0] as Int
                return@thenAnswer devices[deviceId]
            }
            whenever(inputDeviceIds).thenAnswer {
                return@thenAnswer devices.keys.toIntArray()
            }

            fun setDeviceEnabled(invocation: InvocationOnMock, enabled: Boolean) {
                val deviceId = invocation.arguments[0] as Int
                val device = devices[deviceId] ?: return
                devices[deviceId] = device.copy(enabled = enabled)
            }

            whenever(disableInputDevice(anyInt())).thenAnswer { invocation ->
                setDeviceEnabled(invocation, enabled = false)
            }
            whenever(enableInputDevice(anyInt())).thenAnswer { invocation ->
                setDeviceEnabled(invocation, enabled = true)
            }
        }

    fun addPhysicalKeyboard(id: Int, enabled: Boolean = true) {
        check(id > 0) { "Physical keyboard ids have to be > 0" }
        addKeyboard(id, enabled)
    }

    fun addVirtualKeyboard(enabled: Boolean = true) {
        addKeyboard(id = KeyCharacterMap.VIRTUAL_KEYBOARD, enabled)
    }

    private fun addKeyboard(id: Int, enabled: Boolean = true) {
        devices[id] =
            InputDevice.Builder()
                .setId(id)
                .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
                .setSources(InputDevice.SOURCE_KEYBOARD)
                .setEnabled(enabled)
                .build()
    }

    private fun InputDevice.copy(
        id: Int = getId(),
        type: Int = keyboardType,
        sources: Int = getSources(),
        enabled: Boolean = isEnabled
    ) =
        InputDevice.Builder()
            .setId(id)
            .setKeyboardType(type)
            .setSources(sources)
            .setEnabled(enabled)
            .build()
}
+21 −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 android.hardware.input

import com.android.systemui.kosmos.Kosmos

val Kosmos.fakeInputManager by Kosmos.Fixture { FakeInputManager() }
Loading