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

Commit e5adf3b1 authored by Hongyu Long's avatar Hongyu Long
Browse files

a11y: Add first-time use dialog for key gestures (2/n)

This patch provides
* Implemented the interactor
* Implement the unittest and the kosmos or it

Bug: b/410892855
Flag: com.android.hardware.input.enable_talkback_and_magnifier_key_gestures
Test: atest KeyGestureDialogInteractorTest
Change-Id: I7d692b7df4c33d956962b5a16e452f84b7fd0b7e
parent 06994af7
Loading
Loading
Loading
Loading
+127 −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.accessibility.keygesture.domain

import android.content.Intent
import android.hardware.input.KeyGestureEvent
import android.view.KeyEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.data.repository.AccessibilityShortcutsRepository
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.coroutines.collectLastValue
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.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.kotlin.eq
import org.mockito.kotlin.verify

@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class KeyGestureDialogInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val broadcastDispatcher = kosmos.broadcastDispatcher
    private val testDispatcher = kosmos.testDispatcher
    private val testScope = kosmos.testScope

    // mocks
    private val repository = mock(AccessibilityShortcutsRepository::class.java)

    private lateinit var underTest: KeyGestureDialogInteractor

    @Before
    fun setUp() {
        underTest = KeyGestureDialogInteractor(repository, broadcastDispatcher, testDispatcher)
    }

    @Test
    fun onPositiveButtonClick_enabledShortcutsForFakeTarget() {
        val enabledTargetName = "fakeTargetName"

        underTest.onPositiveButtonClick(enabledTargetName)

        verify(repository).enableShortcutsForTargets(eq(enabledTargetName))
    }

    @Test
    fun keyGestureConfirmDialogRequest_invalidRequestReceived() {
        testScope.runTest {
            val keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION
            val metaState = 0
            val keyCode = 0
            val testTargetName = "fakeTargetName"
            val keyGestureConfirmInfo by collectLastValue(underTest.keyGestureConfirmDialogRequest)
            runCurrent()

            sendIntentBroadcast(keyGestureType, metaState, keyCode, testTargetName)
            runCurrent()

            assertThat(keyGestureConfirmInfo).isNull()
        }
    }

    @Test
    fun keyGestureConfirmDialogRequest_getFlowFromIntentForMagnification() {
        testScope.runTest {
            val keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION
            val metaState = KeyEvent.META_META_ON or KeyEvent.META_ALT_ON
            val keyCode = KeyEvent.KEYCODE_M
            val testTargetName = "targetNameForMagnification"
            collectLastValue(underTest.keyGestureConfirmDialogRequest)
            runCurrent()

            sendIntentBroadcast(keyGestureType, metaState, keyCode, testTargetName)
            runCurrent()

            verify(repository)
                .getKeyGestureConfirmInfoByType(
                    eq(keyGestureType),
                    eq(metaState),
                    eq(keyCode),
                    eq(testTargetName),
                )
        }
    }

    private fun sendIntentBroadcast(
        keyGestureType: Int,
        metaState: Int,
        keyCode: Int,
        targetName: String,
    ) {
        val intent =
            Intent().apply {
                action = KeyGestureDialogInteractor.ACTION
                putExtra(KeyGestureDialogInteractor.EXTRA_KEY_GESTURE_TYPE, keyGestureType)
                putExtra(KeyGestureDialogInteractor.EXTRA_META_STATE, metaState)
                putExtra(KeyGestureDialogInteractor.EXTRA_KEY_CODE, keyCode)
                putExtra(KeyGestureDialogInteractor.EXTRA_TARGET_NAME, targetName)
            }

        broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
    }
}
+97 −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.accessibility.keygesture.domain

import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.UserHandle
import androidx.annotation.VisibleForTesting
import com.android.systemui.accessibility.data.model.KeyGestureConfirmInfo
import com.android.systemui.accessibility.data.repository.AccessibilityShortcutsRepository
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext

/** Encapsulates business logic to interact with the key gesture dialog. */
@SysUISingleton
class KeyGestureDialogInteractor
@Inject
constructor(
    private val repository: AccessibilityShortcutsRepository,
    private val broadcastDispatcher: BroadcastDispatcher,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
) {
    /** Emits whenever a launch key gesture dialog broadcast is received. */
    val keyGestureConfirmDialogRequest: Flow<KeyGestureConfirmInfo?> =
        broadcastDispatcher
            .broadcastFlow(
                filter = IntentFilter().apply { addAction(ACTION) },
                user = UserHandle.SYSTEM,
                flags = Context.RECEIVER_NOT_EXPORTED,
            ) { intent, _ ->
                intent
            }
            .map { intent -> processDialogRequest(intent) }

    fun onPositiveButtonClick(targetName: String) {
        repository.enableShortcutsForTargets(targetName)
    }

    private suspend fun processDialogRequest(intent: Intent): KeyGestureConfirmInfo? {
        return withContext(backgroundDispatcher) {
            val keyGestureType = intent.getIntExtra(EXTRA_KEY_GESTURE_TYPE, 0)
            val targetName = intent.getStringExtra(EXTRA_TARGET_NAME)
            val metaState = intent.getIntExtra(EXTRA_META_STATE, 0)
            val keyCode = intent.getIntExtra(EXTRA_KEY_CODE, 0)

            if (isInvalidDialogRequest(keyGestureType, metaState, keyCode, targetName)) {
                null
            } else {
                repository.getKeyGestureConfirmInfoByType(
                    keyGestureType,
                    metaState,
                    keyCode,
                    targetName as String,
                )
            }
        }
    }

    private fun isInvalidDialogRequest(
        keyGestureType: Int,
        metaState: Int,
        keyCode: Int,
        targetName: String?,
    ): Boolean {
        return targetName.isNullOrEmpty() || keyGestureType == 0 || metaState == 0 || keyCode == 0
    }

    companion object {
        @VisibleForTesting
        const val ACTION = "com.android.systemui.action.LAUNCH_KEY_GESTURE_CONFIRM_DIALOG"
        @VisibleForTesting const val EXTRA_KEY_GESTURE_TYPE = "EXTRA_KEY_GESTURE_TYPE"
        @VisibleForTesting const val EXTRA_META_STATE = "EXTRA_META_STATE"
        @VisibleForTesting const val EXTRA_KEY_CODE = "EXTRA_KEY_CODE"
        @VisibleForTesting const val EXTRA_TARGET_NAME = "EXTRA_TARGET_NAME"
    }
}
+31 −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.accessibility.keygesture.domain

import com.android.systemui.accessibility.data.repository.accessibilityShortcutsRepository
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher

val Kosmos.keyGestureDialogInteractor by Fixture {
    KeyGestureDialogInteractor(
        accessibilityShortcutsRepository,
        broadcastDispatcher,
        testDispatcher,
    )
}