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

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

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

This patch provides
* Implemented the repository
* Implemented the unittest and kosmos

Bug: b/410892855
Flag:com.android.hardware.input.enable_talkback_and_magnifier_key_gestures
Test: atest AccessibilityShortcutsImplTest
Change-Id: I77659997d530dbeda2e45aae675550f1727245c2
parent 9bb8be17
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -1390,6 +1390,9 @@
    <!-- Content description of zen mode time condition minus button (not shown on the screen). [CHAR LIMIT=NONE] -->
    <string name="accessibility_manual_zen_less_time">Less time.</string>

    <!-- Title for the accessibility preference screen to enable screen magnification. [CHAR LIMIT=35] -->
    <string name="accessibility_screen_magnification_title">Magnification</string>

    <!-- Button label for generic cancel action [CHAR LIMIT=20] -->
    <string name="cancel">Cancel</string>
    <!-- Button label for generic next action [CHAR LIMIT=20] -->
+272 −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.data.repository

import android.accessibilityservice.AccessibilityServiceInfo
import android.content.ComponentName
import android.content.packageManager
import android.content.pm.ApplicationInfo
import android.content.pm.ResolveInfo
import android.content.pm.ServiceInfo
import android.content.res.mainResources
import android.hardware.input.KeyGestureEvent
import android.view.KeyEvent
import android.view.accessibility.AccessibilityManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME
import com.android.internal.accessibility.common.ShortcutConstants
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
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.ArgumentMatchers.anyInt
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.eq
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
class AccessibilityShortcutsRepositoryImplTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val packageManager = kosmos.packageManager
    private val userTracker = kosmos.userTracker
    private val resources = kosmos.mainResources
    private val testScope = kosmos.testScope

    // mocks
    private val accessibilityManager: AccessibilityManager = mock(AccessibilityManager::class.java)

    private lateinit var underTest: AccessibilityShortcutsRepositoryImpl

    @Before
    fun setUp() {
        underTest =
            AccessibilityShortcutsRepositoryImpl(
                context,
                accessibilityManager,
                packageManager,
                userTracker,
                resources,
                kosmos.testDispatcher,
            )
    }

    @Test
    fun getKeyGestureConfirmInfo_nonExistTypeReceived_isNull() {
        testScope.runTest {
            // Just test a random non-accessibility service type
            val keyGestureConfirmInfo =
                underTest.getKeyGestureConfirmInfoByType(
                    KeyGestureEvent.KEY_GESTURE_TYPE_HOME,
                    0,
                    0,
                    "empty",
                )

            assertThat(keyGestureConfirmInfo).isNull()
        }
    }

    @Test
    fun getKeyGestureConfirmInfo_onMagnificationTypeReceived_getExpectedInfo() {
        testScope.runTest {
            val metaState = KeyEvent.META_META_ON or KeyEvent.META_ALT_ON

            val keyGestureConfirmInfo =
                underTest.getKeyGestureConfirmInfoByType(
                    KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
                    metaState,
                    KeyEvent.KEYCODE_M,
                    getTargetNameByType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION),
                )

            assertThat(keyGestureConfirmInfo).isNotNull()
            assertThat(keyGestureConfirmInfo?.title).isEqualTo("Turn on Magnification?")
            assertThat(keyGestureConfirmInfo?.contentText)
                .isEqualTo(
                    "Action + Alt + M is the keyboard shortcut to use Magnification. " +
                        "This allows you to quickly zoom in on the screen to make content larger."
                )
        }
    }

    @Test
    fun getKeyGestureConfirmInfo_serviceUninstalled_isNull() {
        testScope.runTest {
            val metaState = KeyEvent.META_META_ON or KeyEvent.META_ALT_ON
            // If voice access isn't installed on device.
            whenever(accessibilityManager.getInstalledServiceInfoWithComponentName(anyOrNull()))
                .thenReturn(null)

            val keyGestureConfirmInfo =
                underTest.getKeyGestureConfirmInfoByType(
                    KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS,
                    metaState,
                    KeyEvent.KEYCODE_V,
                    getTargetNameByType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS),
                )

            assertThat(keyGestureConfirmInfo).isNull()
        }
    }

    @Test
    fun getKeyGestureConfirmInfo_onVoiceAccessTypeReceived_getExpectedInfo() {
        testScope.runTest {
            val metaState = KeyEvent.META_META_ON or KeyEvent.META_ALT_ON
            val type = KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS

            val a11yServiceInfo = spy(getMockAccessibilityServiceInfo("Voice access"))
            whenever(a11yServiceInfo.loadIntro(any())).thenReturn("Voice access Intro.")
            whenever(
                    accessibilityManager.getInstalledServiceInfoWithComponentName(
                        ComponentName.unflattenFromString(getTargetNameByType(type))
                    )
                )
                .thenReturn(a11yServiceInfo)

            val keyGestureConfirmInfo =
                underTest.getKeyGestureConfirmInfoByType(
                    type,
                    metaState,
                    KeyEvent.KEYCODE_V,
                    getTargetNameByType(type),
                )

            assertThat(keyGestureConfirmInfo).isNotNull()
            assertThat(keyGestureConfirmInfo?.title).isEqualTo("Turn on Voice access?")
            assertThat(keyGestureConfirmInfo?.contentText)
                .isEqualTo(
                    "Action + Alt + V is the keyboard shortcut to use Voice access. " +
                        "Voice access Intro."
                )
        }
    }

    @Test
    fun enableShortcutsForTargets_targetNameForMagnification_enabled() {
        val targetName = getTargetNameByType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION)

        underTest.enableShortcutsForTargets(targetName)

        verify(accessibilityManager)
            .enableShortcutsForTargets(
                eq(true),
                eq(ShortcutConstants.UserShortcutType.KEY_GESTURE),
                eq(setOf(targetName)),
                anyInt(),
            )
    }

    @Test
    fun enableShortcutsForTargets_targetNameForS2S_enabled() {
        val targetName =
            getTargetNameByType(KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK)

        underTest.enableShortcutsForTargets(targetName)

        verify(accessibilityManager)
            .enableShortcutsForTargets(
                eq(true),
                eq(ShortcutConstants.UserShortcutType.KEY_GESTURE),
                eq(setOf(targetName)),
                anyInt(),
            )
    }

    @Test
    fun enableShortcutsForTargets_targetNameForVoiceAccess_enabled() {
        val targetName = getTargetNameByType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)

        underTest.enableShortcutsForTargets(targetName)

        verify(accessibilityManager)
            .enableShortcutsForTargets(
                eq(true),
                eq(ShortcutConstants.UserShortcutType.KEY_GESTURE),
                eq(setOf(targetName)),
                anyInt(),
            )
    }

    @Test
    fun enableShortcutsForTargets_targetNameForTalkBack_enabled() {
        val targetName = getTargetNameByType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SCREEN_READER)

        underTest.enableShortcutsForTargets(targetName)

        verify(accessibilityManager)
            .enableShortcutsForTargets(
                eq(true),
                eq(ShortcutConstants.UserShortcutType.KEY_GESTURE),
                eq(setOf(targetName)),
                anyInt(),
            )
    }

    private fun getMockAccessibilityServiceInfo(featureName: String): AccessibilityServiceInfo {
        val packageName = "com.android.test"
        val componentName = ComponentName(packageName, "$packageName.test_a11y_service")

        val applicationInfo = mock(ApplicationInfo::class.java)
        applicationInfo.packageName = componentName.packageName

        val serviceInfo = spy(ServiceInfo())
        serviceInfo.packageName = componentName.packageName
        serviceInfo.name = componentName.className
        serviceInfo.applicationInfo = applicationInfo

        val resolveInfo = mock(ResolveInfo::class.java)
        resolveInfo.serviceInfo = serviceInfo
        whenever(resolveInfo.loadLabel(any())).thenReturn(featureName)

        val a11yServiceInfo = AccessibilityServiceInfo(resolveInfo, context)
        a11yServiceInfo.componentName = componentName
        return a11yServiceInfo
    }

    private fun getTargetNameByType(keyGestureType: Int): String {
        return when (keyGestureType) {
            KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION -> MAGNIFICATION_CONTROLLER_NAME
            KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK ->
                resources.getString(
                    com.android.internal.R.string.config_defaultSelectToSpeakService
                )

            KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS ->
                resources.getString(com.android.internal.R.string.config_defaultVoiceAccessService)

            KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SCREEN_READER ->
                resources.getString(
                    com.android.internal.R.string.config_defaultAccessibilityService
                )

            else -> ""
        }
    }
}
+8 −0
Original line number Diff line number Diff line
@@ -2779,6 +2779,14 @@
    <!-- accessibility label for paging indicator in quick settings [CHAR LIMITi=NONE] -->
    <string name="accessibility_quick_settings_page">Page <xliff:g name="current_page" example="1">%1$d</xliff:g> of <xliff:g name="num_pages" example="2">%2$d</xliff:g></string>

    <!-- Strings for the dialog to enable accessibility services in key gestures -->
    <!-- Text for the dialog title. -->
    <string name="accessibility_key_gesture_dialog_title" translatable="false">Turn on <xliff:g name="feature_name" example="Magnification">%1$s</xliff:g>?</string>
    <!-- Text for showing inside the dialog.-->
    <string name="accessibility_key_gesture_dialog_content" translatable="false">Action + <xliff:g name="secondary_key" example="Alt">%1$s</xliff:g> + <xliff:g name="key_code" example="M">%2$s</xliff:g> is the keyboard shortcut to use <xliff:g name="feature_name" example="Magnification">%3$s</xliff:g>. <xliff:g name="feature_intro" example="Magnification intro">%4$s</xliff:g></string>
    <!-- The prefix string for the magnification intro -->
    <string name="accessibility_key_gesture_dialog_magnifier_intro" translatable="false">This allows you to quickly zoom in on the screen to make content larger.</string>

    <!-- Plugin control section of the tuner. Non-translatable since it should
         not appear on production builds ever. -->
    <string name="plugins" translatable="false">Plugins</string>
+7 −1
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.systemui.accessibility

import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepository
import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepositoryImpl
import com.android.systemui.accessibility.data.repository.AccessibilityShortcutsRepository
import com.android.systemui.accessibility.data.repository.AccessibilityShortcutsRepositoryImpl
import com.android.systemui.accessibility.data.repository.ColorCorrectionRepository
import com.android.systemui.accessibility.data.repository.ColorCorrectionRepositoryImpl
import com.android.systemui.accessibility.data.repository.ColorInversionRepository
@@ -43,6 +45,10 @@ interface AccessibilityModule {
        impl: AccessibilityQsShortcutsRepositoryImpl
    ): AccessibilityQsShortcutsRepository

    @Binds fun magnification(impl: MagnificationImpl): Magnification

    @Binds
    fun magnification(impl: MagnificationImpl): Magnification
    fun accessibilityShortcutsRepository(
        impl: AccessibilityShortcutsRepositoryImpl
    ): AccessibilityShortcutsRepository
}
+23 −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.data.model

data class KeyGestureConfirmInfo(
    val title: CharSequence,
    val contentText: CharSequence,
    val targetName: String,
)
Loading