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

Commit 26c3a134 authored by Coco Duan's avatar Coco Duan
Browse files

Add custom accessbility action to open glanceable hub

Added a section under KeyguardRootView which handles accessibility actions.
Added a custom action to open glanceable hub with Talkback on.

Bug: b/319140280
Test: atest AccessibilityActionsViewModelTest
Test: manual with Talkback on
Flag: ACONFIG com.android.systemui.communal_hub TEAMFOOD
Change-Id: Ie1b957e0cca749f24e96a79d4a8f13660601b030
parent d08d92ee
Loading
Loading
Loading
Loading
+154 −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.
 */

@file:OptIn(ExperimentalCoroutinesApi::class)

package com.android.systemui.keyguard.ui.viewmodel

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.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
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.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

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

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val keyguardRepository = kosmos.fakeKeyguardRepository
    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository

    private lateinit var underTest: AccessibilityActionsViewModel

    @Before
    fun setUp() {
        underTest = kosmos.accessibilityActionsViewModelKosmos
    }

    @Test
    fun isOnKeyguard_isFalse_whenTransitioningAwayFromLockscreen() =
        testScope.runTest {
            val isOnKeyguard by collectLastValue(underTest.isOnKeyguard)

            // Shade not opened.
            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
            // Transitioning away from lock screen.
            keyguardTransitionRepository.sendTransitionStep(
                TransitionStep(
                    from = KeyguardState.LOCKSCREEN,
                    to = KeyguardState.PRIMARY_BOUNCER,
                    transitionState = TransitionState.STARTED,
                )
            )

            keyguardTransitionRepository.sendTransitionStep(
                from = KeyguardState.LOCKSCREEN,
                to = KeyguardState.PRIMARY_BOUNCER,
                transitionState = TransitionState.RUNNING,
                value = 0.5f,
            )
            assertThat(isOnKeyguard).isEqualTo(false)

            // Transitioned to bouncer.
            keyguardTransitionRepository.sendTransitionStep(
                from = KeyguardState.LOCKSCREEN,
                to = KeyguardState.PRIMARY_BOUNCER,
                transitionState = TransitionState.FINISHED,
                value = 1f,
            )
            assertThat(isOnKeyguard).isEqualTo(false)
        }

    @Test
    fun isOnKeyguard_isFalse_whenTransitioningToLockscreenIsRunning() =
        testScope.runTest {
            val isOnKeyguard by collectLastValue(underTest.isOnKeyguard)

            // Shade is not opened.
            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
            // Starts transitioning to lock screen.
            keyguardTransitionRepository.sendTransitionStep(
                TransitionStep(
                    from = KeyguardState.GLANCEABLE_HUB,
                    to = KeyguardState.LOCKSCREEN,
                    transitionState = TransitionState.STARTED,
                )
            )
            assertThat(isOnKeyguard).isEqualTo(false)

            keyguardTransitionRepository.sendTransitionStep(
                from = KeyguardState.GLANCEABLE_HUB,
                to = KeyguardState.LOCKSCREEN,
                transitionState = TransitionState.RUNNING,
                value = 0.5f,
            )
            assertThat(isOnKeyguard).isEqualTo(false)

            // Transition has finished.
            keyguardTransitionRepository.sendTransitionStep(
                from = KeyguardState.GLANCEABLE_HUB,
                to = KeyguardState.LOCKSCREEN,
                transitionState = TransitionState.FINISHED,
                value = 1f,
            )
            assertThat(isOnKeyguard).isEqualTo(true)
        }

    @Test
    fun isOnKeyguard_isTrue_whenKeyguardStateIsLockscreen_andShadeIsNotOpened() =
        testScope.runTest {
            val isOnKeyguard by collectLastValue(underTest.isOnKeyguard)

            keyguardTransitionRepository.sendTransitionSteps(
                from = KeyguardState.GONE,
                to = KeyguardState.LOCKSCREEN,
                testScope = testScope,
            )
            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)

            assertThat(isOnKeyguard).isEqualTo(true)
        }

    @Test
    fun isOnKeyguard_isFalse_whenKeyguardStateIsLockscreen_andShadeOpened() =
        testScope.runTest {
            val isOnKeyguard by collectLastValue(underTest.isOnKeyguard)

            keyguardTransitionRepository.sendTransitionSteps(
                from = KeyguardState.GONE,
                to = KeyguardState.LOCKSCREEN,
                testScope = testScope,
            )
            keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)

            assertThat(isOnKeyguard).isEqualTo(false)
        }
}
+3 −0
Original line number Diff line number Diff line
@@ -186,6 +186,8 @@
    <item type="id" name="action_remove_menu"/>
    <item type="id" name="action_edit"/>

    <item type="id" name="accessibility_action_open_communal_hub"/>

    <!-- rounded corner view id -->
    <item type="id" name="rounded_corner_top_left"/>
    <item type="id" name="rounded_corner_top_right"/>
