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

Commit bf18a51e authored by Brad Hinegardner's avatar Brad Hinegardner Committed by Android (Google) Code Review
Browse files

Merge "Change which a11y services we change behavior for" into main

parents a49d4ea3 aece73ca
Loading
Loading
Loading
Loading
+82 −3
Original line number Diff line number Diff line
@@ -16,11 +16,15 @@

package com.android.systemui.accessibility.data.repository

import android.accessibilityservice.AccessibilityServiceInfo
import android.view.accessibility.AccessibilityManager
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.kosmos.backgroundScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
@@ -29,6 +33,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
@@ -42,9 +47,12 @@ class AccessibilityRepositoryTest : SysuiTestCase() {

    // mocks
    @Mock private lateinit var a11yManager: AccessibilityManager
    private val testKosmos = testKosmos()
    private val testScope = testKosmos.testScope
    private val backgroundScope = testKosmos.backgroundScope

    // real impls
    private val underTest by lazy { AccessibilityRepository(a11yManager) }
    private val underTest by lazy { AccessibilityRepository(a11yManager, backgroundScope) }

    @Test
    fun isTouchExplorationEnabled_reflectsA11yManager_initFalse() = runTest {
@@ -79,4 +87,75 @@ class AccessibilityRepositoryTest : SysuiTestCase() {
            .onTouchExplorationStateChanged(/* enabled= */ false)
        assertThat(isTouchExplorationEnabled).isFalse()
    }

    @Test
    fun isEnabledFiltered_reflectsA11yManager_initFalse() =
        testScope.runTest {
            whenever(a11yManager.getEnabledAccessibilityServiceList(eq(FILTERED_A11Y_SERVICES)))
                .thenReturn(emptyList<AccessibilityServiceInfo>())
            val isEnabledFiltered by collectLastValue(underTest.isEnabledFiltered)
            assertThat(isEnabledFiltered).isFalse()
        }

    @Test
    fun isEnabledFiltered_reflectsA11yManager_changeTrue() =
        testScope.runTest {
            whenever(a11yManager.getEnabledAccessibilityServiceList(eq(FILTERED_A11Y_SERVICES)))
                .thenReturn(emptyList())
            val isEnabledFiltered by collectLastValue(underTest.isEnabledFiltered)
            runCurrent()
            withArgCaptor {
                    verify(a11yManager).addAccessibilityServicesStateChangeListener(capture())
                }
                .onAccessibilityServicesStateChanged(a11yManager)
            assertThat(isEnabledFiltered).isFalse()

            // Change the services list to a non-empty list
            val wantedList = listOf(AccessibilityServiceInfo())
            whenever(a11yManager.getEnabledAccessibilityServiceList(eq(FILTERED_A11Y_SERVICES)))
                .thenReturn(wantedList)
            val isEnabledFiltered2 by collectLastValue(underTest.isEnabledFiltered)
            runCurrent()
            withArgCaptor {
                    verify(a11yManager).addAccessibilityServicesStateChangeListener(capture())
                }
                .onAccessibilityServicesStateChanged(a11yManager)
            assertThat(isEnabledFiltered2).isTrue()
        }

    @Test
    fun isEnabledFiltered_reflectsA11yManager_changeFalse() =
        testScope.runTest {
            val wantedList = listOf(AccessibilityServiceInfo())
            whenever(a11yManager.getEnabledAccessibilityServiceList(eq(FILTERED_A11Y_SERVICES)))
                .thenReturn(wantedList)
            val isEnabledFiltered by collectLastValue(underTest.isEnabledFiltered)
            runCurrent()
            withArgCaptor {
                    verify(a11yManager).addAccessibilityServicesStateChangeListener(capture())
                }
                .onAccessibilityServicesStateChanged(a11yManager)
            assertThat(isEnabledFiltered).isTrue()

            // Change the services list to an emptylist
            whenever(a11yManager.getEnabledAccessibilityServiceList(eq(FILTERED_A11Y_SERVICES)))
                .thenReturn(emptyList())

            val isEnabledFiltered2 by collectLastValue(underTest.isEnabledFiltered)
            runCurrent()
            withArgCaptor {
                    verify(a11yManager).addAccessibilityServicesStateChangeListener(capture())
                }
                .onAccessibilityServicesStateChanged(a11yManager)
            assertThat(isEnabledFiltered2).isFalse()
        }

    companion object {
        private const val FILTERED_A11Y_SERVICES =
            AccessibilityServiceInfo.FEEDBACK_AUDIBLE or
                AccessibilityServiceInfo.FEEDBACK_SPOKEN or
                AccessibilityServiceInfo.FEEDBACK_VISUAL or
                AccessibilityServiceInfo.FEEDBACK_HAPTIC or
                AccessibilityServiceInfo.FEEDBACK_BRAILLE
    }
}
+7 −6
Original line number Diff line number Diff line
@@ -20,13 +20,14 @@ package com.android.systemui.keyguard.domain.interactor
import android.app.admin.DevicePolicyManager
import android.os.UserHandle
import android.platform.test.annotations.EnableFlags
import android.view.accessibility.AccessibilityManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor
import com.android.systemui.accessibility.domain.interactor.accessibilityInteractor
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
@@ -99,7 +100,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
    @Mock private lateinit var shadeInteractor: ShadeInteractor
    @Mock private lateinit var logger: KeyguardQuickAffordancesLogger
    @Mock private lateinit var metricsLogger: KeyguardQuickAffordancesMetricsLogger
    @Mock private lateinit var accessibilityManager: AccessibilityManager
    @Mock private lateinit var accessibilityInteractor: AccessibilityInteractor

    private lateinit var underTest: KeyguardQuickAffordanceInteractor

@@ -201,14 +202,14 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
                biometricSettingsRepository = biometricSettingsRepository,
                backgroundDispatcher = kosmos.testDispatcher,
                appContext = context,
                accessibilityManager = accessibilityManager,
                accessibilityInteractor = accessibilityInteractor,
                sceneInteractor = { kosmos.sceneInteractor },
                msdlPlayer = msdlPlayer,
            )
        kosmos.keyguardQuickAffordanceInteractor = underTest

        whenever(shadeInteractor.anyExpansion).thenReturn(MutableStateFlow(0f))
        whenever(accessibilityManager.isEnabled()).thenReturn(false)
        whenever(accessibilityInteractor.isEnabledFiltered).thenReturn(MutableStateFlow(false))
    }

    @Test
