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

Commit aece73ca authored by Brad Hinegardner's avatar Brad Hinegardner
Browse files

Change which a11y services we change behavior for

Previously we were using accessibilityManager.isEnabled

The issue is that various apps include an
AccessibilityService and would trigger
isEnabled to be true when we do not want
that behavior.

Bug: 403771473
Test: atest AccessibilityRepositoryTest
Test: atest KeyguardQuickAffordanceInteractorTest
Flag: EXEMPT bugfix
Change-Id: I0e44bed843f167a402a7b3b47a3df3cde9ab0d6f
parent 0ae551b0
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