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

Commit 80b885c6 authored by Hongyu Long's avatar Hongyu Long Committed by Android (Google) Code Review
Browse files

Merge "a11y: Add first-time use dialog for key gestures (3/n)" into main

parents 946af159 1fa23968
Loading
Loading
Loading
Loading
+50 −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.internal.accessibility.common;

import android.provider.Settings;

/** Collection of common constants for a KeyGestureEvent on any accessibility feature */
public final class KeyGestureEventConstants {
    private KeyGestureEventConstants() {}

    /**
     * Used as the name of the extra data when we put the value of the key gesture type on the key
     * gesture event into an intent.
     */
    public static final String KEY_GESTURE_TYPE = "KEY_GESTURE_TYPE";

    /**
     * Used as the name of the extra data when we put the value of the meta state on the key gesture
     * event into an intent.
     */
    public static final String META_STATE = "META_STATE";

    /**
     * Used as the name of the extra data when we put the value of the key code on the key gesture
     * event into an intent.
     */
    public static final String KEY_CODE = "KEY_CODE";

    /**
     * Used as the name of the extra data when we put the value of the target name, e.g. a flattened
     * componentName or MAGNIFICATION_CONTROLLER_NAME, on the key gesture event into an intent.
     *
     * @see Settings.Secure#ACCESSIBILITY_GESTURE_TARGETS
     */
    public static final String TARGET_NAME = "TARGET_NAME";
}
+49 −24
Original line number Diff line number Diff line
@@ -24,6 +24,8 @@ import android.content.pm.ResolveInfo
import android.content.pm.ServiceInfo
import android.content.res.mainResources
import android.hardware.input.KeyGestureEvent
import android.text.Annotation
import android.text.Spanned
import android.view.KeyEvent
import android.view.accessibility.AccessibilityManager
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -77,66 +79,70 @@ class AccessibilityShortcutsRepositoryImplTest : SysuiTestCase() {
    }

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

            assertThat(keyGestureConfirmInfo).isNull()
            assertThat(titleToContent).isNull()
        }
    }

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

            val keyGestureConfirmInfo =
                underTest.getKeyGestureConfirmInfoByType(
            val titleToContent =
                underTest.getTitleToContentForKeyGestureDialog(
                    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)
            assertThat(titleToContent).isNotNull()
            assertThat(titleToContent?.first).isEqualTo("Turn on Magnification?")
            val contentText = titleToContent?.second
            assertThat(hasExpectedAnnotation(contentText)).isTrue()
            // `contentText` here is an instance of SpannableStringBuilder, so we only need to
            // compare its value here.
            assertThat(contentText?.toString())
                .isEqualTo(
                    "Action + Alt + M is the keyboard shortcut to use Magnification. " +
                    "Action icon + 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() {
    fun getTitleToContentForKeyGestureDialog_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(
            val titleToContent =
                underTest.getTitleToContentForKeyGestureDialog(
                    KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS,
                    metaState,
                    KeyEvent.KEYCODE_V,
                    getTargetNameByType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS),
                )

            assertThat(keyGestureConfirmInfo).isNull()
            assertThat(titleToContent).isNull()
        }
    }

    @Test
    fun getKeyGestureConfirmInfo_onVoiceAccessTypeReceived_getExpectedInfo() {
    fun getTitleToContentForKeyGestureDialog_onVoiceAccessTypeReceived_getExpectedInfo() {
        testScope.runTest {
            val metaState = KeyEvent.META_META_ON or KeyEvent.META_ALT_ON
            val type = KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS
@@ -150,19 +156,23 @@ class AccessibilityShortcutsRepositoryImplTest : SysuiTestCase() {
                )
                .thenReturn(a11yServiceInfo)

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

            assertThat(keyGestureConfirmInfo).isNotNull()
            assertThat(keyGestureConfirmInfo?.title).isEqualTo("Turn on Voice access?")
            assertThat(keyGestureConfirmInfo?.contentText)
            assertThat(titleToContent).isNotNull()
            assertThat(titleToContent?.first).isEqualTo("Turn on Voice access?")
            val contentText = titleToContent?.second
            assertThat(hasExpectedAnnotation(contentText)).isTrue()
            // `contentText` here is an instance of SpannableStringBuilder, so we only need to
            // compare its value here.
            assertThat(contentText?.toString())
                .isEqualTo(
                    "Action + Alt + V is the keyboard shortcut to use Voice access. " +
                    "Action icon + Alt + V is the keyboard shortcut to use Voice access." +
                        " Voice access Intro."
                )
        }
@@ -269,4 +279,19 @@ class AccessibilityShortcutsRepositoryImplTest : SysuiTestCase() {
            else -> ""
        }
    }

    // Return true if the text contains the expected annotation.
    private fun hasExpectedAnnotation(text: CharSequence?): Boolean {
        if (text == null || text !is Spanned) {
            return false
        }

        val annotations = text.getSpans(0, text.length, Annotation::class.java)
        for (annotation in annotations) {
            if (annotation.key == "id" && annotation.value == "action_key_icon") {
                return true
            }
        }
        return false
    }
}
+6 −5
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.hardware.input.KeyGestureEvent
import android.view.KeyEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.accessibility.common.KeyGestureEventConstants
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.data.repository.AccessibilityShortcutsRepository
import com.android.systemui.broadcast.broadcastDispatcher
@@ -98,7 +99,7 @@ class KeyGestureDialogInteractorTest : SysuiTestCase() {
            runCurrent()

            verify(repository)
                .getKeyGestureConfirmInfoByType(
                .getTitleToContentForKeyGestureDialog(
                    eq(keyGestureType),
                    eq(metaState),
                    eq(keyCode),
@@ -116,10 +117,10 @@ class KeyGestureDialogInteractorTest : SysuiTestCase() {
        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)
                putExtra(KeyGestureEventConstants.KEY_GESTURE_TYPE, keyGestureType)
                putExtra(KeyGestureEventConstants.META_STATE, metaState)
                putExtra(KeyGestureEventConstants.KEY_CODE, keyCode)
                putExtra(KeyGestureEventConstants.TARGET_NAME, targetName)
            }

        broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
+132 −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.ui

import android.content.Intent
import android.hardware.input.KeyGestureEvent
import android.platform.test.annotations.EnableFlags
import android.view.KeyEvent
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.hardware.input.Flags
import com.android.internal.accessibility.common.KeyGestureEventConstants
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.keygesture.domain.KeyGestureDialogInteractor
import com.android.systemui.accessibility.keygesture.domain.keyGestureDialogInteractor
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.phone.systemUIDialogFactory
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.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@UiThreadTest
@EnableFlags(Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
class KeyGestureDialogStartableTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val broadcastDispatcher = kosmos.broadcastDispatcher
    private val interactor = kosmos.keyGestureDialogInteractor
    private val testScope = kosmos.testScope

    private lateinit var underTest: KeyGestureDialogStartable

    @Before
    fun setUp() {
        underTest =
            KeyGestureDialogStartable(
                interactor,
                kosmos.systemUIDialogFactory,
                kosmos.applicationCoroutineScope,
            )
    }

    @After
    fun tearDown() {
        // If we show the dialog, we must dismiss the dialog at the end of the test on the main
        // thread.
        underTest.currentDialog?.dismiss()
    }

    @Test
    fun start_doesNotShowDialogByDefault() =
        testScope.runTest {
            underTest.start()
            runCurrent()

            assertThat(underTest.currentDialog).isNull()
        }

    @Test
    fun start_onValidRequestReceived_showDialog() =
        testScope.runTest {
            underTest.start()
            runCurrent()

            // Trigger to send a broadcast event
            sendIntentBroadcast(
                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
                KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
                KeyEvent.KEYCODE_M,
                "targetNameForMagnification",
            )
            runCurrent()

            assertThat(underTest.currentDialog?.isShowing).isTrue()
        }

    @Test
    fun start_onInvalidRequestReceived_noDialog() =
        testScope.runTest {
            underTest.start()
            runCurrent()

            // Trigger to send a broadcast event
            sendIntentBroadcast(0, 0, KeyEvent.KEYCODE_M, "targetName")
            runCurrent()

            assertThat(underTest.currentDialog).isNull()
        }

    private fun sendIntentBroadcast(
        keyGestureType: Int,
        metaState: Int,
        keyCode: Int,
        targetName: String,
    ) {
        val intent =
            Intent().apply {
                action = KeyGestureDialogInteractor.ACTION
                putExtra(KeyGestureEventConstants.KEY_GESTURE_TYPE, keyGestureType)
                putExtra(KeyGestureEventConstants.META_STATE, metaState)
                putExtra(KeyGestureEventConstants.KEY_CODE, keyCode)
                putExtra(KeyGestureEventConstants.TARGET_NAME, targetName)
            }

        broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -2837,9 +2837,11 @@
    <!-- 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>
    <string name="accessibility_key_gesture_dialog_content" translatable="false">Action <annotation id="action_key_icon">icon</annotation> + <xliff:g name="secondary_key" example="Alt">^1</xliff:g> + <xliff:g name="key_code" example="M">^2</xliff:g> is the keyboard shortcut to use <xliff:g name="feature_name" example="Magnification">^3</xliff:g>. <xliff:g name="feature_intro" example="Magnification intro">^4</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>
    <!-- Positive button text for the dialog -->
    <string name="accessibility_key_gesture_dialog_positive_button_text" translatable="false">Turn on</string>

    <!-- Plugin control section of the tuner. Non-translatable since it should
         not appear on production builds ever. -->
Loading