From 7f3b8d49a2f2dbb753aa308156b9184439c9e99a Mon Sep 17 00:00:00 2001 From: 7heMech <83923848+7heMech@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:47:18 +0300 Subject: [PATCH] bug: Track accessibility service connection state from the service Android 16 no longer reports this app's own accessibility service back to the app: neither Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES nor AccessibilityManager.getEnabledAccessibilityServiceList returns it, so isAccessibilityServiceEnabled always answered false. The setting reads as an empty string and the enabled service list is empty, while the same values are fully populated for adb and for an unrelated app on the same device. The service knows when it is connected, so record that instead of asking the platform. AccessibilityEnabledManager exposes isAccessibilityServiceConnected, set from onServiceConnected and onUnbind, and BitwardenAutofillTileService reads the manager rather than querying the platform on every tap. isAccessibilityServiceEnabled remains the initial value so behaviour before Android 16 is unchanged. --- .../BitwardenAccessibilityService.kt | 12 +--- .../manager/AccessibilityEnabledManager.kt | 9 ++- .../AccessibilityEnabledManagerImpl.kt | 16 ++--- .../tiles/BitwardenAutofillTileService.kt | 7 ++- .../AccessibilityEnabledManagerTest.kt | 61 ++++++++++++++++--- .../FakeAccessibilityEnabledManager.kt | 6 +- .../manager/ReviewPromptManagerTest.kt | 6 +- .../repository/SettingsRepositoryTest.kt | 4 +- 8 files changed, 80 insertions(+), 41 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/BitwardenAccessibilityService.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/BitwardenAccessibilityService.kt index b665680e9c0..ee529b725b6 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/BitwardenAccessibilityService.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/BitwardenAccessibilityService.kt @@ -32,19 +32,13 @@ class BitwardenAccessibilityService : AccessibilityService() { override fun onInterrupt() = Unit - override fun onCreate() { - super.onCreate() - accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings() - } - override fun onUnbind(intent: Intent?): Boolean { - return super - .onUnbind(intent) - .also { accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings() } + accessibilityEnabledManager.isAccessibilityServiceConnected = false + return super.onUnbind(intent) } override fun onServiceConnected() { super.onServiceConnected() - accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings() + accessibilityEnabledManager.isAccessibilityServiceConnected = true } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManager.kt index ec89e4dac9f..d1aaf33e967 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManager.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManager.kt @@ -12,7 +12,12 @@ interface AccessibilityEnabledManager { val isAccessibilityEnabledStateFlow: StateFlow /** - * Gets the accessibility enabled state from the system settings. + * Whether this app's accessibility service is currently connected. + * + * The service reports its own connection state because the platform cannot be asked: from + * Android 16 onwards neither `Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES` nor + * `AccessibilityManager.getEnabledAccessibilityServiceList` reveals this app's own service to + * the app itself, so any check based on them always answers false. */ - fun refreshAccessibilityEnabledFromSettings() + var isAccessibilityServiceConnected: Boolean } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerImpl.kt index 937b9308b91..3d56de862c7 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerImpl.kt @@ -10,20 +10,20 @@ import kotlinx.coroutines.flow.asStateFlow * The default implementation of [AccessibilityEnabledManager]. */ class AccessibilityEnabledManagerImpl( - private val context: Context, + context: Context, ) : AccessibilityEnabledManager { + // Seeded from the platform so the state is correct before the service connects; on platforms + // that no longer report our own service this is false until the service reports itself. private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow( value = context.isAccessibilityServiceEnabled, ) - init { - mutableIsAccessibilityEnabledStateFlow.value = context.isAccessibilityServiceEnabled - } - override val isAccessibilityEnabledStateFlow: StateFlow get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow() - override fun refreshAccessibilityEnabledFromSettings() { - mutableIsAccessibilityEnabledStateFlow.value = context.isAccessibilityServiceEnabled - } + override var isAccessibilityServiceConnected: Boolean + get() = mutableIsAccessibilityEnabledStateFlow.value + set(value) { + mutableIsAccessibilityEnabledStateFlow.value = value + } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/tiles/BitwardenAutofillTileService.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/tiles/BitwardenAutofillTileService.kt index e01f15cfeef..844fc828fd9 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/tiles/BitwardenAutofillTileService.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/tiles/BitwardenAutofillTileService.kt @@ -13,8 +13,8 @@ import com.bitwarden.core.util.isBuildVersionAtLeast import com.bitwarden.ui.platform.resource.BitwardenString import com.x8bit.bitwarden.AccessibilityActivity import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager +import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction -import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -28,6 +28,9 @@ class BitwardenAutofillTileService : TileService() { @Inject lateinit var accessibilityAutofillManager: AccessibilityAutofillManager + @Inject + lateinit var accessibilityEnabledManager: AccessibilityEnabledManager + override fun onClick() { if (isLocked) { unlockAndRun { launchAutofill() } @@ -38,7 +41,7 @@ class BitwardenAutofillTileService : TileService() { @SuppressLint("StartActivityAndCollapseDeprecated") private fun launchAutofill() { - if (!applicationContext.isAccessibilityServiceEnabled) { + if (!accessibilityEnabledManager.isAccessibilityEnabledStateFlow.value) { showDialog(getAccessibilityServiceRequiredDialog()) return } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerTest.kt index b898947b800..f699fd8efeb 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerTest.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.manager 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.mockk @@ -31,20 +32,60 @@ class AccessibilityEnabledManagerTest { } @Test - fun `isAccessibilityEnabled returns false when setting does not contain our service`() = - runTest { - every { context.isAccessibilityServiceEnabled } returns false - accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings() - val result = accessibilityEnabledManager.isAccessibilityEnabledStateFlow.value - assertFalse(result) - } + fun `isAccessibilityEnabled is false when the service has not connected`() = runTest { + val result = accessibilityEnabledManager.isAccessibilityEnabledStateFlow.value + + assertFalse(result) + } @Test - fun `isAccessibilityEnabled returns true when setting contains the defined service`() = + fun `isAccessibilityEnabled is seeded from the platform when it reports the service`() = runTest { every { context.isAccessibilityServiceEnabled } returns true - accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings() - val result = accessibilityEnabledManager.isAccessibilityEnabledStateFlow.value + + val result = AccessibilityEnabledManagerImpl(context) + .isAccessibilityEnabledStateFlow + .value + assertTrue(result) } + + @Test + fun `isAccessibilityEnabled is true when the service reports it has connected`() = runTest { + accessibilityEnabledManager.isAccessibilityServiceConnected = true + + assertTrue(accessibilityEnabledManager.isAccessibilityEnabledStateFlow.value) + } + + @Test + fun `isAccessibilityEnabled is false when the service reports it has disconnected`() = + runTest { + accessibilityEnabledManager.isAccessibilityServiceConnected = true + accessibilityEnabledManager.isAccessibilityServiceConnected = false + + assertFalse(accessibilityEnabledManager.isAccessibilityEnabledStateFlow.value) + } + + @Test + fun `isAccessibilityEnabledStateFlow emits when the service connection state changes`() = + runTest { + accessibilityEnabledManager.isAccessibilityEnabledStateFlow.test { + assertFalse(awaitItem()) + + accessibilityEnabledManager.isAccessibilityServiceConnected = true + assertTrue(awaitItem()) + + accessibilityEnabledManager.isAccessibilityServiceConnected = false + assertFalse(awaitItem()) + } + } + + @Test + fun `isAccessibilityServiceConnected reflects the current enabled state`() = runTest { + assertFalse(accessibilityEnabledManager.isAccessibilityServiceConnected) + + accessibilityEnabledManager.isAccessibilityServiceConnected = true + + assertTrue(accessibilityEnabledManager.isAccessibilityServiceConnected) + } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/FakeAccessibilityEnabledManager.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/FakeAccessibilityEnabledManager.kt index 264de55192a..434c60154e2 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/FakeAccessibilityEnabledManager.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/accessibility/manager/FakeAccessibilityEnabledManager.kt @@ -11,11 +11,7 @@ class FakeAccessibilityEnabledManager : AccessibilityEnabledManager { override val isAccessibilityEnabledStateFlow: StateFlow get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow() - override fun refreshAccessibilityEnabledFromSettings() { - mutableIsAccessibilityEnabledStateFlow.value = isAccessibilityEnabled - } - - var isAccessibilityEnabled: Boolean + override var isAccessibilityServiceConnected: Boolean get() = mutableIsAccessibilityEnabledStateFlow.value set(value) { mutableIsAccessibilityEnabledStateFlow.value = value diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/ReviewPromptManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/ReviewPromptManagerTest.kt index 12d85a4f944..c56200f80c7 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/ReviewPromptManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/ReviewPromptManagerTest.kt @@ -111,7 +111,7 @@ class ReviewPromptManagerTest { @Test fun `shouldPromptForAppReview should return true if one auto fill service is enabled and one actions requirement is met`() { fakeAuthDiskSource.userState = MOCK_USER_STATE - fakeAccessibilityEnabledManager.isAccessibilityEnabled = true + fakeAccessibilityEnabledManager.isAccessibilityServiceConnected = true autofillEnabledManager.isAutofillEnabled = false fakeSettingsDiskSource.storeGeneratedResultActionCount(count = 0) fakeSettingsDiskSource.storeCreateSendActionCount(count = 0) @@ -122,7 +122,7 @@ class ReviewPromptManagerTest { @Test fun `shouldPromptForAppReview should return false if no auto fill service is enabled`() { fakeAuthDiskSource.userState = MOCK_USER_STATE - fakeAccessibilityEnabledManager.isAccessibilityEnabled = false + fakeAccessibilityEnabledManager.isAccessibilityServiceConnected = false autofillEnabledManager.isAutofillEnabled = false fakeSettingsDiskSource.storeGeneratedResultActionCount(count = 0) fakeSettingsDiskSource.storeCreateSendActionCount(count = 0) @@ -133,7 +133,7 @@ class ReviewPromptManagerTest { @Test fun `shouldPromptForAppReview should return false if no action count is met`() { fakeAuthDiskSource.userState = MOCK_USER_STATE - fakeAccessibilityEnabledManager.isAccessibilityEnabled = true + fakeAccessibilityEnabledManager.isAccessibilityServiceConnected = true autofillEnabledManager.isAutofillEnabled = true fakeSettingsDiskSource.storeGeneratedResultActionCount(count = 1) fakeSettingsDiskSource.storeCreateSendActionCount(count = 0) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt index 97afc9e6ceb..29dd7788b71 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt @@ -786,10 +786,10 @@ class SettingsRepositoryTest { settingsRepository.isAccessibilityEnabledStateFlow.test { assertFalse(awaitItem()) - fakeAccessibilityEnabledManager.isAccessibilityEnabled = true + fakeAccessibilityEnabledManager.isAccessibilityServiceConnected = true assertTrue(awaitItem()) - fakeAccessibilityEnabledManager.isAccessibilityEnabled = false + fakeAccessibilityEnabledManager.isAccessibilityServiceConnected = false assertFalse(awaitItem()) } }