Skip to content
Merged
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 @@ -176,4 +176,7 @@ interface AutofillFeature {

@Toggle.DefaultValue(defaultValue = DefaultFeatureValue.TRUE)
fun canDetectSystemAutofillEngagement(): Toggle

@Toggle.DefaultValue(defaultValue = DefaultFeatureValue.TRUE)
fun prioritizeDomainMatchesOnSearch(): Toggle
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,12 @@ class AutofillSimpleCredentialsListFragment : DuckDuckGoFragment(R.layout.fragme
logcat(INFO) { "DDGAutofillService showSuggestionsFor: $showSuggestionsFor" }
val directSuggestions = suggestionMatcher.getDirectSuggestions(showSuggestionsFor, credentials)
val shareableCredentials = suggestionMatcher.getShareableSuggestions(showSuggestionsFor)
val directSuggestionsListItems = suggestionListBuilder.build(directSuggestions, shareableCredentials, allowBreakageReporting = false)
val directSuggestionsListItems = suggestionListBuilder.build(
unsortedQuerySuggestions = listOf(),
unsortedDirectSuggestions = directSuggestions,
unsortedSharableSuggestions = shareableCredentials,
allowBreakageReporting = false,
)
val groupedCredentials = credentialGrouper.group(credentials)

withContext(dispatchers.main()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,10 @@ class AutofillPasswordsManagementViewModel @Inject constructor(
fun onViewCreated() {
if (combineJob != null) return
combineJob = viewModelScope.launch(dispatchers.io()) {
_viewState.value = _viewState.value.copy(autofillEnabled = autofillStore.autofillEnabled)
_viewState.value = _viewState.value.copy(
autofillEnabled = autofillStore.autofillEnabled,
prioritizeDomainMatchesOnSearch = autofillFeature.prioritizeDomainMatchesOnSearch().isEnabled(),
)

val allCredentials = autofillStore.getAllCredentials().distinctUntilChanged()
val combined = allCredentials.combine(searchQueryFilter) { credentials, filter ->
Expand Down Expand Up @@ -820,6 +823,7 @@ class AutofillPasswordsManagementViewModel @Inject constructor(
val reportBreakageState: ReportBreakageState = ReportBreakageState(),
val canShowPromo: Boolean = false,
val canImportFromGooglePasswords: Boolean = false,
val prioritizeDomainMatchesOnSearch: Boolean = false,
)

data class ReportBreakageState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,21 @@ class SuggestionListBuilder @Inject constructor(
) {

fun build(
unsortedQuerySuggestions: List<LoginCredentials>,
unsortedDirectSuggestions: List<LoginCredentials>,
unsortedSharableSuggestions: List<LoginCredentials>,
allowBreakageReporting: Boolean,
): List<ListItem> {
val list = mutableListOf<ListItem>()

if (unsortedDirectSuggestions.isNotEmpty() || unsortedSharableSuggestions.isNotEmpty()) {
if (unsortedQuerySuggestions.isNotEmpty() || unsortedDirectSuggestions.isNotEmpty() || unsortedSharableSuggestions.isNotEmpty()) {
list.add(GroupHeading(context.getString(string.credentialManagementSuggestionsLabel)))

val sortedQuerySuggestions = sorter.sort(unsortedQuerySuggestions)
val sortedDirectSuggestions = sorter.sort(unsortedDirectSuggestions)
val sortedSharableSuggestions = sorter.sort(unsortedSharableSuggestions)

val allSuggestions = sortedDirectSuggestions + sortedSharableSuggestions
val allSuggestions = (sortedQuerySuggestions + sortedDirectSuggestions + sortedSharableSuggestions).distinct()
list.addAll(allSuggestions.map { SuggestedCredential(it) })

if (allowBreakageReporting) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ class SuggestionMatcher @Inject constructor(
}
}

fun getQuerySuggestions(
query: String?,
credentials: List<LoginCredentials>,
): List<LoginCredentials> {
if (query.isNullOrBlank()) return emptyList()
return credentials.filter { it.domain?.contains(query, ignoreCase = true) == true }
}

/**
* Returns a list of credentials that are not a direct match for the current URL, but are considered shareable.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
canShowImportGooglePasswordsButton = state.canImportFromGooglePasswords,
showAutofillToggle = state.showAutofillEnabledToggle,
promotionView = promotionView,
query = state.credentialSearchQuery,
prioritizeDomainMatchesOnSearch = state.prioritizeDomainMatchesOnSearch,
)
parentActivity()?.invalidateOptionsMenu()
}
Expand Down Expand Up @@ -413,6 +415,8 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
canShowImportGooglePasswordsButton: Boolean,
showAutofillToggle: Boolean,
promotionView: View?,
query: String,
prioritizeDomainMatchesOnSearch: Boolean,
) {
if (credentials == null) {
logcat(VERBOSE) { "Credentials is null, meaning we haven't retrieved them yet. Don't know if empty or not yet" }
Expand All @@ -423,12 +427,16 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
autofillEnabled = viewModel.viewState.value.autofillEnabled,
promotionView = promotionView,
showGoogleImportPasswordsButton = canShowImportGooglePasswordsButton,
query = query,
prioritizeDomainMatchesOnSearch = prioritizeDomainMatchesOnSearch,
)
} else if (credentials.isEmpty() && credentialSearchQuery.isEmpty()) {
showEmptyCredentialsPlaceholders(
canShowImportGooglePasswordsButton = canShowImportGooglePasswordsButton,
showAutofillToggle = showAutofillToggle,
promotionView = promotionView,
query = query,
prioritizeDomainMatchesOnSearch = prioritizeDomainMatchesOnSearch,
)
} else if (credentials.isEmpty()) {
showNoResultsPlaceholders(credentialSearchQuery)
Expand All @@ -440,6 +448,8 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
autofillEnabled = viewModel.viewState.value.autofillEnabled,
promotionView = promotionView,
showGoogleImportPasswordsButton = canShowImportGooglePasswordsButton,
query = query,
prioritizeDomainMatchesOnSearch = prioritizeDomainMatchesOnSearch,
)
}
}
Expand All @@ -452,6 +462,8 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
canShowImportGooglePasswordsButton: Boolean,
showAutofillToggle: Boolean,
promotionView: View?,
query: String,
prioritizeDomainMatchesOnSearch: Boolean,
) {
renderCredentialList(
credentials = emptyList(),
Expand All @@ -460,6 +472,8 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
autofillEnabled = viewModel.viewState.value.autofillEnabled,
promotionView = promotionView,
showGoogleImportPasswordsButton = canShowImportGooglePasswordsButton,
query = query,
prioritizeDomainMatchesOnSearch = prioritizeDomainMatchesOnSearch,
)

if (canShowImportGooglePasswordsButton) {
Expand All @@ -474,16 +488,24 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
autofillEnabled: Boolean,
promotionView: View?,
showGoogleImportPasswordsButton: Boolean,
query: String,
prioritizeDomainMatchesOnSearch: Boolean,
) {
withContext(dispatchers.io()) {
val currentUrl = getCurrentSiteUrl()

val credentialLoadingState = if (credentials == null) {
Loading
} else {
val querySuggestions =
if (prioritizeDomainMatchesOnSearch) { suggestionMatcher.getQuerySuggestions(query, credentials) } else emptyList()
val directSuggestions = suggestionMatcher.getDirectSuggestions(currentUrl, credentials)
val shareableCredentials = suggestionMatcher.getShareableSuggestions(currentUrl)
val directSuggestionsListItems = suggestionListBuilder.build(directSuggestions, shareableCredentials, allowBreakageReporting)
val directSuggestionsListItems = suggestionListBuilder.build(
querySuggestions,
directSuggestions,
shareableCredentials,
allowBreakageReporting,
)
val groupedCredentials = grouper.group(credentials)

val hasSuggestions = directSuggestions.isNotEmpty() || shareableCredentials.isNotEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import androidx.test.platform.app.InstrumentationRegistry
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.impl.encoding.TestUrlUnicodeNormalizer
import com.duckduckgo.autofill.impl.ui.credential.management.sorting.CredentialListSorterByTitleAndDomain
import com.duckduckgo.autofill.impl.ui.credential.management.suggestion.SuggestionListBuilderTest.Type.DIRECT
import com.duckduckgo.autofill.impl.ui.credential.management.suggestion.SuggestionListBuilderTest.Type.QUERY
import com.duckduckgo.autofill.impl.ui.credential.management.suggestion.SuggestionListBuilderTest.Type.SHAREABLE
import com.duckduckgo.autofill.impl.ui.credential.management.viewing.list.ListItem
import com.duckduckgo.autofill.impl.ui.credential.management.viewing.list.ListItem.CredentialListItem.SuggestedCredential
import com.duckduckgo.autofill.impl.urlmatcher.AutofillDomainNameUrlMatcher
Expand All @@ -39,95 +42,144 @@ class SuggestionListBuilderTest {

@Test
fun whenNoSuggestionThenEmptyListReturned() {
assertTrue(testee.build(emptyList(), emptyList(), allowBreakageReporting = false).isEmpty())
assertTrue(testee.build(emptyList(), emptyList(), emptyList(), allowBreakageReporting = false).isEmpty())
}

@Test
fun whenOneDirectSuggestionThenDividerAddedLast() {
val suggestions = buildSuggestions(1)
val list = testee.build(suggestions, emptyList(), allowBreakageReporting = false)
val suggestions = buildSuggestions(1, type = DIRECT)
val list = testee.build(emptyList(), suggestions, emptyList(), allowBreakageReporting = false)
assertTrue(list.last() is ListItem.Divider)
}

@Test
fun whenOneQuerySuggestionThenDividerAddedLast() {
val suggestions = buildSuggestions(1, type = QUERY)
val list = testee.build(emptyList(), suggestions, emptyList(), allowBreakageReporting = false)
assertTrue(list.last() is ListItem.Divider)
}

@Test
fun whenTwoDirectSuggestionsThenDividerAddedLast() {
val suggestions = buildSuggestions(2)
val list = testee.build(suggestions, emptyList(), allowBreakageReporting = false)
val suggestions = buildSuggestions(2, type = DIRECT)
val list = testee.build(emptyList(), suggestions, emptyList(), allowBreakageReporting = false)
assertTrue(list.last() is ListItem.Divider)
}

@Test
fun whenOneDirectSuggestionThenCorrectNumberOfListItemsReturned() {
val suggestions = buildSuggestions(1)
val list = testee.build(suggestions, emptyList(), allowBreakageReporting = false)
val suggestions = buildSuggestions(1, type = DIRECT)
val list = testee.build(emptyList(), suggestions, emptyList(), allowBreakageReporting = false)
assertEquals(NUM_SUGGESTION_HEADERS + NUM_DIVIDERS + suggestions.size, list.size)
}

@Test
fun whenTwoDirectSuggestionsThenCorrectNumberOfListItemsReturned() {
val suggestions = buildSuggestions(2)
val list = testee.build(suggestions, emptyList(), allowBreakageReporting = false)
val suggestions = buildSuggestions(2, type = DIRECT)
val list = testee.build(emptyList(), suggestions, emptyList(), allowBreakageReporting = false)
assertEquals(NUM_SUGGESTION_HEADERS + NUM_DIVIDERS + suggestions.size, list.size)
}

@Test
fun whenTenDirectSuggestionsThenThirteenListItemsReturned() {
val suggestions = buildSuggestions(10)
val list = testee.build(suggestions, emptyList(), allowBreakageReporting = false)
val suggestions = buildSuggestions(10, type = DIRECT)
val list = testee.build(emptyList(), suggestions, emptyList(), allowBreakageReporting = false)
assertEquals(NUM_SUGGESTION_HEADERS + NUM_DIVIDERS + suggestions.size, list.size)
}

@Test
fun whenDirectSuggestionAddedThenGroupNameIsCorrect() {
val suggestions = buildSuggestions(1)
val heading = testee.build(suggestions, emptyList(), allowBreakageReporting = false).first()
val suggestions = buildSuggestions(1, type = DIRECT)
val heading = testee.build(emptyList(), suggestions, emptyList(), allowBreakageReporting = false).first()
assertTrue(heading is ListItem.GroupHeading)
}

@Test
fun whenNoDirectSuggestionsButOneShareableThenCorrectNumberOfListItemsReturned() {
val suggestions = buildSuggestions(1)
val list = testee.build(emptyList(), suggestions, allowBreakageReporting = false)
val suggestions = buildSuggestions(1, type = DIRECT)
val list = testee.build(emptyList(), emptyList(), suggestions, allowBreakageReporting = false)
assertEquals(NUM_SUGGESTION_HEADERS + NUM_DIVIDERS + suggestions.size, list.size)
}

@Test
fun whenNoDirectSuggestionsButMultipleShareableThenCorrectNumberOfListItemsReturned() {
val suggestions = buildSuggestions(10)
val list = testee.build(emptyList(), suggestions, allowBreakageReporting = false)
val suggestions = buildSuggestions(10, type = DIRECT)
val list = testee.build(emptyList(), emptyList(), suggestions, allowBreakageReporting = false)
assertEquals(NUM_SUGGESTION_HEADERS + NUM_DIVIDERS + suggestions.size, list.size)
}

@Test
fun whenOneDirectAndOneShareableThenDirectSuggestionsAppearFirst() {
val directSuggestions = buildSuggestions(1)
val sharableSuggestions = buildSuggestions(1, startingIndex = directSuggestions.size)
val list = testee.build(directSuggestions, sharableSuggestions, allowBreakageReporting = false)
val directSuggestions = buildSuggestions(1, type = DIRECT)
val sharableSuggestions = buildSuggestions(1, startingIndex = directSuggestions.size, type = SHAREABLE)
val list = testee.build(emptyList(), directSuggestions, sharableSuggestions, allowBreakageReporting = false)
assertEquals(NUM_SUGGESTION_HEADERS + NUM_DIVIDERS + directSuggestions.size + sharableSuggestions.size, list.size)
assertTrue(list[0] is ListItem.GroupHeading)
assertEquals(0L, (list[1] as SuggestedCredential).credentials.id)
assertEquals(1L, (list[2] as SuggestedCredential).credentials.id)
assertEquals(DIRECT.name, (list[1] as SuggestedCredential).credentials.username)
assertEquals(SHAREABLE.name, (list[2] as SuggestedCredential).credentials.username)
}

@Test
fun whenOneQueryAndOneDirectAndOneShareableThenQuerySuggestionsAppearFirst() {
val querySuggestions = buildSuggestions(1, type = QUERY)
val directSuggestions = buildSuggestions(1, startingIndex = querySuggestions.size, type = DIRECT)
val sharableSuggestions = buildSuggestions(1, startingIndex = querySuggestions.size + directSuggestions.size, type = SHAREABLE)
val list = testee.build(querySuggestions, directSuggestions, sharableSuggestions, allowBreakageReporting = false)
assertEquals(NUM_SUGGESTION_HEADERS + NUM_DIVIDERS + querySuggestions.size + directSuggestions.size + sharableSuggestions.size, list.size)
assertTrue(list[0] is ListItem.GroupHeading)
assertEquals(QUERY.name, (list[1] as SuggestedCredential).credentials.username)
assertEquals(DIRECT.name, (list[2] as SuggestedCredential).credentials.username)
assertEquals(SHAREABLE.name, (list[3] as SuggestedCredential).credentials.username)
}

@Test
fun whenOneQueryAndOneDirectWithDuplicatesThenRemoveDuplicates() {
val credential = aCredential(username = "test")
val querySuggestions = listOf(credential)
val directSuggestions = listOf(credential)
val sharableSuggestions = emptyList<LoginCredentials>()
val list = testee.build(querySuggestions, directSuggestions, sharableSuggestions, allowBreakageReporting = false)
assertEquals(NUM_SUGGESTION_HEADERS + NUM_DIVIDERS + 1, list.size)
}

@Test
fun whenMultipleDirectAndSomeShareableThenDirectSuggestionsAppearFirst() {
val directSuggestions = buildSuggestions(10, isShareable = false)
val sharableSuggestions = buildSuggestions(1, isShareable = true, startingIndex = directSuggestions.size)
val list = testee.build(directSuggestions, sharableSuggestions, allowBreakageReporting = false)
val directSuggestions = buildSuggestions(10, type = DIRECT)
val sharableSuggestions = buildSuggestions(1, type = SHAREABLE, startingIndex = directSuggestions.size)
val list = testee.build(emptyList(), directSuggestions, sharableSuggestions, allowBreakageReporting = false)
assertEquals(NUM_SUGGESTION_HEADERS + NUM_DIVIDERS + directSuggestions.size + sharableSuggestions.size, list.size)
assertTrue(list[0] is ListItem.GroupHeading)
assertEquals("direct", (list[1] as SuggestedCredential).credentials.username)
assertEquals("shareable", (list[11] as SuggestedCredential).credentials.username)
assertEquals(DIRECT.name, (list[1] as SuggestedCredential).credentials.username)
assertEquals(SHAREABLE.name, (list[11] as SuggestedCredential).credentials.username)
}

@Test
fun whenMultipleQueryAndMultipleDirectAndSomeShareableThenQuerySuggestionsAppearFirst() {
val querySuggestions = buildSuggestions(10, type = QUERY)
val directSuggestions = buildSuggestions(10, type = DIRECT, startingIndex = querySuggestions.size)
val sharableSuggestions = buildSuggestions(1, type = SHAREABLE, startingIndex = querySuggestions.size + directSuggestions.size)
val list = testee.build(querySuggestions, directSuggestions, sharableSuggestions, allowBreakageReporting = false)
assertEquals(NUM_SUGGESTION_HEADERS + NUM_DIVIDERS + querySuggestions.size + directSuggestions.size + sharableSuggestions.size, list.size)
assertTrue(list[0] is ListItem.GroupHeading)
assertEquals(QUERY.name, (list[1] as SuggestedCredential).credentials.username)
assertEquals(DIRECT.name, (list[11] as SuggestedCredential).credentials.username)
assertEquals(SHAREABLE.name, (list[21] as SuggestedCredential).credentials.username)
}

private fun buildSuggestions(listSize: Int, startingIndex: Int = 0, isShareable: Boolean = false): List<LoginCredentials> {
private fun buildSuggestions(listSize: Int, startingIndex: Int = 0, type: Type): List<LoginCredentials> {
val list = mutableListOf<LoginCredentials>()
for (i in 0 until listSize) {
list.add(aCredential(id = startingIndex + i.toLong(), username = if (isShareable) "shareable" else "direct"))
list.add(aCredential(id = startingIndex + i.toLong(), username = type.name))
}
return list
}

private enum class Type {
QUERY,
DIRECT,
SHAREABLE,
}

private fun aCredential(id: Long = 0, username: String): LoginCredentials {
return LoginCredentials(id = id, domain = null, password = null, username = username)
}
Expand Down
Loading
Loading