[PM-14435] Accessibility enabled settings changes to address older and custom Android phone versions (#4756)

This commit is contained in:
aj-rosado 2025-02-28 22:25:11 +00:00 committed by GitHub
parent ec030f2c2e
commit ac6ff98041
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 67 additions and 38 deletions

View File

@ -1,8 +1,10 @@
package com.x8bit.bitwarden.data.autofill.accessibility package com.x8bit.bitwarden.data.autofill.accessibility
import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService
import android.content.Intent
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import androidx.annotation.Keep import androidx.annotation.Keep
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessor import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessor
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
@ -21,9 +23,23 @@ class BitwardenAccessibilityService : AccessibilityService() {
@Inject @Inject
lateinit var processor: BitwardenAccessibilityProcessor lateinit var processor: BitwardenAccessibilityProcessor
@Inject
lateinit var accessibilityEnabledManager: AccessibilityEnabledManager
override fun onAccessibilityEvent(event: AccessibilityEvent) { override fun onAccessibilityEvent(event: AccessibilityEvent) {
processor.processAccessibilityEvent(event = event) { rootInActiveWindow } processor.processAccessibilityEvent(event = event) { rootInActiveWindow }
} }
override fun onInterrupt() = Unit override fun onInterrupt() = Unit
override fun onUnbind(intent: Intent?): Boolean {
return super
.onUnbind(intent)
.also { accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings() }
}
override fun onServiceConnected() {
super.onServiceConnected()
accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings()
}
} }

View File

@ -57,10 +57,10 @@ object AccessibilityModule {
@Singleton @Singleton
@Provides @Provides
fun providesAccessibilityEnabledManager( fun providesAccessibilityEnabledManager(
accessibilityManager: AccessibilityManager, @ApplicationContext context: Context,
): AccessibilityEnabledManager = ): AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl( AccessibilityEnabledManagerImpl(
accessibilityManager = accessibilityManager, context = context,
) )
@Singleton @Singleton

View File

@ -10,4 +10,9 @@ interface AccessibilityEnabledManager {
* Emits updates that track whether the accessibility autofill service is enabled.. * Emits updates that track whether the accessibility autofill service is enabled..
*/ */
val isAccessibilityEnabledStateFlow: StateFlow<Boolean> val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
/**
* Gets the accessibility enabled state from the system settings.
*/
fun refreshAccessibilityEnabledFromSettings()
} }

View File

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.view.accessibility.AccessibilityManager import android.content.Context
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -9,20 +10,20 @@ import kotlinx.coroutines.flow.asStateFlow
* The default implementation of [AccessibilityEnabledManager]. * The default implementation of [AccessibilityEnabledManager].
*/ */
class AccessibilityEnabledManagerImpl( class AccessibilityEnabledManagerImpl(
accessibilityManager: AccessibilityManager, private val context: Context,
) : AccessibilityEnabledManager { ) : AccessibilityEnabledManager {
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow( private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(
value = accessibilityManager.isEnabled, value = context.isAccessibilityServiceEnabled,
) )
init { init {
accessibilityManager.addAccessibilityStateChangeListener( mutableIsAccessibilityEnabledStateFlow.value = context.isAccessibilityServiceEnabled
AccessibilityManager.AccessibilityStateChangeListener { isEnabled ->
mutableIsAccessibilityEnabledStateFlow.value = isEnabled
},
)
} }
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean> override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow() get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()
override fun refreshAccessibilityEnabledFromSettings() {
mutableIsAccessibilityEnabledStateFlow.value = context.isAccessibilityServiceEnabled
}
} }

View File

@ -1,46 +1,50 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.view.accessibility.AccessibilityManager import android.content.Context
import app.cash.turbine.test import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class AccessibilityEnabledManagerTest { class AccessibilityEnabledManagerTest {
private val context: Context = mockk()
private val accessibilityStateChangeListener = private lateinit var accessibilityEnabledManager: AccessibilityEnabledManager
slot<AccessibilityManager.AccessibilityStateChangeListener>()
private val accessibilityManager = mockk<AccessibilityManager> { @BeforeEach
every { isEnabled } returns false fun setUp() {
every { mockkStatic(Context::isAccessibilityServiceEnabled)
addAccessibilityStateChangeListener(capture(accessibilityStateChangeListener)) every { context.isAccessibilityServiceEnabled } returns false
} returns true accessibilityEnabledManager = AccessibilityEnabledManagerImpl(context)
} }
private val accessibilityEnabledManager: AccessibilityEnabledManager = @AfterEach
AccessibilityEnabledManagerImpl( fun tearDown() {
accessibilityManager = accessibilityManager, unmockkAll()
) }
@Suppress("MaxLineLength")
@Test @Test
fun `isAccessibilityEnabledStateFlow should emit whenever accessibilityStateChangeListener emits a unique value`() = fun `isAccessibilityEnabled returns false when setting does not contain our service`() =
runTest { runTest {
accessibilityEnabledManager.isAccessibilityEnabledStateFlow.test { every { context.isAccessibilityServiceEnabled } returns false
assertFalse(awaitItem()) accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings()
val result = accessibilityEnabledManager.isAccessibilityEnabledStateFlow.value
assertFalse(result)
}
accessibilityStateChangeListener.captured.onAccessibilityStateChanged(true) @Test
assertTrue(awaitItem()) fun `isAccessibilityEnabled returns true when setting contains the defined service`() =
runTest {
accessibilityStateChangeListener.captured.onAccessibilityStateChanged(true) every { context.isAccessibilityServiceEnabled } returns true
expectNoEvents() accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings()
val result = accessibilityEnabledManager.isAccessibilityEnabledStateFlow.value
accessibilityStateChangeListener.captured.onAccessibilityStateChanged(false) assertTrue(result)
assertFalse(awaitItem())
}
} }
} }

View File

@ -11,6 +11,10 @@ class FakeAccessibilityEnabledManager : AccessibilityEnabledManager {
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean> override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow() get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()
override fun refreshAccessibilityEnabledFromSettings() {
mutableIsAccessibilityEnabledStateFlow.value = isAccessibilityEnabled
}
var isAccessibilityEnabled: Boolean var isAccessibilityEnabled: Boolean
get() = mutableIsAccessibilityEnabledStateFlow.value get() = mutableIsAccessibilityEnabledStateFlow.value
set(value) { set(value) {

View File

@ -14,7 +14,6 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class ReviewPromptManagerTest { class ReviewPromptManagerTest {
private val autofillEnabledManager: AutofillEnabledManager = AutofillEnabledManagerImpl() private val autofillEnabledManager: AutofillEnabledManager = AutofillEnabledManagerImpl()
private val fakeAccessibilityEnabledManager = FakeAccessibilityEnabledManager() private val fakeAccessibilityEnabledManager = FakeAccessibilityEnabledManager()
private val fakeAuthDiskSource = FakeAuthDiskSource() private val fakeAuthDiskSource = FakeAuthDiskSource()