@@ -679,7 +680,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
    @Test
    fun useLongPress_withA11yEnabled_isFalse() =
        testScope.runTest {
            whenever(accessibilityManager.isEnabled()).thenReturn(true)
            whenever(accessibilityInteractor.isEnabledFiltered).thenReturn(MutableStateFlow(true))
            val useLongPress by collectLastValue(underTest.useLongPress())
            assertThat(useLongPress).isFalse()
        }
@@ -687,7 +688,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
    @Test
    fun useLongPress_withA11yDisabled_isFalse() =
        testScope.runTest {
            whenever(accessibilityManager.isEnabled()).thenReturn(false)
            whenever(accessibilityInteractor.isEnabledFiltered).thenReturn(MutableStateFlow(false))
            val useLongPress by collectLastValue(underTest.useLongPress())
            assertThat(useLongPress).isTrue()
        }
+44 −5
Original line number Diff line number Diff line
@@ -16,16 +16,22 @@

package com.android.systemui.accessibility.data.repository

import android.accessibilityservice.AccessibilityServiceInfo
import android.view.accessibility.AccessibilityManager
import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
import com.android.app.tracing.FlowTracing.tracedAwaitClose
import com.android.app.tracing.FlowTracing.tracedConflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Background
import dagger.Module
import dagger.Provides
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.stateIn

