Skip to content
Draft
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 @@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.autofill.manager.FillAssistManager
import com.x8bit.bitwarden.data.autofill.manager.FillAssistManagerImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -44,6 +45,7 @@ object FillAssistModule {
fillAssistDiskSource: FillAssistDiskSource,
featureFlagManager: FeatureFlagManager,
serverConfigRepository: ServerConfigRepository,
settingsRepository: SettingsRepository,
environmentDiskSource: EnvironmentDiskSource,
clock: Clock,
dispatcherManager: DispatcherManager,
Expand All @@ -53,6 +55,7 @@ object FillAssistModule {
fillAssistDiskSource = fillAssistDiskSource,
featureFlagManager = featureFlagManager,
serverConfigRepository = serverConfigRepository,
settingsRepository = settingsRepository,
environmentDiskSource = environmentDiskSource,
clock = clock,
dispatcherManager = dispatcherManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.autofill.model.FillAssistRules
import com.x8bit.bitwarden.data.autofill.model.FillAssistRules.SelectorClause
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filterNotNull
Expand Down Expand Up @@ -57,6 +58,7 @@ class FillAssistManagerImpl(
private val fillAssistDiskSource: FillAssistDiskSource,
private val featureFlagManager: FeatureFlagManager,
private val serverConfigRepository: ServerConfigRepository,
private val settingsRepository: SettingsRepository,
private val environmentDiskSource: EnvironmentDiskSource,
private val clock: Clock,
dispatcherManager: DispatcherManager,
Expand All @@ -78,7 +80,11 @@ class FillAssistManagerImpl(
}

override fun syncIfNecessary() {
if (!featureFlagManager.getFeatureFlag(FlagKey.FillAssistTargetingRules)) return
if (!featureFlagManager.getFeatureFlag(FlagKey.FillAssistTargetingRules) ||
!settingsRepository.isFillAssistEnabled
) {
return
}
val serverUrl = serverConfigRepository
.serverConfigStateFlow
.value
Expand All @@ -87,8 +93,11 @@ class FillAssistManagerImpl(
?.fillAssistRulesUrl
?: return
val lastFetch = fillAssistDiskSource.getLastFetchTimestamp(serverUrl) ?: 0L
if (clock.millis() - lastFetch < UPDATE_INTERVAL_MS) return
if (!syncJob.isCompleted) return
if (clock.millis() - lastFetch < UPDATE_INTERVAL_MS ||
!syncJob.isCompleted
) {
return
}
syncJob = ioScope.launch { sync(serverUrl) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,10 @@ class AutofillParserImpl(
// over heuristics; unmatched view nodes are excluded entirely (no heuristic fallback).
val fillAssistHostRules = uri
?.takeUnless { it.startsWith("androidapp://") }?.toUri()?.host
?.takeIf { featureFlagManager.getFeatureFlag(FlagKey.FillAssistTargetingRules) }
?.takeIf {
featureFlagManager.getFeatureFlag(FlagKey.FillAssistTargetingRules) &&
settingsRepository.isFillAssistEnabled
}
?.let { host ->
fillAssistManager.getFillAssistRules()?.hostRules?.get(host.removePrefix("www."))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,16 @@ interface SettingsDiskSource : FlightRecorderDiskSource {
*/
fun storeInlineAutofillEnabled(userId: String, isInlineAutofillEnabled: Boolean?)

/**
* Gets the value determining if fill assist is enabled for the given [userId].
*/
fun getFillAssistEnabled(userId: String): Boolean?

/**
* Stores the given [isFillAssistEnabled] value for the given [userId].
*/
fun storeFillAssistEnabled(userId: String, isFillAssistEnabled: Boolean?)

/**
* Gets a list of blocked autofill URI's for the given [userId].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ private const val APP_LANGUAGE_KEY = "appLocale"
private const val APP_THEME_KEY = "theme"
private const val PULL_TO_REFRESH_KEY = "syncOnRefresh"
private const val INLINE_AUTOFILL_ENABLED_KEY = "inlineAutofillEnabled"
private const val FILL_ASSIST_ENABLED_KEY = "fillAssistEnabled"
private const val BLOCKED_AUTOFILL_URIS_KEY = "autofillBlacklistedUris"
private const val VAULT_LAST_SYNC_TIME = "vaultLastSyncTime"
private const val VAULT_TIMEOUT_ACTION_KEY = "vaultTimeoutAction"
Expand Down Expand Up @@ -543,6 +544,16 @@ class SettingsDiskSourceImpl(
)
}

override fun getFillAssistEnabled(userId: String): Boolean? =
getBoolean(key = FILL_ASSIST_ENABLED_KEY.appendIdentifier(userId))

override fun storeFillAssistEnabled(userId: String, isFillAssistEnabled: Boolean?) {
putBoolean(
key = FILL_ASSIST_ENABLED_KEY.appendIdentifier(userId),
value = isFillAssistEnabled,
)
}

override fun getBlockedAutofillUris(userId: String): List<String>? =
getString(key = BLOCKED_AUTOFILL_URIS_KEY.appendIdentifier(userId))?.let {
json.decodeFromStringOrNull(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ interface SettingsRepository : FlightRecorderManager {
*/
var isInlineAutofillEnabled: Boolean

/**
* Whether fill assist is enabled for the current user.
*/
var isFillAssistEnabled: Boolean

/**
* Whether the auto copying totp when autofilling is disabled for the current user.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private val DEFAULT_IS_SCREEN_CAPTURE_ALLOWED = BuildConfig.DEBUG
/**
* Primary implementation of [SettingsRepository].
*/
@Suppress("TooManyFunctions", "LongParameterList")
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
class SettingsRepositoryImpl(
private val autofillManager: AutofillManager,
private val autofillEnabledManager: AutofillEnabledManager,
Expand Down Expand Up @@ -314,6 +314,18 @@ class SettingsRepositoryImpl(
)
}

override var isFillAssistEnabled: Boolean
get() = activeUserId
?.let { settingsDiskSource.getFillAssistEnabled(userId = it) }
?: false
set(value) {
val userId = activeUserId ?: return
settingsDiskSource.storeFillAssistEnabled(
userId = userId,
isFillAssistEnabled = value,
)
}

override var isAutoCopyTotpDisabled: Boolean
get() = activeUserId
?.let { settingsDiskSource.getAutoCopyTotpDisabled(userId = it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ fun AutoFillScreen(
onNavigateToPrivilegedAppsList()
}

AutoFillEvent.NavigateToFillAssistHelp -> {
intentManager.launchUri(
"https://bitwarden.com/help/fill-assist/".toUri(),
)
}

AutoFillEvent.NavigateToLearnMore -> {
intentManager.launchUri("https://bitwarden.com/help/uri-match-detection/".toUri())
}
Expand Down Expand Up @@ -301,6 +307,29 @@ private fun AutoFillScreenContent(
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
if (state.showFillAssistOption) {
Spacer(modifier = Modifier.height(8.dp))
BitwardenSwitch(
label = stringResource(id = BitwardenString.turn_on_fill_assist),
supportingText = stringResource(
id = BitwardenString.fill_assist_improves_autofill_accuracy_on_supported_sites,
),
isChecked = state.isFillAssistEnabled,
onCheckedChange = autoFillHandlers.onFillAssistToggleClick,
helpData = BitwardenHelpButtonData(
onClick = autoFillHandlers.onFillAssistInfoClick,
contentDescription = stringResource(
id = BitwardenString.learn_more,
),
isExternalLink = true,
),
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("FillAssistSwitch")
.standardHorizontalMargin(),
)
}
Spacer(modifier = Modifier.height(8.dp))
BitwardenSwitch(
label = stringResource(id = BitwardenString.copy_totp_automatically),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import android.os.Build
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.manager.FillAssistManager
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
Expand All @@ -33,11 +36,13 @@ private const val KEY_STATE = "state"
/**
* View model for the auto-fill screen.
*/
@Suppress("TooManyFunctions")
@Suppress("LongParameterList", "TooManyFunctions")
@HiltViewModel
class AutoFillViewModel @Inject constructor(
authRepository: AuthRepository,
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
featureFlagManager: FeatureFlagManager,
private val fillAssistManager: FillAssistManager,
private val savedStateHandle: SavedStateHandle,
private val settingsRepository: SettingsRepository,
private val firstTimeActionManager: FirstTimeActionManager,
Expand All @@ -47,6 +52,9 @@ class AutoFillViewModel @Inject constructor(
val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId
val firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState
AutoFillState(
showFillAssistOption = featureFlagManager
.getFeatureFlag(FlagKey.FillAssistTargetingRules),
isFillAssistEnabled = settingsRepository.isFillAssistEnabled,
isAskToAddLoginEnabled = !settingsRepository.isAutofillSavePromptDisabled,
isAccessibilityAutofillEnabled = settingsRepository
.isAccessibilityEnabledStateFlow
Expand Down Expand Up @@ -109,6 +117,8 @@ class AutoFillViewModel @Inject constructor(
}

override fun handleAction(action: AutoFillAction) = when (action) {
is AutoFillAction.FillAssistToggleClick -> handleFillAssistToggleClick(action)
AutoFillAction.FillAssistInfoClick -> handleFillAssistInfoClick()
is AutoFillAction.AskToAddLoginClick -> handleAskToAddLoginClick(action)
is AutoFillAction.AutoFillServicesClick -> handleAutoFillServicesClick(action)
AutoFillAction.BackClick -> handleBackClick()
Expand Down Expand Up @@ -215,6 +225,18 @@ class AutoFillViewModel @Inject constructor(
}
}

private fun handleFillAssistToggleClick(action: AutoFillAction.FillAssistToggleClick) {
settingsRepository.isFillAssistEnabled = action.isEnabled
mutableStateFlow.update { it.copy(isFillAssistEnabled = action.isEnabled) }
if (action.isEnabled) {
fillAssistManager.syncIfNecessary()
}
}

private fun handleFillAssistInfoClick() {
sendEvent(AutoFillEvent.NavigateToFillAssistHelp)
}

private fun handleAskToAddLoginClick(action: AutoFillAction.AskToAddLoginClick) {
settingsRepository.isAutofillSavePromptDisabled = !action.isEnabled
mutableStateFlow.update { it.copy(isAskToAddLoginEnabled = action.isEnabled) }
Expand Down Expand Up @@ -293,6 +315,8 @@ class AutoFillViewModel @Inject constructor(
*/
@Parcelize
data class AutoFillState(
val showFillAssistOption: Boolean,
val isFillAssistEnabled: Boolean,
val isAskToAddLoginEnabled: Boolean,
val isAccessibilityAutofillEnabled: Boolean,
val isAutoFillServicesEnabled: Boolean,
Expand Down Expand Up @@ -422,12 +446,29 @@ sealed class AutoFillEvent {
* Navigate to the autofill help page.
*/
data object NavigateToAutofillHelp : AutoFillEvent()

/**
* Navigate to the fill assist help page.
*/
data object NavigateToFillAssistHelp : AutoFillEvent()
}

/**
* Models actions for the auto-fill screen.
*/
sealed class AutoFillAction {
/**
* User toggled the fill assist switch.
*/
data class FillAssistToggleClick(
val isEnabled: Boolean,
) : AutoFillAction()

/**
* User clicked the fill assist info icon.
*/
data object FillAssistInfoClick : AutoFillAction()

/**
* User clicked ask to add login button.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class AutoFillHandlers(
val onAskToAddLoginClick: (isEnabled: Boolean) -> Unit,
val onDefaultUriMatchTypeSelect: (defaultUriMatchType: UriMatchType) -> Unit,
val onBlockAutoFillClick: () -> Unit,
val onFillAssistToggleClick: (isEnabled: Boolean) -> Unit,
val onFillAssistInfoClick: () -> Unit,
val onLearnMoreClick: () -> Unit,
val onHelpCardClick: () -> Unit,
) {
Expand Down Expand Up @@ -82,6 +84,12 @@ class AutoFillHandlers(
viewModel.trySendAction(AutoFillAction.DefaultUriMatchTypeSelect(it))
},
onBlockAutoFillClick = { viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) },
onFillAssistToggleClick = {
viewModel.trySendAction(AutoFillAction.FillAssistToggleClick(it))
},
onFillAssistInfoClick = {
viewModel.trySendAction(AutoFillAction.FillAssistInfoClick)
},
onLearnMoreClick = { viewModel.trySendAction(AutoFillAction.LearnMoreClick) },
onHelpCardClick = { viewModel.trySendAction(AutoFillAction.HelpCardClick) },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.autofill.datasource.disk.FillAssistDiskSource
import com.x8bit.bitwarden.data.autofill.model.FillAssistRules
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
Expand Down Expand Up @@ -75,11 +76,16 @@ class FillAssistManagerTest {
every { fillAssistRulesUrl = any() } just runs
}

private val settingsRepository: SettingsRepository = mockk {
every { isFillAssistEnabled } returns true
}

private val manager = FillAssistManagerImpl(
fillAssistService = fillAssistService,
fillAssistDiskSource = fillAssistDiskSource,
featureFlagManager = featureFlagManager,
serverConfigRepository = serverConfigRepository,
settingsRepository = settingsRepository,
environmentDiskSource = environmentDiskSource,
clock = FIXED_CLOCK,
dispatcherManager = FakeDispatcherManager(),
Expand Down Expand Up @@ -114,6 +120,16 @@ class FillAssistManagerTest {
verify(exactly = 0) { fillAssistDiskSource.storeFillAssistRules(any(), any()) }
}

@Test
fun `sync returns success and does nothing when fill assist is disabled by user`() = runTest {
every { settingsRepository.isFillAssistEnabled } returns false

manager.syncIfNecessary()

coVerify(exactly = 0) { fillAssistService.getManifest() }
verify(exactly = 0) { fillAssistDiskSource.storeFillAssistRules(any(), any()) }
}

@Test
fun `sync returns success and does nothing when fillAssistRulesUrl is null`() = runTest {
serverConfigFlow.value = null
Expand Down
Loading
Loading