Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
@@ -1,18 +1,40 @@
package com.x8bit.bitwarden.data.autofill.accessibility.util

import android.accessibilityservice.AccessibilityServiceInfo
import android.content.Context
import android.provider.Settings
import android.view.accessibility.AccessibilityManager
import androidx.core.content.getSystemService
import com.x8bit.bitwarden.LEGACY_ACCESSIBILITY_SERVICE_NAME
import com.x8bit.bitwarden.LEGACY_SHORT_ACCESSIBILITY_SERVICE_NAME
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
import com.x8bit.bitwarden.data.autofill.util.containsAnyTerms

/**
* Helper method to determine if the [BitwardenAccessibilityService] is enabled.
*
* Uses [AccessibilityManager.getEnabledAccessibilityServiceList] as the primary check
* (required for Android 16+ where [Settings.Secure] is restricted for third-party apps),
* falling back to [Settings.Secure] string parsing for older Android versions.
*/
val Context.isAccessibilityServiceEnabled: Boolean
get() {
val appContext = this.applicationContext

// Primary check: AccessibilityManager API (Android 16+ compatible).
val isEnabledViaManager = appContext
.getSystemService<AccessibilityManager>()
?.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
?.any { service ->
val serviceInfo = service.resolveInfo?.serviceInfo
serviceInfo?.packageName == appContext.packageName &&
serviceInfo.name == BitwardenAccessibilityService::class.java.name
Comment thread
7heMech marked this conversation as resolved.
Outdated
}
?: false

if (isEnabledViaManager) return true
Comment thread
7heMech marked this conversation as resolved.

// Fallback: legacy Settings.Secure string parsing.
val packageName = appContext.packageName
val accessibilityServiceName = packageName?.let {
"$it/$LEGACY_ACCESSIBILITY_SERVICE_NAME"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.x8bit.bitwarden.data.autofill.accessibility.util

import android.accessibilityservice.AccessibilityServiceInfo
import android.content.Context
import android.content.pm.ResolveInfo
import android.content.pm.ServiceInfo
import android.provider.Settings
import android.view.accessibility.AccessibilityManager
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
Expand Down Expand Up @@ -30,6 +35,7 @@ class ContextExtensionsTest {
every { applicationContext } returns this
every { packageName } returns null
every { contentResolver } returns mockk()
every { getSystemService(AccessibilityManager::class.java) } returns null
}

assertFalse(context.isAccessibilityServiceEnabled)
Expand All @@ -41,6 +47,7 @@ class ContextExtensionsTest {
every { applicationContext } returns this
every { packageName } returns "com.x8bit.bitwarden"
every { contentResolver } returns mockk()
every { getSystemService(AccessibilityManager::class.java) } returns null
}
mockkSettingsSecureGetString(value = null)

Expand All @@ -53,6 +60,7 @@ class ContextExtensionsTest {
every { applicationContext } returns this
every { packageName } returns "com.x8bit.bitwarden"
every { contentResolver } returns mockk()
every { getSystemService(AccessibilityManager::class.java) } returns null
}
@Suppress("MaxLineLength")
mockkSettingsSecureGetString(
Expand All @@ -68,6 +76,7 @@ class ContextExtensionsTest {
every { applicationContext } returns this
every { packageName } returns "com.x8bit.bitwarden"
every { contentResolver } returns mockk()
every { getSystemService(AccessibilityManager::class.java) } returns null
}
mockkSettingsSecureGetString(
value = "com.x8bit.bitwarden/com.x8bit.bitwarden.Accessibility.AccessibilityService",
Expand All @@ -82,6 +91,7 @@ class ContextExtensionsTest {
every { applicationContext } returns this
every { packageName } returns "com.x8bit.bitwarden"
every { contentResolver } returns mockk()
every { getSystemService(AccessibilityManager::class.java) } returns null
}
mockkSettingsSecureGetString(
value = "com.x8bit.bitwarden/.Accessibility.AccessibilityService",
Expand All @@ -90,6 +100,133 @@ class ContextExtensionsTest {
assertTrue(context.isAccessibilityServiceEnabled)
}

@Test
fun `isAccessibilityServiceEnabled with matching enabled service returns true`() {
val context: Context = mockk {
every { applicationContext } returns this
every { packageName } returns "com.x8bit.bitwarden"
every { contentResolver } returns mockk()
every {
getSystemService(AccessibilityManager::class.java)
} returns mockkAccessibilityManager(
enabledServices = listOf(
createAccessibilityServiceInfo(
servicePackageName = "com.x8bit.bitwarden",
serviceName = BitwardenAccessibilityService::class.java.name,
),
),
)
}

assertTrue(context.isAccessibilityServiceEnabled)
}

@Suppress("MaxLineLength")
@Test
fun `isAccessibilityServiceEnabled with non-matching enabled service falls back to secure string`() {
val context: Context = mockk {
every { applicationContext } returns this
every { packageName } returns "com.x8bit.bitwarden"
every { contentResolver } returns mockk()
every {
getSystemService(AccessibilityManager::class.java)
} returns mockkAccessibilityManager(
enabledServices = listOf(
createAccessibilityServiceInfo(
servicePackageName = "com.other.app",
serviceName = "com.other.app.SomeService",
),
),
)
}
mockkSettingsSecureGetString(value = null)

assertFalse(context.isAccessibilityServiceEnabled)
}

@Suppress("MaxLineLength")
@Test
fun `isAccessibilityServiceEnabled with matching package but wrong service name falls back to secure string`() {
val context: Context = mockk {
every { applicationContext } returns this
every { packageName } returns "com.x8bit.bitwarden"
every { contentResolver } returns mockk()
every {
getSystemService(AccessibilityManager::class.java)
} returns mockkAccessibilityManager(
enabledServices = listOf(
createAccessibilityServiceInfo(
servicePackageName = "com.x8bit.bitwarden",
serviceName = "com.x8bit.bitwarden.SomeOtherService",
),
),
)
}
mockkSettingsSecureGetString(
value = "com.x8bit.bitwarden/com.x8bit.bitwarden.Accessibility.AccessibilityService",
)

assertTrue(context.isAccessibilityServiceEnabled)
}

@Test
fun `isAccessibilityServiceEnabled with empty enabled service list returns false`() {
val context: Context = mockk {
every { applicationContext } returns this
every { packageName } returns "com.x8bit.bitwarden"
every { contentResolver } returns mockk()
every {
getSystemService(AccessibilityManager::class.java)
} returns mockkAccessibilityManager(enabledServices = emptyList())
}
mockkSettingsSecureGetString(value = null)

assertFalse(context.isAccessibilityServiceEnabled)
}

@Test
fun `isAccessibilityServiceEnabled with null resolveInfo falls back to secure string`() {
val context: Context = mockk {
every { applicationContext } returns this
every { packageName } returns "com.x8bit.bitwarden"
every { contentResolver } returns mockk()
every {
getSystemService(AccessibilityManager::class.java)
} returns mockkAccessibilityManager(
enabledServices = listOf(
mockk<AccessibilityServiceInfo> {
every { resolveInfo } returns null
},
),
)
}
mockkSettingsSecureGetString(value = null)

assertFalse(context.isAccessibilityServiceEnabled)
}

private fun mockkAccessibilityManager(
enabledServices: List<AccessibilityServiceInfo>,
): AccessibilityManager =
mockk {
every {
getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
} returns enabledServices
}

private fun createAccessibilityServiceInfo(
servicePackageName: String,
serviceName: String,
): AccessibilityServiceInfo =
mockk {
every { resolveInfo } returns ResolveInfo().apply {
serviceInfo = ServiceInfo().apply {
packageName = servicePackageName
name = serviceName
}
}
}

private fun mockkSettingsSecureGetString(value: String?) {
every {
Settings.Secure.getString(any(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
Expand Down