PM-14409: Add realtime check for when the accessibility service is enabled or disabled (#4314)

This commit is contained in:
David Perez 2024-11-15 16:15:19 -06:00 committed by GitHub
parent a04598c77a
commit 30eb11b85e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 68 additions and 188 deletions

View file

@ -15,7 +15,6 @@ import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
@ -39,9 +38,6 @@ class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels() private val mainViewModel: MainViewModel by viewModels()
@Inject
lateinit var accessibilityActivityManager: AccessibilityActivityManager
@Inject @Inject
lateinit var autofillActivityManager: AutofillActivityManager lateinit var autofillActivityManager: AutofillActivityManager

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.di
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.PowerManager import android.os.PowerManager
import android.view.accessibility.AccessibilityManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
@ -55,8 +56,12 @@ object AccessibilityModule {
@Singleton @Singleton
@Provides @Provides
fun providesAccessibilityEnabledManager(): AccessibilityEnabledManager = fun providesAccessibilityEnabledManager(
AccessibilityEnabledManagerImpl() accessibilityManager: AccessibilityManager,
): AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl(
accessibilityManager = accessibilityManager,
)
@Singleton @Singleton
@Provides @Provides
@ -110,6 +115,12 @@ object AccessibilityModule {
@ApplicationContext context: Context, @ApplicationContext context: Context,
): PackageManager = context.packageManager ): PackageManager = context.packageManager
@Singleton
@Provides
fun provideAccessibilityManager(
@ApplicationContext context: Context,
): AccessibilityManager = context.getSystemService(AccessibilityManager::class.java)
@Singleton @Singleton
@Provides @Provides
fun providesPowerManager( fun providesPowerManager(

View file

@ -1,36 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.di
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
/**
* Provides dependencies within the accessibility package scoped to the activity.
*/
@Module
@InstallIn(ActivityComponent::class)
object ActivityAccessibilityModule {
@ActivityScoped
@Provides
fun providesAccessibilityActivityManager(
@ApplicationContext context: Context,
accessibilityEnabledManager: AccessibilityEnabledManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
): AccessibilityActivityManager =
AccessibilityActivityManagerImpl(
context = context,
accessibilityEnabledManager = accessibilityEnabledManager,
appStateManager = appStateManager,
lifecycleScope = lifecycleScope,
)
}

View file

@ -1,10 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.app.Activity
/**
* A helper for dealing with accessibility configuration that must be scoped to a specific
* [Activity]. In particular, this should be injected into an [Activity] to ensure that the
* [AccessibilityEnabledManager] reports correct values.
*/
interface AccessibilityActivityManager

View file

@ -1,28 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
/**
* The default implementation of the [AccessibilityActivityManager].
*/
class AccessibilityActivityManagerImpl(
private val context: Context,
private val accessibilityEnabledManager: AccessibilityEnabledManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
) : AccessibilityActivityManager {
init {
appStateManager
.appForegroundStateFlow
.onEach {
accessibilityEnabledManager.isAccessibilityEnabled =
context.isAccessibilityServiceEnabled
}
.launchIn(lifecycleScope)
}
}

View file

@ -7,15 +7,7 @@ import kotlinx.coroutines.flow.StateFlow
*/ */
interface AccessibilityEnabledManager { interface AccessibilityEnabledManager {
/** /**
* Whether or not the accessibility service should be considered enabled. * Emits updates that track whether the accessibility autofill service is enabled..
*
* Note that changing this does not enable or disable autofill; it is only an indicator that
* this has occurred elsewhere.
*/
var isAccessibilityEnabled: Boolean
/**
* Emits updates that track [isAccessibilityEnabled] values.
*/ */
val isAccessibilityEnabledStateFlow: StateFlow<Boolean> val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.view.accessibility.AccessibilityManager
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -7,14 +8,18 @@ import kotlinx.coroutines.flow.asStateFlow
/** /**
* The default implementation of [AccessibilityEnabledManager]. * The default implementation of [AccessibilityEnabledManager].
*/ */
class AccessibilityEnabledManagerImpl : AccessibilityEnabledManager { class AccessibilityEnabledManagerImpl(
accessibilityManager: AccessibilityManager,
) : AccessibilityEnabledManager {
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false) private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false)
override var isAccessibilityEnabled: Boolean init {
get() = mutableIsAccessibilityEnabledStateFlow.value accessibilityManager.addAccessibilityStateChangeListener(
set(value) { AccessibilityManager.AccessibilityStateChangeListener { isEnabled ->
mutableIsAccessibilityEnabledStateFlow.value = value mutableIsAccessibilityEnabledStateFlow.value = isEnabled
} },
)
}
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean> override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow() get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()

View file

@ -1,81 +0,0 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import app.cash.turbine.test
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
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
@OptIn(ExperimentalCoroutinesApi::class)
class AccessibilityActivityManagerTest {
private val context: Context = mockk()
private val accessibilityEnabledManager: AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl()
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
private val appStateManager: AppStateManager = mockk {
every { appForegroundStateFlow } returns mutableAppForegroundStateFlow
}
private val lifecycleScope = mockk<LifecycleCoroutineScope> {
every { coroutineContext } returns UnconfinedTestDispatcher()
}
// We will construct an instance here just to hook the various dependencies together internally
private lateinit var autofillActivityManager: AccessibilityActivityManager
@BeforeEach
fun setup() {
mockkStatic(Context::isAccessibilityServiceEnabled)
every { context.isAccessibilityServiceEnabled } returns false
autofillActivityManager = AccessibilityActivityManagerImpl(
context = context,
accessibilityEnabledManager = accessibilityEnabledManager,
appStateManager = appStateManager,
lifecycleScope = lifecycleScope,
)
}
@AfterEach
fun tearDown() {
unmockkStatic(Context::isAccessibilityServiceEnabled)
}
@Test
fun `changes in app foreground status should update the AutofillEnabledManager as necessary`() =
runTest {
accessibilityEnabledManager.isAccessibilityEnabledStateFlow.test {
assertFalse(awaitItem())
// An update is received when both the accessibility state and foreground state
// change
every { context.isAccessibilityServiceEnabled } returns true
mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED
assertTrue(awaitItem())
// An update is not received when only the foreground state changes
mutableAppForegroundStateFlow.value = AppForegroundState.BACKGROUNDED
expectNoEvents()
// An update is not received when only the accessibility state changes
every { context.isAccessibilityServiceEnabled } returns false
expectNoEvents()
// An update is received after both states have changed
mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED
assertFalse(awaitItem())
}
}
}

View file

@ -1,6 +1,10 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.view.accessibility.AccessibilityManager
import app.cash.turbine.test import app.cash.turbine.test
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
@ -8,23 +12,33 @@ import org.junit.jupiter.api.Test
class AccessibilityEnabledManagerTest { class AccessibilityEnabledManagerTest {
private val accessibilityStateChangeListener =
slot<AccessibilityManager.AccessibilityStateChangeListener>()
private val accessibilityManager = mockk<AccessibilityManager> {
every {
addAccessibilityStateChangeListener(capture(accessibilityStateChangeListener))
} returns true
}
private val accessibilityEnabledManager: AccessibilityEnabledManager = private val accessibilityEnabledManager: AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl() AccessibilityEnabledManagerImpl(
accessibilityManager = accessibilityManager,
)
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `isAccessibilityEnabledStateFlow should emit whenever isAccessibilityEnabled is set to a unique value`() = fun `isAccessibilityEnabledStateFlow should emit whenever accessibilityStateChangeListener emits a unique value`() =
runTest { runTest {
accessibilityEnabledManager.isAccessibilityEnabledStateFlow.test { accessibilityEnabledManager.isAccessibilityEnabledStateFlow.test {
assertFalse(awaitItem()) assertFalse(awaitItem())
accessibilityEnabledManager.isAccessibilityEnabled = true accessibilityStateChangeListener.captured.onAccessibilityStateChanged(true)
assertTrue(awaitItem()) assertTrue(awaitItem())
accessibilityEnabledManager.isAccessibilityEnabled = true accessibilityStateChangeListener.captured.onAccessibilityStateChanged(true)
expectNoEvents() expectNoEvents()
accessibilityEnabledManager.isAccessibilityEnabled = false accessibilityStateChangeListener.captured.onAccessibilityStateChanged(false)
assertFalse(awaitItem()) assertFalse(awaitItem())
} }
} }

View file

@ -0,0 +1,19 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeAccessibilityEnabledManager : AccessibilityEnabledManager {
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false)
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()
var isAccessibilityEnabled: Boolean
get() = mutableIsAccessibilityEnabledStateFlow.value
set(value) {
mutableIsAccessibilityEnabledStateFlow.value = value
}
}

View file

@ -12,8 +12,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.FakeAccessibilityEnabledManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
@ -55,8 +54,7 @@ class SettingsRepositoryTest {
every { disableAutofillServices() } just runs every { disableAutofillServices() } just runs
} }
private val autofillEnabledManager: AutofillEnabledManager = AutofillEnabledManagerImpl() private val autofillEnabledManager: AutofillEnabledManager = AutofillEnabledManagerImpl()
private val accessibilityEnabledManager: AccessibilityEnabledManager = private val fakeAccessibilityEnabledManager = FakeAccessibilityEnabledManager()
AccessibilityEnabledManagerImpl()
private val fakeAuthDiskSource = FakeAuthDiskSource() private val fakeAuthDiskSource = FakeAuthDiskSource()
private val fakeSettingsDiskSource = FakeSettingsDiskSource() private val fakeSettingsDiskSource = FakeSettingsDiskSource()
private val vaultSdkSource: VaultSdkSource = mockk() private val vaultSdkSource: VaultSdkSource = mockk()
@ -75,7 +73,7 @@ class SettingsRepositoryTest {
settingsDiskSource = fakeSettingsDiskSource, settingsDiskSource = fakeSettingsDiskSource,
vaultSdkSource = vaultSdkSource, vaultSdkSource = vaultSdkSource,
biometricsEncryptionManager = biometricsEncryptionManager, biometricsEncryptionManager = biometricsEncryptionManager,
accessibilityEnabledManager = accessibilityEnabledManager, accessibilityEnabledManager = fakeAccessibilityEnabledManager,
dispatcherManager = FakeDispatcherManager(), dispatcherManager = FakeDispatcherManager(),
policyManager = policyManager, policyManager = policyManager,
) )
@ -691,10 +689,10 @@ class SettingsRepositoryTest {
settingsRepository.isAccessibilityEnabledStateFlow.test { settingsRepository.isAccessibilityEnabledStateFlow.test {
assertFalse(awaitItem()) assertFalse(awaitItem())
accessibilityEnabledManager.isAccessibilityEnabled = true fakeAccessibilityEnabledManager.isAccessibilityEnabled = true
assertTrue(awaitItem()) assertTrue(awaitItem())
accessibilityEnabledManager.isAccessibilityEnabled = false fakeAccessibilityEnabledManager.isAccessibilityEnabled = false
assertFalse(awaitItem()) assertFalse(awaitItem())
} }
} }