Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ interface AccessibilityEnabledManager {
val isAccessibilityEnabledStateFlow: StateFlow<Boolean>

/**
* 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean>
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()

override fun refreshAccessibilityEnabledFromSettings() {
mutableIsAccessibilityEnabledStateFlow.value = context.isAccessibilityServiceEnabled
}
override var isAccessibilityServiceConnected: Boolean
get() = mutableIsAccessibilityEnabledStateFlow.value
set(value) {
mutableIsAccessibilityEnabledStateFlow.value = value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -28,6 +28,9 @@ class BitwardenAutofillTileService : TileService() {
@Inject
lateinit var accessibilityAutofillManager: AccessibilityAutofillManager

@Inject
lateinit var accessibilityEnabledManager: AccessibilityEnabledManager

override fun onClick() {
if (isLocked) {
unlockAndRun { launchAutofill() }
Expand All @@ -38,7 +41,7 @@ class BitwardenAutofillTileService : TileService() {

@SuppressLint("StartActivityAndCollapseDeprecated")
private fun launchAutofill() {
if (!applicationContext.isAccessibilityServiceEnabled) {
if (!accessibilityEnabledManager.isAccessibilityEnabledStateFlow.value) {
showDialog(getAccessibilityServiceRequiredDialog())
return
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ class FakeAccessibilityEnabledManager : AccessibilityEnabledManager {
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()

override fun refreshAccessibilityEnabledFromSettings() {
mutableIsAccessibilityEnabledStateFlow.value = isAccessibilityEnabled
}

var isAccessibilityEnabled: Boolean
override var isAccessibilityServiceConnected: Boolean
get() = mutableIsAccessibilityEnabledStateFlow.value
set(value) {
mutableIsAccessibilityEnabledStateFlow.value = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
Expand Down