@@ -231,6 +233,7 @@
    <item type="id" name="smart_space_barrier_bottom" />
    <item type="id" name="small_clock_guideline_top" />
    <item type="id" name="weather_clock_date_and_icons_barrier_bottom" />
    <item type="id" name="accessibility_actions_view" />

    <!-- Privacy dialog -->
    <item type="id" name="privacy_dialog_close_app_button" />
+4 −1
Original line number Diff line number Diff line
@@ -568,7 +568,7 @@
    <!-- Content description for the split notification shade that also includes QS (not shown on the screen). [CHAR LIMIT=NONE] -->
    <string name="accessibility_desc_qs_notification_shade">Quick settings and Notification shade.</string>
    <!-- Content description for the lock screen (not shown on the screen). [CHAR LIMIT=NONE] -->
    <string name="accessibility_desc_lock_screen">Lock screen.</string>
    <string name="accessibility_desc_lock_screen">Lock screen</string>
    <!-- Content description for the work profile lock screen. This prevents work profile apps from being used, but personal apps can be used as normal (not shown on the screen). [CHAR LIMIT=NONE] -->
    <string name="accessibility_desc_work_lock">Work lock screen</string>
    <!-- Content description for the close button in the zen mode panel introduction message. [CHAR LIMIT=NONE] -->
@@ -1128,6 +1128,9 @@
    <!-- Indication on the keyguard that is shown when the device is dock charging. [CHAR LIMIT=80]-->
    <string name="keyguard_indication_charging_time_dock"><xliff:g id="percentage" example="20%">%2$s</xliff:g> • Charging • Full in <xliff:g id="charging_time_left" example="4 hr, 2 min">%1$s</xliff:g></string>

    <!-- Label for accessibility action that shows widgets on lock screen on click. [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_open_communal_hub">Widgets on lock screen</string>

    <!-- Indicator on keyguard to start the communal tutorial. [CHAR LIMIT=100] -->
    <string name="communal_tutorial_indicator_text">Swipe left to start the communal tutorial</string>

+99 −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.keyguard.ui.binder

import android.os.Bundle
import android.view.View
import android.view.accessibility.AccessibilityNodeInfo
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.keyguard.ui.viewmodel.AccessibilityActionsViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.launch

/** View binder for accessibility actions placeholder on keyguard. */
object AccessibilityActionsViewBinder {
    fun bind(
        view: View,
        viewModel: AccessibilityActionsViewModel,
    ): DisposableHandle {
        val disposableHandle =
            view.repeatWhenAttached {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    view.contentDescription =
                        view.resources.getString(R.string.accessibility_desc_lock_screen)

                    launch {
                        viewModel.isOnKeyguard.collect { isOnKeyguard ->
                            view.importantForAccessibility =
                                if (isOnKeyguard) {
                                    View.IMPORTANT_FOR_ACCESSIBILITY_YES
                                } else {
                                    // The border won't be displayed when keyguard is not showing or
                                    // when the focus was previously on it but is now transitioning
                                    // away from the keyguard.
                                    View.IMPORTANT_FOR_ACCESSIBILITY_NO
                                }
                        }
                    }

                    launch {
                        viewModel.isCommunalAvailable.collect { canOpenGlanceableHub ->
                            view.accessibilityDelegate =
                                object : View.AccessibilityDelegate() {
                                    override fun onInitializeAccessibilityNodeInfo(
                                        host: View,
                                        info: AccessibilityNodeInfo
                                    ) {
                                        super.onInitializeAccessibilityNodeInfo(host, info)
                                        // Add custom actions
                                        if (canOpenGlanceableHub) {
                                            val action =
                                                AccessibilityNodeInfo.AccessibilityAction(
                                                    R.id.accessibility_action_open_communal_hub,
                                                    view.resources.getString(
                                                        R.string
                                                            .accessibility_action_open_communal_hub
                                                    ),
                                                )
                                            info.addAction(action)
                                        }
                                    }

                                    override fun performAccessibilityAction(
                                        host: View,
                                        action: Int,
                                        args: Bundle?
                                    ): Boolean {
                                        return if (
                                            action == R.id.accessibility_action_open_communal_hub
                                        ) {
                                            viewModel.openCommunalHub()
                                            true
                                        } else super.performAccessibilityAction(host, action, args)
                                    }
                                }
                        }
                    }
                }
            }
        return disposableHandle
    }
}
+3 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import com.android.systemui.communal.ui.view.layout.sections.CommunalTutorialInd
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.view.layout.sections.AccessibilityActionsSection
import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
@@ -52,6 +53,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
class DefaultKeyguardBlueprint
@Inject
constructor(
    accessibilityActionsSection: AccessibilityActionsSection,
    defaultIndicationAreaSection: DefaultIndicationAreaSection,
    defaultDeviceEntrySection: DefaultDeviceEntrySection,
    defaultShortcutsSection: DefaultShortcutsSection,
@@ -73,6 +75,7 @@ constructor(

    override val sections =
        listOfNotNull(
            accessibilityActionsSection,
            defaultIndicationAreaSection,
            defaultShortcutsSection,
            defaultAmbientIndicationAreaSection.getOrNull(),
Loading