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()) } }