/** Exposes accessibility-related state. */
interface AccessibilityRepository {
@@ -34,18 +40,25 @@ interface AccessibilityRepository {
    /** @see [AccessibilityManager.isEnabled] */
    val isEnabled: Flow<Boolean>

    /** Returns whether a filtered set of [AccessibilityServiceInfo]s are enabled. */
    val isEnabledFiltered: StateFlow<Boolean>

    fun getRecommendedTimeout(originalTimeout: Duration, uiFlags: Int): Duration

    companion object {
        operator fun invoke(a11yManager: AccessibilityManager): AccessibilityRepository =
            AccessibilityRepositoryImpl(a11yManager)
        operator fun invoke(
            a11yManager: AccessibilityManager,
            @Background backgroundScope: CoroutineScope,
        ): AccessibilityRepository = AccessibilityRepositoryImpl(a11yManager, backgroundScope)
    }
}

private const val TAG = "AccessibilityRepository"

private class AccessibilityRepositoryImpl(private val manager: AccessibilityManager) :
    AccessibilityRepository {
private class AccessibilityRepositoryImpl(
    private val manager: AccessibilityManager,
    @Background private val scope: CoroutineScope,
) : AccessibilityRepository {
    override val isTouchExplorationEnabled: Flow<Boolean> =
        tracedConflatedCallbackFlow(TAG) {
                val listener = TouchExplorationStateChangeListener(::trySend)
@@ -66,6 +79,30 @@ private class AccessibilityRepositoryImpl(private val manager: AccessibilityMana
            }
            .distinctUntilChanged()

    override val isEnabledFiltered: StateFlow<Boolean> =
        tracedConflatedCallbackFlow(TAG) {
                val listener =
                    AccessibilityManager.AccessibilityServicesStateChangeListener {
                        accessibilityManager ->
                        trySend(
                            accessibilityManager
                                .getEnabledAccessibilityServiceList(
                                    AccessibilityServiceInfo.FEEDBACK_AUDIBLE or
                                        AccessibilityServiceInfo.FEEDBACK_SPOKEN or
                                        AccessibilityServiceInfo.FEEDBACK_VISUAL or
                                        AccessibilityServiceInfo.FEEDBACK_HAPTIC or
                                        AccessibilityServiceInfo.FEEDBACK_BRAILLE
                                )
                                .isNotEmpty()
                        )
                    }
                manager.addAccessibilityServicesStateChangeListener(listener)
                tracedAwaitClose(TAG) {
                    manager.removeAccessibilityServicesStateChangeListener(listener)
                }
            }
            .stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = false)

    override fun getRecommendedTimeout(originalTimeout: Duration, uiFlags: Int): Duration {
        return manager
            .getRecommendedTimeoutMillis(originalTimeout.inWholeMilliseconds.toInt(), uiFlags)
@@ -75,5 +112,7 @@ private class AccessibilityRepositoryImpl(private val manager: AccessibilityMana

@Module
object AccessibilityRepositoryModule {
    @Provides fun provideRepo(manager: AccessibilityManager) = AccessibilityRepository(manager)
    @Provides
    fun provideRepo(manager: AccessibilityManager, @Background backgroundScope: CoroutineScope) =
        AccessibilityRepository(manager, backgroundScope)
}
+5 −0
Original line number Diff line number Diff line
@@ -16,10 +16,12 @@

package com.android.systemui.accessibility.domain.interactor

import android.accessibilityservice.AccessibilityServiceInfo
import com.android.systemui.accessibility.data.repository.AccessibilityRepository
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow

@SysUISingleton
class AccessibilityInteractor
@@ -32,4 +34,7 @@ constructor(

    /** @see [android.view.accessibility.AccessibilityManager.isEnabled] */
    val isEnabled: Flow<Boolean> = a11yRepo.isEnabled

    /** This returns whether a filtered set of [AccessibilityServiceInfo]s are enabled. */
    val isEnabledFiltered: StateFlow<Boolean> = a11yRepo.isEnabledFiltered
}
+6 −4
Original line number Diff line number Diff line
@@ -22,12 +22,12 @@ import android.app.admin.DevicePolicyManager
import android.content.Context
import android.content.Intent
import android.util.Log
import android.view.accessibility.AccessibilityManager
import com.android.app.tracing.coroutines.withContextTraced as withContext
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.internal.widget.LockPatternUtils
import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.systemui.Flags.msdlFeedback
import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton
@@ -91,7 +91,7 @@ constructor(
    private val devicePolicyManager: DevicePolicyManager,
    private val dockManager: DockManager,
    private val biometricSettingsRepository: BiometricSettingsRepository,
    private val accessibilityManager: AccessibilityManager,
    private val accessibilityInteractor: AccessibilityInteractor,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    @ShadeDisplayAware private val appContext: Context,
    private val sceneInteractor: Lazy<SceneInteractor>,
@@ -109,8 +109,10 @@ constructor(
     * If `false`, the UI goes back to using single taps.
     */
    fun useLongPress(): Flow<Boolean> =
        dockManager.retrieveIsDocked().map { isDocked ->
            !isDocked && !accessibilityManager.isEnabled()
        combine(dockManager.retrieveIsDocked(), accessibilityInteractor.isEnabledFiltered) {
            isDocked,
            isAccessibilityEnabled ->
            !isDocked && !isAccessibilityEnabled
        }

    /** Returns an observable for the quick affordance at the given position. */
Loading