[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
import android.accessibilityservice.AccessibilityService
import android.content.Intent
import android.view.accessibility.AccessibilityEvent
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.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
@ -21,9 +23,23 @@ class BitwardenAccessibilityService : AccessibilityService() {
@Inject
lateinit var processor: BitwardenAccessibilityProcessor
@Inject
lateinit var accessibilityEnabledManager: AccessibilityEnabledManager
override fun onAccessibilityEvent(event: AccessibilityEvent) {
processor.processAccessibilityEvent(event = event) { rootInActiveWindow }
}
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
@Provides
fun providesAccessibilityEnabledManager(
accessibilityManager: AccessibilityManager,
@ApplicationContext context: Context,
): AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl(
accessibilityManager = accessibilityManager,
context = context,
)
@Singleton

View File

@ -10,4 +10,9 @@ interface AccessibilityEnabledManager {
* Emits updates that track whether the accessibility autofill service is enabled..
*/
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
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.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -9,20 +10,20 @@ import kotlinx.coroutines.flow.asStateFlow
* The default implementation of [AccessibilityEnabledManager].
*/
class AccessibilityEnabledManagerImpl(
accessibilityManager: AccessibilityManager,
private val context: Context,
) : AccessibilityEnabledManager {
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(
value = accessibilityManager.isEnabled,
value = context.isAccessibilityServiceEnabled,
)
init {
accessibilityManager.addAccessibilityStateChangeListener(
AccessibilityManager.AccessibilityStateChangeListener { isEnabled ->
mutableIsAccessibilityEnabledStateFlow.value = isEnabled
},
)
mutableIsAccessibilityEnabledStateFlow.value = context.isAccessibilityServiceEnabled
}
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()
override fun refreshAccessibilityEnabledFromSettings() {
mutableIsAccessibilityEnabledStateFlow.value = context.isAccessibilityServiceEnabled
}
}

View File

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

View File

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

View File

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