From 49eb3e6c8c59ebffb3b51ddc7ac50d0d68d2e3b9 Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Mon, 16 Sep 2024 19:10:50 +0100 Subject: [PATCH 1/3] Initial commit. --- .../java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt index 1a65278158c9..d745f460f4cb 100644 --- a/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt +++ b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt @@ -113,6 +113,7 @@ class AutoCompleteApi @Inject constructor( private val dispatcherProvider: DispatcherProvider, ) : AutoComplete { + // TODO ANA: Add option to "Switch to tab" in the AutoComplete suggestions override fun autoComplete(query: String): Observable { if (query.isBlank()) { return Observable.just(AutoCompleteResult(query = query, suggestions = emptyList())) From f392504d918f3192132231c774d4facba6279038 Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Tue, 22 Oct 2024 12:43:56 +0100 Subject: [PATCH 2/3] Include open tabs in the suggestions (#5019) Task/Issue URL: Added all open tabs to suggestions. All opened tabs (no checks on the query) are placed above history, for simplicity. Matching and ranking to be done separately. - [x] Install from this branch. - [x] Open a few websites in different tabs. - [x] Search for something and notice the "Tab" suggestions. There is no matching done, only check the UI (the switch to tab text is not yet present). | Example 1 (with history) | Example 2 (without history) | | ------ | ----- | |![Screenshot_20240916_215653](https://github.com/user-attachments/assets/af875653-1dd1-4376-bda7-9f753c4a354c)|![Screenshot_20240916_220717](https://github.com/user-attachments/assets/27a7a05e-c0d1-43d2-ae68-37db43b71f20)| --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1208264037618740 --------- Co-authored-by: Dax The Translator --- .../app/browser/BrowserTabViewModelTest.kt | 105 +- .../autocomplete/AutocompleteTabsFeature.kt | 30 + .../app/autocomplete/api/AutoComplete.kt | 330 +++--- .../autocomplete/api/AutoCompleteService.kt | 5 +- .../duckduckgo/app/browser/BrowserActivity.kt | 16 + .../app/browser/BrowserTabFragment.kt | 12 + .../app/browser/BrowserTabViewModel.kt | 73 +- .../app/browser/BrowserViewModel.kt | 6 + .../BrowserAutoCompleteSuggestionsAdapter.kt | 13 +- .../autocomplete/SuggestionItemDecoration.kt | 75 ++ .../SuggestionViewHolderFactory.kt | 91 +- .../app/browser/commands/Command.kt | 1 + .../com/duckduckgo/app/pixels/AppPixelName.kt | 3 + .../app/systemsearch/SystemSearchActivity.kt | 29 +- .../app/systemsearch/SystemSearchViewModel.kt | 85 +- .../res/drawable/ic_autocomplete_20dp.xml | 14 +- .../drawable/ic_autocomplete_down_20dp.xml | 18 +- .../main/res/drawable/ic_switch_to_tab_20.xml | 10 + .../main/res/drawable/suggestions_divider.xml | 26 + .../item_autocomplete_bookmark_suggestion.xml | 17 +- .../res/layout/item_autocomplete_default.xml | 5 +- ...autocomplete_history_search_suggestion.xml | 55 + .../item_autocomplete_history_suggestion.xml | 17 +- .../item_autocomplete_search_suggestion.xml | 5 +- ..._autocomplete_switch_to_tab_suggestion.xml | 70 ++ .../main/res/layout/view_legacy_omnibar.xml | 3 +- .../res/layout/view_legacy_omnibar_bottom.xml | 3 +- app/src/main/res/values-bg/strings.xml | 1 + app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-da/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-el/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-et/strings.xml | 1 + app/src/main/res/values-fi/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-hr/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-lt/strings.xml | 1 + app/src/main/res/values-lv/strings.xml | 1 + app/src/main/res/values-nb/strings.xml | 1 + app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 1 + app/src/main/res/values-ro/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sk/strings.xml | 1 + app/src/main/res/values-sl/strings.xml | 1 + app/src/main/res/values-sv/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../autocomplete/api/AutoCompleteApiTest.kt | 945 +++++++++++------- .../app/browser/BrowserViewModelTest.kt | 9 + .../systemsearch/SystemSearchViewModelTest.kt | 78 +- .../main/res/values-et/strings-duckplayer.xml | 12 +- history/history-api/build.gradle | 2 - .../history/api/NavigationHistory.kt | 7 +- history/history-impl/build.gradle | 1 - .../history/impl/HistoryRepository.kt | 28 +- .../history/impl/RealNavigationHistory.kt | 7 +- .../duckduckgo/app/statistics/pixels/Pixel.kt | 2 + 62 files changed, 1534 insertions(+), 699 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/autocomplete/AutocompleteTabsFeature.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/autocomplete/SuggestionItemDecoration.kt create mode 100644 app/src/main/res/drawable/ic_switch_to_tab_20.xml create mode 100644 app/src/main/res/drawable/suggestions_divider.xml create mode 100644 app/src/main/res/layout/item_autocomplete_history_search_suggestion.xml create mode 100644 app/src/main/res/layout/item_autocomplete_switch_to_tab_suggestion.xml diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 285f9a4a4369..5b3de950acaf 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -45,13 +45,15 @@ import com.duckduckgo.adclick.api.AdClickManager import com.duckduckgo.app.ValueCaptorObserver import com.duckduckgo.app.accessibility.data.AccessibilitySettingsDataStore import com.duckduckgo.app.accessibility.data.AccessibilitySettingsSharedPreferences +import com.duckduckgo.app.autocomplete.AutocompleteTabsFeature import com.duckduckgo.app.autocomplete.api.AutoComplete import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult -import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteDefaultSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteBookmarkSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteSwitchToTabSuggestion import com.duckduckgo.app.autocomplete.api.AutoCompleteApi import com.duckduckgo.app.autocomplete.api.AutoCompleteScorer import com.duckduckgo.app.autocomplete.api.AutoCompleteService @@ -240,9 +242,6 @@ import com.duckduckgo.sync.api.favicons.FaviconsFetchingPrompt import com.duckduckgo.voice.api.VoiceSearchAvailability import com.duckduckgo.voice.api.VoiceSearchAvailabilityPixelLogger import dagger.Lazy -import io.reactivex.Observable -import io.reactivex.Single -import io.reactivex.observers.TestObserver import java.io.File import java.math.BigInteger import java.security.cert.X509Certificate @@ -253,6 +252,7 @@ import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow @@ -491,6 +491,7 @@ class BrowserTabViewModelTest { private val changeOmnibarPositionFeature: ChangeOmnibarPositionFeature = mock() private val mockHighlightsOnboardingExperimentManager: HighlightsOnboardingExperimentManager = mock() private var fakeAndroidConfigBrowserFeature = FakeFeatureToggleFactory.create(AndroidBrowserConfigFeature::class.java) + private val mockAutocompleteTabsFeature: AutocompleteTabsFeature = mock() @Before fun before() = runTest { @@ -507,8 +508,10 @@ class BrowserTabViewModelTest { mockNavigationHistory, mockAutoCompleteScorer, mockAutoCompleteRepository, + mockTabRepository, mockUserStageStore, coroutineRule.testDispatcherProvider, + mockAutocompleteTabsFeature, ) val fireproofWebsiteRepositoryImpl = FireproofWebsiteRepositoryImpl( fireproofWebsiteDao, @@ -535,6 +538,8 @@ class BrowserTabViewModelTest { whenever(mockDuckPlayer.isDuckPlayerUri(anyString())).thenReturn(false) whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(ENABLED) whenever(changeOmnibarPositionFeature.refactor()).thenReturn(mockEnabledToggle) + whenever(mockAutocompleteTabsFeature.self()).thenReturn(mockEnabledToggle) + whenever(mockAutocompleteTabsFeature.self().isEnabled()).thenReturn(true) remoteMessagingModel = givenRemoteMessagingModel(mockRemoteMessagingRepository, mockPixel, coroutineRule.testDispatcherProvider) @@ -1393,8 +1398,8 @@ class BrowserTabViewModelTest { } @Test - fun whenTriggeringAutocompleteThenAutoCompleteSuggestionsShown() { - whenever(mockAutoCompleteService.autoComplete("foo")).thenReturn(Observable.just(emptyList())) + fun whenTriggeringAutocompleteThenAutoCompleteSuggestionsShown() = runTest { + whenever(mockAutoCompleteService.autoComplete("foo")).thenReturn(emptyList()) doReturn(true).whenever(mockSettingsStore).autoCompleteSuggestionsEnabled testee.triggerAutocomplete("foo", true, hasQueryChanged = true) assertTrue(autoCompleteViewState().showSuggestions) @@ -1453,15 +1458,18 @@ class BrowserTabViewModelTest { @Test fun wheneverAutoCompleteIsGoneAndHistoryIAMHasBeenShownThenNotifyUserSeenIAM() { runTest { - whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(emptyList())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just(listOf(Bookmark("abc", "title", "https://example.com", lastModified = null))), + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf(listOf(Bookmark("abc", "title", "https://example.com", lastModified = null))), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just(listOf(Favorite("abc", "title", "https://example.com", position = 1, lastModified = null))), + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf(listOf(Favorite("abc", "title", "https://example.com", position = 1, lastModified = null))), ) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just(listOf(VisitedPage("https://foo.com".toUri(), "title", listOf(LocalDateTime.now())))), + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf(listOf(VisitedPage("https://foo.com".toUri(), "title", listOf(LocalDateTime.now())))), + ) + whenever(mockTabRepository.flowTabs).thenReturn( + flowOf(listOf(TabEntity(tabId = "1", position = 1, url = "https://example.com", title = "title"))), ) doReturn(true).whenever(mockSettingsStore).autoCompleteSuggestionsEnabled @@ -1470,7 +1478,9 @@ class BrowserTabViewModelTest { whenever(mockAutoCompleteScorer.score("title", "https://foo.com".toUri(), 1, "title")).thenReturn(1) whenever(mockUserStageStore.getUserAppStage()).thenReturn(ESTABLISHED) - testee.autoCompletePublishSubject.accept("title") + testee.onAutoCompleteSuggestionsChanged() + testee.autoCompleteStateFlow.value = "title" + delay(500) testee.autoCompleteSuggestionsGone() verify(mockAutoCompleteRepository).submitUserSeenHistoryIAM() verify(mockPixel).fire(AUTOCOMPLETE_BANNER_SHOWN) @@ -1480,16 +1490,16 @@ class BrowserTabViewModelTest { @Test fun wheneverAutoCompleteIsGoneAndHistoryIAMHasNotBeenShownThenDoNotNotifyUserSeenIAM() { runTest { - whenever(mockAutoCompleteService.autoComplete("query")).thenReturn(Observable.just(emptyList())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just(listOf(Bookmark("abc", "title", "https://example.com", lastModified = null))), + whenever(mockAutoCompleteService.autoComplete("query")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf(listOf(Bookmark("abc", "title", "https://example.com", lastModified = null))), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just(listOf(Favorite("abc", "title", "https://example.com", position = 1, lastModified = null))), + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf(listOf(Favorite("abc", "title", "https://example.com", position = 1, lastModified = null))), ) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn(Single.just(listOf())) + whenever(mockNavigationHistory.getHistory()).thenReturn(flowOf(emptyList())) doReturn(true).whenever(mockSettingsStore).autoCompleteSuggestionsEnabled - testee.autoCompletePublishSubject.accept("query") + testee.autoCompleteStateFlow.value = "query" testee.autoCompleteSuggestionsGone() verify(mockAutoCompleteRepository, never()).submitUserSeenHistoryIAM() verify(mockPixel, never()).fire(AUTOCOMPLETE_BANNER_SHOWN) @@ -2346,6 +2356,37 @@ class BrowserTabViewModelTest { assertEquals("false", argumentCaptor.firstValue[PixelParameter.BOOKMARK_CAPABLE]) } + @Test + fun whenSearchSuggestionSubmittedWithTabsThenAutoCompleteSearchSelectionPixelSent() = runTest { + whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(false) + whenever(mockNavigationHistory.hasHistory()).thenReturn(false) + tabsLiveData.value = listOf(TabEntity("1", "https://example.com", position = 0), TabEntity("2", "https://example.com", position = 1)) + val suggestions = listOf(AutoCompleteSwitchToTabSuggestion("example", "", "", "")) + testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", suggestions)) + testee.fireAutocompletePixel(AutoCompleteSwitchToTabSuggestion("example", "", "", "")) + + val argumentCaptor = argumentCaptor>() + verify(mockPixel).fire(eq(AppPixelName.AUTOCOMPLETE_SWITCH_TO_TAB_SELECTION), argumentCaptor.capture(), any(), any()) + + assertEquals("true", argumentCaptor.firstValue[PixelParameter.SHOWED_SWITCH_TO_TAB]) + assertEquals("true", argumentCaptor.firstValue[PixelParameter.SWITCH_TO_TAB_CAPABLE]) + } + + @Test + fun whenSearchSuggestionSubmittedWithoutTabsThenAutoCompleteSearchSelectionPixelSent() = runTest { + whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(false) + whenever(mockNavigationHistory.hasHistory()).thenReturn(false) + tabsLiveData.value = listOf(TabEntity("1", "https://example.com", position = 0)) + testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", emptyList())) + testee.fireAutocompletePixel(AutoCompleteSwitchToTabSuggestion("example", "", "", "")) + + val argumentCaptor = argumentCaptor>() + verify(mockPixel).fire(eq(AppPixelName.AUTOCOMPLETE_SWITCH_TO_TAB_SELECTION), argumentCaptor.capture(), any(), any()) + + assertEquals("false", argumentCaptor.firstValue[PixelParameter.SHOWED_SWITCH_TO_TAB]) + assertEquals("false", argumentCaptor.firstValue[PixelParameter.SWITCH_TO_TAB_CAPABLE]) + } + @Test fun whenUserSelectToEditQueryThenMoveCaretToTheEnd() = runTest { testee.onUserSelectedToEditQuery("foo") @@ -5837,15 +5878,11 @@ class BrowserTabViewModelTest { val suggestion = AutoCompleteHistorySuggestion(phrase = "phrase", title = "title", url = "url", isAllowedInTopHits = false) val omnibarText = "foo" - val testObserver = TestObserver.create() - testee.autoCompletePublishSubject.subscribe(testObserver) - testee.onRemoveSearchSuggestionConfirmed(suggestion, omnibarText) verify(mockPixel).fire(AppPixelName.AUTOCOMPLETE_RESULT_DELETED) verify(mockPixel).fire(AppPixelName.AUTOCOMPLETE_RESULT_DELETED_DAILY, type = Daily()) verify(mockNavigationHistory).removeHistoryEntryByUrl(suggestion.url) - testObserver.assertValue(omnibarText) assertCommandIssued() } @@ -5854,15 +5891,11 @@ class BrowserTabViewModelTest { val suggestion = AutoCompleteHistorySearchSuggestion(phrase = "phrase", isAllowedInTopHits = false) val omnibarText = "foo" - val testObserver = TestObserver.create() - testee.autoCompletePublishSubject.subscribe(testObserver) - testee.onRemoveSearchSuggestionConfirmed(suggestion, omnibarText) verify(mockPixel).fire(AppPixelName.AUTOCOMPLETE_RESULT_DELETED) verify(mockPixel).fire(AppPixelName.AUTOCOMPLETE_RESULT_DELETED_DAILY, type = Daily()) verify(mockNavigationHistory).removeHistoryEntryByQuery(suggestion.phrase) - testObserver.assertValue(omnibarText) assertCommandIssued() } @@ -6027,6 +6060,20 @@ class BrowserTabViewModelTest { verify(mockPixel, never()).enqueueFire(AppPixelName.ERROR_PAGE_SHOWN) } + @Test + fun whenUserSelectedAutocompleteWithAutoCompleteSwitchToTabSuggestionThenSwitchToTabCommandSentWithTabId() = runTest { + val tabId = "tabId" + val suggestion = AutoCompleteSwitchToTabSuggestion(phrase = "phrase", title = "title", url = "https://www.example.com", tabId = tabId) + whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(false) + whenever(mockNavigationHistory.hasHistory()).thenReturn(false) + + testee.userSelectedAutocomplete(suggestion) + + assertCommandIssued { + assertEquals(tabId, this.tabId) + } + } + private fun aCredential(): LoginCredentials { return LoginCredentials(domain = null, username = null, password = null) } diff --git a/app/src/main/java/com/duckduckgo/app/autocomplete/AutocompleteTabsFeature.kt b/app/src/main/java/com/duckduckgo/app/autocomplete/AutocompleteTabsFeature.kt new file mode 100644 index 000000000000..f804000b223a --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/autocomplete/AutocompleteTabsFeature.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.autocomplete + +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle + +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "autocompleteTabs", +) +interface AutocompleteTabsFeature { + @Toggle.DefaultValue(false) + fun self(): Toggle +} diff --git a/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt index d745f460f4cb..9e3b9ad4507a 100644 --- a/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt +++ b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt @@ -17,20 +17,26 @@ package com.duckduckgo.app.autocomplete.api import android.net.Uri +import androidx.annotation.VisibleForTesting import androidx.core.net.toUri +import com.duckduckgo.app.autocomplete.AutocompleteTabsFeature import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion -import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteDefaultSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteInAppMessageSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteBookmarkSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteSwitchToTabSuggestion import com.duckduckgo.app.autocomplete.impl.AutoCompleteRepository import com.duckduckgo.app.browser.UriString import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.UserStageStore +import com.duckduckgo.app.tabs.model.TabEntity +import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.common.utils.AppUrl import com.duckduckgo.common.utils.AppUrl.Url import com.duckduckgo.common.utils.DispatcherProvider @@ -47,18 +53,21 @@ import com.duckduckgo.savedsites.api.models.SavedSite import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark import com.duckduckgo.savedsites.api.models.SavedSite.Favorite import com.squareup.anvil.annotations.ContributesBinding -import io.reactivex.Observable -import java.io.InterruptedIOException import javax.inject.Inject import kotlin.math.max -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map const val maximumNumberOfSuggestions = 12 const val maximumNumberOfTopHits = 2 const val minimumNumberInSuggestionGroup = 5 interface AutoComplete { - fun autoComplete(query: String): Observable + fun autoComplete(query: String): Flow suspend fun userDismissedHistoryInAutoCompleteIAM() suspend fun submitUserSeenHistoryIAM() @@ -77,12 +86,26 @@ interface AutoComplete { override val phrase: String, ) : AutoCompleteSuggestion(phrase) - data class AutoCompleteBookmarkSuggestion( - override val phrase: String, - val title: String, - val url: String, - val isFavorite: Boolean = false, - ) : AutoCompleteSuggestion(phrase) + sealed class AutoCompleteUrlSuggestion( + phrase: String, + open val title: String, + open val url: String, + ) : AutoCompleteSuggestion(phrase) { + + data class AutoCompleteBookmarkSuggestion( + override val phrase: String, + override val title: String, + override val url: String, + val isFavorite: Boolean = false, + ) : AutoCompleteUrlSuggestion(phrase, title, url) + + data class AutoCompleteSwitchToTabSuggestion( + override val phrase: String, + override val title: String, + override val url: String, + val tabId: String, + ) : AutoCompleteUrlSuggestion(phrase, title, url) + } sealed class AutoCompleteHistoryRelatedSuggestion(phrase: String) : AutoCompleteSuggestion(phrase) { data class AutoCompleteHistorySuggestion( @@ -109,76 +132,108 @@ class AutoCompleteApi @Inject constructor( private val navigationHistory: NavigationHistory, private val autoCompleteScorer: AutoCompleteScorer, private val autoCompleteRepository: AutoCompleteRepository, + private val tabRepository: TabRepository, private val userStageStore: UserStageStore, private val dispatcherProvider: DispatcherProvider, + private val autocompleteTabsFeature: AutocompleteTabsFeature, ) : AutoComplete { - // TODO ANA: Add option to "Switch to tab" in the AutoComplete suggestions - override fun autoComplete(query: String): Observable { + private var isAutocompleteTabsFeatureEnabled: Boolean? = null + + override fun autoComplete(query: String): Flow { if (query.isBlank()) { - return Observable.just(AutoCompleteResult(query = query, suggestions = emptyList())) + return flowOf(AutoCompleteResult(query = query, suggestions = emptyList())) } - val savedSitesObservable: Observable> = - getAutoCompleteBookmarkResults(query) - .zipWith( - getAutoCompleteFavoritesResults(query), - ) { bookmarks, favorites -> - (favorites + bookmarks.filter { favorites.none { favorite -> (it.suggestion).url == favorite.suggestion.url } }) - }.zipWith( - getHistoryResults(query), - ) { bookmarksAndFavorites, historyItems -> - val searchHistory = historyItems.filter { it.suggestion is AutoCompleteHistorySearchSuggestion } - val navigationHistory = historyItems - .filter { it.suggestion is AutoCompleteHistorySuggestion } as List> - (removeDuplicates(navigationHistory, bookmarksAndFavorites) + searchHistory).sortedByDescending { it.score }.map { it.suggestion } - } - - return savedSitesObservable.zipWith(getAutoCompleteSearchResults(query)) { bookmarksAndHistory, searchResults -> - val topHits = (searchResults + bookmarksAndHistory).filter { - when (it) { - is AutoCompleteHistorySearchSuggestion -> it.isAllowedInTopHits - is AutoCompleteHistorySuggestion -> it.isAllowedInTopHits - is AutoCompleteBookmarkSuggestion -> true - else -> false - } - }.take(maximumNumberOfTopHits) - - val maxBottomSection = maximumNumberOfSuggestions - (topHits.size + minimumNumberInSuggestionGroup) - val filteredBookmarks = - bookmarksAndHistory - .filter { suggestion -> topHits.none { it.phrase == suggestion.phrase } } - .take(maxBottomSection) - val maxSearchResults = maximumNumberOfSuggestions - (topHits.size + filteredBookmarks.size) - val filteredSearchResults = searchResults - .filter { searchSuggestion -> filteredBookmarks.none { it.phrase == searchSuggestion.phrase } } - .take(maxSearchResults) + return combine( + getAutoCompleteBookmarkResults(query), + getAutoCompleteFavoritesResults(query), + getAutocompleteSwitchToTabResults(query), + getAutoCompleteHistoryResults(query), + getAutoCompleteSearchResults(query), + ) { bookmarks, favorites, tabs, historyResults, searchResults -> + val bookmarksFavoritesTabsAndHistory = combineBookmarksFavoritesTabsAndHistory(bookmarks, favorites, tabs, historyResults) + val topHits = getTopHits(bookmarksFavoritesTabsAndHistory, searchResults) + val filteredBookmarksFavoritesTabsAndHistory = filterBookmarksAndTabsAndHistory(bookmarksFavoritesTabsAndHistory, topHits) + val distinctSearchResults = getDistinctSearchResults(searchResults, topHits, filteredBookmarksFavoritesTabsAndHistory) + + (topHits + distinctSearchResults + filteredBookmarksFavoritesTabsAndHistory).distinctBy { + Pair(it.phrase, it::class.java) + } + }.map { suggestions -> val inAppMessage = mutableListOf() - - val suggestions = (topHits + filteredSearchResults + filteredBookmarks).distinctBy { it.phrase } - - runBlocking(dispatcherProvider.io()) { - if (shouldShowHistoryInAutoCompleteIAM(suggestions)) { - inAppMessage.add(0, AutoCompleteInAppMessageSuggestion) - } + if (shouldShowHistoryInAutoCompleteIAM(suggestions)) { + inAppMessage.add(0, AutoCompleteInAppMessageSuggestion) } AutoCompleteResult( query = query, suggestions = inAppMessage + suggestions.ifEmpty { listOf(AutoCompleteDefaultSuggestion(query)) }, ) - }.onErrorResumeNext(Observable.empty()) + } + } + + private fun combineBookmarksFavoritesTabsAndHistory( + bookmarks: List>, + favorites: List>, + tabs: List>, + historyItems: List>, + ): List { + val bookmarksAndFavorites = (favorites + bookmarks.filter { favorites.none { favorite -> (it.suggestion).url == favorite.suggestion.url } }) + val bookmarksFavoritesAndTabs = (tabs + bookmarksAndFavorites) as List> + val searchHistory = historyItems.filter { it.suggestion is AutoCompleteHistorySearchSuggestion } + val navigationHistory = + historyItems.filter { it.suggestion is AutoCompleteHistorySuggestion } as List> + return (removeDuplicates(navigationHistory, bookmarksFavoritesAndTabs) + searchHistory) + .sortedByDescending { it.score } + .map { it.suggestion } + } + + private fun getTopHits( + bookmarksAndFavoritesAndTabsAndHistory: List, + searchResults: List, + ): List { + return (searchResults + bookmarksAndFavoritesAndTabsAndHistory).filter { + when (it) { + is AutoCompleteHistorySearchSuggestion -> it.isAllowedInTopHits + is AutoCompleteHistorySuggestion -> it.isAllowedInTopHits + is AutoCompleteUrlSuggestion -> true + else -> false + } + }.take(maximumNumberOfTopHits) + } + + private fun filterBookmarksAndTabsAndHistory( + bookmarksAndFavoritesAndTabsAndHistory: List, + topHits: List, + ): List { + val maxBottomSection = maximumNumberOfSuggestions - (topHits.size + minimumNumberInSuggestionGroup) + return bookmarksAndFavoritesAndTabsAndHistory + .filter { suggestion -> topHits.none { it.phrase == suggestion.phrase } } + .take(maxBottomSection) + } + + private fun getDistinctSearchResults( + searchResults: List, + topHits: List, + filteredBookmarksAndTabsAndHistory: List, + ): List { + val distinctPhrases = (topHits + filteredBookmarksAndTabsAndHistory).distinctBy { it.phrase }.map { it.phrase }.toSet() + val distinctPairs = (topHits + filteredBookmarksAndTabsAndHistory).distinctBy { Pair(it.phrase, it::class.java) }.size + val maxSearchResults = maximumNumberOfSuggestions - distinctPairs + return searchResults.distinctBy { it.phrase }.filterNot { it.phrase in distinctPhrases }.take(maxSearchResults) } private fun removeDuplicates( historySuggestions: List>, - bookmarkSuggestions: List>, + bookmarkSuggestions: List>, ): List> { val bookmarkMap = bookmarkSuggestions.associateBy { it.suggestion.phrase.lowercase() } val uniqueHistorySuggestions = historySuggestions.filter { !bookmarkMap.containsKey(it.suggestion.phrase.lowercase()) } val updatedBookmarkSuggestions = bookmarkSuggestions.map { bookmarkSuggestion -> - val historySuggestion = historySuggestions.find { it.suggestion.phrase.equals(bookmarkSuggestion.suggestion.phrase, ignoreCase = true) } + val historySuggestion = + historySuggestions.find { it.suggestion.phrase.equals(bookmarkSuggestion.suggestion.phrase, ignoreCase = true) } if (historySuggestion != null) { bookmarkSuggestion.copy( score = max(historySuggestion.score, bookmarkSuggestion.score), @@ -218,50 +273,70 @@ class AutoCompleteApi @Inject constructor( return entry.visits.size > 3 || entry.url.isRoot() } - private fun getAutoCompleteSearchResults(query: String) = - autoCompleteService.autoComplete(query) - .flatMapIterable { it } - .map { - AutoCompleteSearchSuggestion(phrase = it.phrase, isUrl = (it.isNav ?: UriString.isWebUrl(it.phrase))) + private fun getAutocompleteSwitchToTabResults(query: String): Flow>> = + runCatching { + if (autocompleteTabsEnabled) { + tabRepository.flowTabs + .map { rankTabs(query, it) } + .distinctUntilChanged() + } else { + flowOf(emptyList()) } - .toList() - .toObservable() - .onErrorResumeNext { throwable: Throwable -> - if (throwable is InterruptedIOException) { - // If the query text is deleted quickly, the request may be cancelled, resulting in an InterruptedIOException. - // Return an empty observable to avoid showing the default state. - Observable.empty() - } else { - Observable.just(emptyList()) - } + }.getOrElse { flowOf(emptyList()) } + + private val autocompleteTabsEnabled: Boolean by lazy { + isAutocompleteTabsFeatureEnabled ?: run { + val enabled = autocompleteTabsFeature.self().isEnabled() + isAutocompleteTabsFeatureEnabled = enabled + enabled + } + } + + private fun getAutoCompleteSearchResults(query: String) = flow { + val searchSuggestionsList = mutableListOf() + runCatching { + val rawResults = autoCompleteService.autoComplete(query) + for (rawResult in rawResults) { + val searchSuggestion = AutoCompleteSearchSuggestion( + phrase = rawResult.phrase.formatIfUrl(), + isUrl = rawResult.isNav ?: UriString.isWebUrl(rawResult.phrase), + ) + searchSuggestionsList.add(searchSuggestion) } + emit(searchSuggestionsList.toList()) + }.getOrElse { emit(searchSuggestionsList.toList()) } + } - private fun getAutoCompleteBookmarkResults(query: String): Observable>> = - savedSitesRepository.getBookmarksObservable() - .map { rankBookmarks(query, it) } - .flattenAsObservable { it } - .distinctUntilChanged() - .toList() - .onErrorReturn { emptyList() } - .toObservable() - - private fun getAutoCompleteFavoritesResults(query: String): Observable>> = - savedSitesRepository.getFavoritesObservable() - .map { rankFavorites(query, it) } - .flattenAsObservable { it } - .distinctUntilChanged() - .toList() - .onErrorReturn { emptyList() } - .toObservable() - - private fun getHistoryResults(query: String): Observable>> = - navigationHistory.getHistorySingle() - .map { rankHistory(query, it) } - .flattenAsObservable { it } - .distinctUntilChanged() - .toList() - .onErrorReturn { emptyList() } - .toObservable() + private fun getAutoCompleteBookmarkResults(query: String): Flow>> = + runCatching { + savedSitesRepository.getBookmarks() + .map { rankBookmarks(query, it) } + .distinctUntilChanged() + }.getOrElse { flowOf(emptyList()) } + + private fun getAutoCompleteFavoritesResults(query: String): Flow>> = + runCatching { + savedSitesRepository.getFavorites() + .map { rankFavorites(query, it) } + .distinctUntilChanged() + }.getOrElse { flowOf(emptyList()) } + + private fun getAutoCompleteHistoryResults(query: String): Flow>> = + runCatching { + navigationHistory.getHistory() + .map { rankHistory(query, it) } + .distinctUntilChanged() + }.getOrElse { flowOf(emptyList()) } + + private fun rankTabs( + query: String, + tabs: List, + ): List> { + return tabs.asSequence() + .filter { it.url != null } + .distinctBy { it.url } + .sortTabsByRank(query) + } private fun rankBookmarks( query: String, @@ -285,11 +360,28 @@ class AutoCompleteApi @Inject constructor( return history.asSequence().sortHistoryByRank(query) } + private fun Sequence.sortTabsByRank(query: String): List> { + return this.map { tabEntity -> + RankedSuggestion( + AutoCompleteSwitchToTabSuggestion( + phrase = tabEntity.url?.formatIfUrl().orEmpty(), + title = tabEntity.title.orEmpty(), + url = tabEntity.url.orEmpty(), + tabId = tabEntity.tabId, + ), + ) + } + .map { scoreTitle(it, query) } + .map { scoreTokens(it, query) } + .filter { it.score > 0 } + .toList() + } + private fun Sequence.sortByRank(query: String): List> { return this.map { savedSite -> RankedSuggestion( AutoCompleteBookmarkSuggestion( - phrase = savedSite.url.toUri().toStringDropScheme(), + phrase = savedSite.url.formatIfUrl(), title = savedSite.title, url = savedSite.url, isFavorite = savedSite is Favorite, @@ -324,7 +416,7 @@ class AutoCompleteApi @Inject constructor( when (entry) { is VisitedPage -> { AutoCompleteHistorySuggestion( - phrase = entry.url.toStringDropScheme(), + phrase = entry.url.toString().formatIfUrl(), title = entry.title, url = entry.url.toString(), isAllowedInTopHits = isAllowedInTopHits(entry), @@ -332,7 +424,7 @@ class AutoCompleteApi @Inject constructor( } is VisitedSERP -> { AutoCompleteHistorySearchSuggestion( - phrase = entry.query, + phrase = entry.query.formatIfUrl(), isAllowedInTopHits = isAllowedInTopHits(entry), ) } @@ -343,23 +435,23 @@ class AutoCompleteApi @Inject constructor( .toList() } - private fun scoreTitle( - rankedBookmark: RankedSuggestion, + private fun scoreTitle( + rankedSuggestion: RankedSuggestion, query: String, - ): RankedSuggestion { - return if (rankedBookmark.suggestion.title.startsWith(query, ignoreCase = true)) { - rankedBookmark.copy(score = rankedBookmark.score + 200) - } else if (rankedBookmark.suggestion.title.contains(" $query", ignoreCase = true)) { - rankedBookmark.copy(score = rankedBookmark.score + 100) + ): RankedSuggestion { + return if (rankedSuggestion.suggestion.title.startsWith(query, ignoreCase = true)) { + rankedSuggestion.copy(score = rankedSuggestion.score + 200) + } else if (rankedSuggestion.suggestion.title.contains(" $query", ignoreCase = true)) { + rankedSuggestion.copy(score = rankedSuggestion.score + 100) } else { - rankedBookmark + rankedSuggestion } } - private fun scoreTokens( - rankedBookmark: RankedSuggestion, + private fun scoreTokens( + rankedBookmark: RankedSuggestion, query: String, - ): RankedSuggestion { + ): RankedSuggestion { val suggestion = rankedBookmark.suggestion val domain = suggestion.url.toUri().baseHost val tokens = query.split(" ") @@ -397,3 +489,15 @@ class AutoCompleteApi @Inject constructor( val score: Int = DEFAULT_SCORE, ) } + +@VisibleForTesting +internal fun String.formatIfUrl(): String { + val trimmedUrl = this.trimEnd('/') + + val prefixToRemove = listOf("http://www.", "https://www.", "www.", "http://", "https://") + val formattedUrl = prefixToRemove.find { trimmedUrl.startsWith(it, ignoreCase = true) }?.let { + trimmedUrl.substring(it.length) + } ?: trimmedUrl + + return formattedUrl +} diff --git a/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteService.kt b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteService.kt index 13dd3e3e0aca..454d11700b7e 100644 --- a/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteService.kt +++ b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteService.kt @@ -19,7 +19,6 @@ package com.duckduckgo.app.autocomplete.api import com.duckduckgo.anvil.annotations.ContributesNonCachingServiceApi import com.duckduckgo.common.utils.AppUrl import com.duckduckgo.di.scopes.AppScope -import io.reactivex.Observable import java.util.* import retrofit2.http.GET import retrofit2.http.Query @@ -28,11 +27,11 @@ import retrofit2.http.Query interface AutoCompleteService { @GET("${AppUrl.Url.API}/ac/") - fun autoComplete( + suspend fun autoComplete( @Query("q") query: String, @Query("kl") languageCode: String = Locale.getDefault().language, @Query("is_nav") nav: String = "1", - ): Observable> + ): List } data class AutoCompleteServiceRawResult( diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index e91eca55f16f..c674bf41b3b4 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -349,6 +349,12 @@ open class BrowserActivity : DuckDuckGoActivity() { return } + val existingTabId = intent.getStringExtra(OPEN_EXISTING_TAB_ID_EXTRA) + if (existingTabId != null) { + openExistingTab(existingTabId) + return + } + val sharedText = intent.intentText if (sharedText != null) { if (intent.getBooleanExtra(ShortcutBuilder.SHORTCUT_EXTRA_ARG, false)) { @@ -509,6 +515,12 @@ open class BrowserActivity : DuckDuckGoActivity() { } } + fun openExistingTab(tabId: String) { + lifecycleScope.launch { + viewModel.onTabSelected(tabId) + } + } + fun launchSettings() { startActivity(SettingsActivity.intent(this)) } @@ -570,6 +582,7 @@ open class BrowserActivity : DuckDuckGoActivity() { selectedText: Boolean = false, isExternal: Boolean = false, interstitialScreen: Boolean = false, + openExistingTabId: String? = null, ): Intent { val intent = Intent(context, BrowserActivity::class.java) intent.putExtra(EXTRA_TEXT, queryExtra) @@ -579,6 +592,7 @@ open class BrowserActivity : DuckDuckGoActivity() { intent.putExtra(SELECTED_TEXT_EXTRA, selectedText) intent.putExtra(LAUNCH_FROM_EXTERNAL_EXTRA, isExternal) intent.putExtra(LAUNCH_FROM_INTERSTITIAL_EXTRA, interstitialScreen) + intent.putExtra(OPEN_EXISTING_TAB_ID_EXTRA, openExistingTabId) return intent } @@ -591,6 +605,8 @@ open class BrowserActivity : DuckDuckGoActivity() { const val OPEN_IN_CURRENT_TAB_EXTRA = "OPEN_IN_CURRENT_TAB_EXTRA" const val SELECTED_TEXT_EXTRA = "SELECTED_TEXT_EXTRA" private const val LAUNCH_FROM_INTERSTITIAL_EXTRA = "INTERSTITIAL_SCREEN_EXTRA" + const val OPEN_EXISTING_TAB_ID_EXTRA = "OPEN_EXISTING_TAB_ID_EXTRA" + private const val LAUNCH_FROM_EXTERNAL_EXTRA = "LAUNCH_FROM_EXTERNAL_EXTRA" private const val MAX_ACTIVE_TABS = 40 diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 2016655f6ecf..658e2e307e2c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -109,6 +109,7 @@ import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability import com.duckduckgo.app.browser.applinks.AppLinksLauncher import com.duckduckgo.app.browser.applinks.AppLinksSnackBarConfigurator import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter +import com.duckduckgo.app.browser.autocomplete.SuggestionItemDecoration import com.duckduckgo.app.browser.commands.Command import com.duckduckgo.app.browser.commands.Command.ShowBackNavigationHistory import com.duckduckgo.app.browser.commands.NavigationCommand @@ -1742,6 +1743,12 @@ class BrowserTabFragment : hideOnboardingDaxDialog(it.onboardingCta) browserActivity?.launchFire() } + is Command.SwitchToTab -> { + binding.focusedView.gone() + viewModel.autoCompleteSuggestionsGone() + binding.autoCompleteSuggestionsList.gone() + browserActivity?.openExistingTab(it.tabId) + } else -> { // NO OP } @@ -1791,6 +1798,7 @@ class BrowserTabFragment : } private fun autocompleteItemRemoved() { + viewModel.onAutoCompleteSuggestionsChanged() showKeyboardAndRestorePosition(autocompleteFirstVisibleItemPosition, autocompleteItemOffsetTop) } @@ -2398,6 +2406,9 @@ class BrowserTabFragment : omnibarPosition = settingsDataStore.omnibarPosition, ) binding.autoCompleteSuggestionsList.adapter = autoCompleteSuggestionsAdapter + binding.autoCompleteSuggestionsList.addItemDecoration( + SuggestionItemDecoration(ContextCompat.getDrawable(context, R.drawable.suggestions_divider)!!), + ) } private fun configureNewTab() { @@ -3724,6 +3735,7 @@ class BrowserTabFragment : binding.autoCompleteSuggestionsList.gone() } else { binding.autoCompleteSuggestionsList.show() + viewModel.onAutoCompleteSuggestionsChanged() autoCompleteSuggestionsAdapter.updateData(viewState.searchResults.query, viewState.searchResults.suggestions) hideFocusedView() } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index f1c99f6c5839..689933308bbc 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -52,12 +52,13 @@ import com.duckduckgo.app.accessibility.data.AccessibilitySettingsDataStore import com.duckduckgo.app.autocomplete.api.AutoComplete import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion -import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteDefaultSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteInAppMessageSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteBookmarkSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteSwitchToTabSuggestion import com.duckduckgo.app.browser.LongPressHandler.RequiredAction import com.duckduckgo.app.browser.SSLErrorType.EXPIRED import com.duckduckgo.app.browser.SSLErrorType.GENERIC @@ -302,15 +303,11 @@ import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.sync.api.favicons.FaviconsFetchingPrompt import com.duckduckgo.voice.api.VoiceSearchAvailability import com.duckduckgo.voice.api.VoiceSearchAvailabilityPixelLogger -import com.jakewharton.rxrelay2.PublishRelay import dagger.Lazy -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import java.net.URI import java.net.URISyntaxException import java.util.Locale -import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.collections.List @@ -348,9 +345,12 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -496,7 +496,7 @@ class BrowserTabViewModel @Inject constructor( private val locationPermissionSession: MutableMap = mutableMapOf() @VisibleForTesting - val autoCompletePublishSubject = PublishRelay.create() + internal val autoCompleteStateFlow = MutableStateFlow("") private val fireproofWebsiteState: LiveData> = fireproofWebsiteRepository.getFireproofWebsites() @ExperimentalCoroutinesApi @@ -505,7 +505,7 @@ class BrowserTabViewModel @Inject constructor( context = viewModelScope.coroutineContext, ) - private var autoCompleteDisposable: Disposable? = null + private var autoCompleteJob: Job? = null private var site: Site? = null private lateinit var tabId: String private var webNavigationState: WebNavigationState? = null @@ -586,7 +586,6 @@ class BrowserTabViewModel @Inject constructor( init { initializeViewStates() - configureAutoComplete() logVoiceSearchAvailability() fireproofWebsiteState.observeForever(fireproofWebsitesObserver) @@ -749,22 +748,24 @@ class BrowserTabViewModel @Inject constructor( if (voiceSearchAvailability.isVoiceSearchSupported) voiceSearchPixelLogger.log() } + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @SuppressLint("CheckResult") private fun configureAutoComplete() { - autoCompleteDisposable = autoCompletePublishSubject - .debounce(300, TimeUnit.MILLISECONDS) - .switchMap { autoComplete.autoComplete(it) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { result -> - if (result.suggestions.contains(AutoCompleteInAppMessageSuggestion)) { - hasUserSeenHistoryIAM = true - } - onAutoCompleteResultReceived(result) - }, - { t: Throwable? -> Timber.w(t, "Failed to get search results") }, - ) + autoCompleteJob?.cancel() + autoCompleteJob = autoCompleteStateFlow + .debounce(300) + .distinctUntilChanged() + .flatMapLatest { autoComplete.autoComplete(it) } + .flowOn(dispatchers.io()) + .onEach { result -> + if (result.suggestions.contains(AutoCompleteInAppMessageSuggestion)) { + hasUserSeenHistoryIAM = true + } + onAutoCompleteResultReceived(result) + } + .flowOn(dispatchers.main()) + .catch { t: Throwable? -> Timber.w(t, "Failed to get search results") } + .launchIn(viewModelScope) } private fun onAutoCompleteResultReceived(result: AutoCompleteResult) { @@ -778,8 +779,8 @@ class BrowserTabViewModel @Inject constructor( @VisibleForTesting public override fun onCleared() { buildingSiteFactoryJob?.cancel() - autoCompleteDisposable?.dispose() - autoCompleteDisposable = null + autoCompleteJob?.cancel() + autoCompleteJob = null fireproofWebsiteState.removeObserver(fireproofWebsitesObserver) navigationAwareLoginDetector.loginEventLiveData.removeObserver(loginDetectionObserver) fireproofDialogsEventHandler.event.removeObserver(fireproofDialogEventObserver) @@ -847,10 +848,14 @@ class BrowserTabViewModel @Inject constructor( val hasHistory = withContext(dispatchers.io()) { history.hasHistory() } + val hasTabs = withContext(dispatchers.io()) { + (tabRepository.liveTabs.value?.size ?: 0) > 1 + } val hasBookmarkResults = currentViewState.searchResults.suggestions.any { it is AutoCompleteBookmarkSuggestion && !it.isFavorite } val hasFavoriteResults = currentViewState.searchResults.suggestions.any { it is AutoCompleteBookmarkSuggestion && it.isFavorite } val hasHistoryResults = currentViewState.searchResults.suggestions.any { it is AutoCompleteHistorySuggestion || it is AutoCompleteHistorySearchSuggestion } + val hasSwitchToTabResults = currentViewState.searchResults.suggestions.any { it is AutoCompleteSwitchToTabSuggestion } val params = mapOf( PixelParameter.SHOWED_BOOKMARKS to hasBookmarkResults.toString(), PixelParameter.SHOWED_FAVORITES to hasFavoriteResults.toString(), @@ -858,6 +863,8 @@ class BrowserTabViewModel @Inject constructor( PixelParameter.FAVORITE_CAPABLE to hasFavorites.toString(), PixelParameter.HISTORY_CAPABLE to hasHistory.toString(), PixelParameter.SHOWED_HISTORY to hasHistoryResults.toString(), + PixelParameter.SWITCH_TO_TAB_CAPABLE to hasTabs.toString(), + PixelParameter.SHOWED_SWITCH_TO_TAB to hasSwitchToTabResults.toString(), ) val pixelName = when (suggestion) { is AutoCompleteBookmarkSuggestion -> { @@ -871,6 +878,7 @@ class BrowserTabViewModel @Inject constructor( is AutoCompleteSearchSuggestion -> if (suggestion.isUrl) AUTOCOMPLETE_SEARCH_WEBSITE_SELECTION else AUTOCOMPLETE_SEARCH_PHRASE_SELECTION is AutoCompleteHistorySuggestion -> AUTOCOMPLETE_HISTORY_SITE_SELECTION is AutoCompleteHistorySearchSuggestion -> AUTOCOMPLETE_HISTORY_SEARCH_SELECTION + is AutoCompleteSwitchToTabSuggestion -> AppPixelName.AUTOCOMPLETE_SWITCH_TO_TAB_SELECTION else -> return } @@ -888,6 +896,7 @@ class BrowserTabViewModel @Inject constructor( is AutoCompleteSearchSuggestion -> onUserSubmittedQuery(suggestion.phrase, FromAutocomplete(isNav = suggestion.isUrl)) is AutoCompleteHistorySuggestion -> onUserSubmittedQuery(suggestion.url, FromAutocomplete(isNav = true)) is AutoCompleteHistorySearchSuggestion -> onUserSubmittedQuery(suggestion.phrase, FromAutocomplete(isNav = false)) + is AutoCompleteSwitchToTabSuggestion -> onUserSwitchedToTab(suggestion.tabId) is AutoCompleteInAppMessageSuggestion -> return@withContext } } @@ -927,7 +936,7 @@ class BrowserTabViewModel @Inject constructor( else -> {} } withContext(dispatchers.main()) { - autoCompletePublishSubject.accept(omnibarText) + autoCompleteStateFlow.value = omnibarText command.value = AutocompleteItemRemoved } } @@ -2170,7 +2179,7 @@ class BrowserTabViewModel @Inject constructor( ) if (hasFocus && autoCompleteSuggestionsEnabled) { - autoCompletePublishSubject.accept(query.trim()) + autoCompleteStateFlow.value = query.trim() } } @@ -3699,6 +3708,10 @@ class BrowserTabViewModel @Inject constructor( } } + fun onAutoCompleteSuggestionsChanged() { + configureAutoComplete() + } + fun autoCompleteSuggestionsGone() { viewModelScope.launch(dispatchers.io()) { if (hasUserSeenHistoryIAM) { @@ -3722,8 +3735,12 @@ class BrowserTabViewModel @Inject constructor( if (suggestions.any { it is AutoCompleteSearchSuggestion && it.isUrl }) { pixel.fire(AppPixelName.AUTOCOMPLETE_DISPLAYED_LOCAL_WEBSITE) } + if (suggestions.any { it is AutoCompleteSwitchToTabSuggestion }) { + pixel.fire(AppPixelName.AUTOCOMPLETE_DISPLAYED_LOCAL_SWITCH_TO_TAB) + } } lastAutoCompleteState = null + autoCompleteJob?.cancel() } } @@ -3845,6 +3862,10 @@ class BrowserTabViewModel @Inject constructor( } } + private fun onUserSwitchedToTab(tabId: String) { + command.value = Command.SwitchToTab(tabId) + } + companion object { private const val FIXED_PROGRESS = 50 diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index b9359b5f3586..73686393e4c7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -284,6 +284,12 @@ class BrowserViewModel @Inject constructor( fun onBookmarksActivityResult(url: String) { command.value = Command.OpenSavedSite(url) } + + fun onTabSelected(tabId: String) { + launch(dispatchers.io()) { + tabRepository.select(tabId) + } + } } /** diff --git a/app/src/main/java/com/duckduckgo/app/browser/autocomplete/BrowserAutoCompleteSuggestionsAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/autocomplete/BrowserAutoCompleteSuggestionsAdapter.kt index af698625923f..eebc10b4da5c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/autocomplete/BrowserAutoCompleteSuggestionsAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/autocomplete/BrowserAutoCompleteSuggestionsAdapter.kt @@ -20,11 +20,12 @@ import android.view.ViewGroup import androidx.annotation.UiThread import androidx.recyclerview.widget.RecyclerView import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion -import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteDefaultSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteInAppMessageSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteBookmarkSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteSwitchToTabSuggestion import com.duckduckgo.app.browser.autocomplete.AutoCompleteViewHolder.EmptySuggestionViewHolder import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.BOOKMARK_TYPE import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.DEFAULT_TYPE @@ -33,6 +34,7 @@ import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAda import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.HISTORY_TYPE import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.IN_APP_MESSAGE_TYPE import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.SUGGESTION_TYPE +import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.SWITCH_TO_TAB_TYPE import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition class BrowserAutoCompleteSuggestionsAdapter( @@ -53,11 +55,12 @@ class BrowserAutoCompleteSuggestionsAdapter( private val viewHolderFactoryMap: Map = mapOf( EMPTY_TYPE to EmptySuggestionViewHolderFactory(), SUGGESTION_TYPE to SearchSuggestionViewHolderFactory(omnibarPosition), - BOOKMARK_TYPE to BookmarkSuggestionViewHolderFactory(omnibarPosition), - HISTORY_TYPE to HistorySuggestionViewHolderFactory(omnibarPosition), - HISTORY_SEARCH_TYPE to HistorySearchSuggestionViewHolderFactory(omnibarPosition), + BOOKMARK_TYPE to BookmarkSuggestionViewHolderFactory(), + HISTORY_TYPE to HistorySuggestionViewHolderFactory(), + HISTORY_SEARCH_TYPE to HistorySearchSuggestionViewHolderFactory(), IN_APP_MESSAGE_TYPE to InAppMessageViewHolderFactory(), DEFAULT_TYPE to DefaultSuggestionViewHolderFactory(omnibarPosition), + SWITCH_TO_TAB_TYPE to SwitchToTabSuggestionViewHolderFactory(), ) private var phrase = "" @@ -77,6 +80,7 @@ class BrowserAutoCompleteSuggestionsAdapter( suggestions[position] is AutoCompleteHistorySearchSuggestion -> HISTORY_SEARCH_TYPE suggestions[position] is AutoCompleteInAppMessageSuggestion -> IN_APP_MESSAGE_TYPE suggestions[position] is AutoCompleteDefaultSuggestion -> DEFAULT_TYPE + suggestions[position] is AutoCompleteSwitchToTabSuggestion -> SWITCH_TO_TAB_TYPE else -> SUGGESTION_TYPE } } @@ -126,5 +130,6 @@ class BrowserAutoCompleteSuggestionsAdapter( const val HISTORY_SEARCH_TYPE = 5 const val IN_APP_MESSAGE_TYPE = 6 const val DEFAULT_TYPE = 7 + const val SWITCH_TO_TAB_TYPE = 8 } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/autocomplete/SuggestionItemDecoration.kt b/app/src/main/java/com/duckduckgo/app/browser/autocomplete/SuggestionItemDecoration.kt new file mode 100644 index 000000000000..486aaf58b469 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/autocomplete/SuggestionItemDecoration.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.autocomplete + +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.view.ViewGroup.MarginLayoutParams +import androidx.recyclerview.widget.RecyclerView + +class SuggestionItemDecoration(private val divider: Drawable) : RecyclerView.ItemDecoration() { + + override fun onDrawOver( + canvas: Canvas, + parent: RecyclerView, + state: RecyclerView.State, + ) { + canvas.save() + + val childCount = parent.childCount + val parentRight = parent.width - parent.paddingRight + + for (i in 0 until childCount) { + val child = parent.getChildAt(i) + val params = child.layoutParams as MarginLayoutParams + val currentViewType = child.tag + val nextViewType = if (i + 1 < childCount) parent.getChildAt(i + 1).tag else UNKNOWN + + if (nextViewType == UNKNOWN) { + continue + } + + if (currentViewType == SEARCH_ITEM && nextViewType == OTHER_ITEM) { + drawDivider(canvas, child, params, parentRight) + } + + if (currentViewType == OTHER_ITEM && nextViewType == SEARCH_ITEM) { + drawDivider(canvas, child, params, parentRight) + } + } + + canvas.restore() + } + + private fun drawDivider( + canvas: Canvas, + child: android.view.View, + params: MarginLayoutParams, + parentRight: Int, + ) { + val horizontalSize = parentRight + val verticalSize = child.bottom + params.bottomMargin + divider.setBounds(0, verticalSize, horizontalSize, verticalSize + divider.intrinsicHeight) + divider.draw(canvas) + } + + companion object { + internal const val SEARCH_ITEM = "SEARCH_ITEM" + internal const val OTHER_ITEM = "OTHER_ITEM" + internal const val UNKNOWN = "UNKNOWN" + } +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/autocomplete/SuggestionViewHolderFactory.kt b/app/src/main/java/com/duckduckgo/app/browser/autocomplete/SuggestionViewHolderFactory.kt index 676b2bf0186b..144ea194c1e3 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/autocomplete/SuggestionViewHolderFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/autocomplete/SuggestionViewHolderFactory.kt @@ -21,18 +21,23 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion -import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteDefaultSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteBookmarkSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteSwitchToTabSuggestion import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.autocomplete.AutoCompleteViewHolder.InAppMessageViewHolder +import com.duckduckgo.app.browser.autocomplete.SuggestionItemDecoration.Companion.OTHER_ITEM +import com.duckduckgo.app.browser.autocomplete.SuggestionItemDecoration.Companion.SEARCH_ITEM import com.duckduckgo.app.browser.databinding.ItemAutocompleteBookmarkSuggestionBinding import com.duckduckgo.app.browser.databinding.ItemAutocompleteDefaultBinding +import com.duckduckgo.app.browser.databinding.ItemAutocompleteHistorySearchSuggestionBinding import com.duckduckgo.app.browser.databinding.ItemAutocompleteHistorySuggestionBinding import com.duckduckgo.app.browser.databinding.ItemAutocompleteInAppMessageBinding import com.duckduckgo.app.browser.databinding.ItemAutocompleteSearchSuggestionBinding +import com.duckduckgo.app.browser.databinding.ItemAutocompleteSwitchToTabSuggestionBinding import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition import com.duckduckgo.common.ui.view.MessageCta.Message @@ -78,7 +83,7 @@ class SearchSuggestionViewHolderFactory(private val omnibarPosition: OmnibarPosi } } -class HistorySuggestionViewHolderFactory(private val omnibarPosition: OmnibarPosition) : SuggestionViewHolderFactory { +class HistorySuggestionViewHolderFactory() : SuggestionViewHolderFactory { override fun onCreateViewHolder(parent: ViewGroup): AutoCompleteViewHolder { val inflater = LayoutInflater.from(parent.context) @@ -99,18 +104,16 @@ class HistorySuggestionViewHolderFactory(private val omnibarPosition: OmnibarPos searchSuggestionViewHolder.bind( suggestion as AutoCompleteHistorySuggestion, immediateSearchClickListener, - editableSearchClickListener, longPressClickListener, - omnibarPosition, ) } } -class HistorySearchSuggestionViewHolderFactory(private val omnibarPosition: OmnibarPosition) : SuggestionViewHolderFactory { +class HistorySearchSuggestionViewHolderFactory() : SuggestionViewHolderFactory { override fun onCreateViewHolder(parent: ViewGroup): AutoCompleteViewHolder { val inflater = LayoutInflater.from(parent.context) - val binding = ItemAutocompleteSearchSuggestionBinding.inflate(inflater, parent, false) + val binding = ItemAutocompleteHistorySearchSuggestionBinding.inflate(inflater, parent, false) return AutoCompleteViewHolder.HistorySearchSuggestionViewHolder(binding) } @@ -127,14 +130,12 @@ class HistorySearchSuggestionViewHolderFactory(private val omnibarPosition: Omni historySearchSuggestionViewHolder.bind( suggestion as AutoCompleteHistorySearchSuggestion, immediateSearchClickListener, - editableSearchClickListener, longPressClickListener, - omnibarPosition, ) } } -class BookmarkSuggestionViewHolderFactory(private val omnibarPosition: OmnibarPosition) : SuggestionViewHolderFactory { +class BookmarkSuggestionViewHolderFactory : SuggestionViewHolderFactory { override fun onCreateViewHolder(parent: ViewGroup): AutoCompleteViewHolder { val inflater = LayoutInflater.from(parent.context) @@ -155,8 +156,31 @@ class BookmarkSuggestionViewHolderFactory(private val omnibarPosition: OmnibarPo bookmarkSuggestionViewHolder.bind( suggestion as AutoCompleteBookmarkSuggestion, immediateSearchClickListener, - editableSearchClickListener, - omnibarPosition, + ) + } +} + +class SwitchToTabSuggestionViewHolderFactory : SuggestionViewHolderFactory { + + override fun onCreateViewHolder(parent: ViewGroup): AutoCompleteViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = ItemAutocompleteSwitchToTabSuggestionBinding.inflate(inflater, parent, false) + return AutoCompleteViewHolder.SwitchToTabSuggestionViewHolder(binding) + } + + override fun onBindViewHolder( + holder: AutoCompleteViewHolder, + suggestion: AutoCompleteSuggestion, + immediateSearchClickListener: (AutoCompleteSuggestion) -> Unit, + editableSearchClickListener: (AutoCompleteSuggestion) -> Unit, + deleteClickListener: (AutoCompleteSuggestion) -> Unit, + openSettingsClickListener: () -> Unit, + longPressClickListener: (AutoCompleteSuggestion) -> Unit, + ) { + val switchToTabSuggestionViewHolder = holder as AutoCompleteViewHolder.SwitchToTabSuggestionViewHolder + switchToTabSuggestionViewHolder.bind( + suggestion as AutoCompleteSwitchToTabSuggestion, + immediateSearchClickListener, ) } } @@ -246,31 +270,26 @@ sealed class AutoCompleteViewHolder(itemView: View) : RecyclerView.ViewHolder(it if (omnibarPosition == OmnibarPosition.BOTTOM) { editQueryImage.setImageResource(R.drawable.ic_autocomplete_down_20dp) } + + root.tag = SEARCH_ITEM } } - class HistorySearchSuggestionViewHolder(val binding: ItemAutocompleteSearchSuggestionBinding) : AutoCompleteViewHolder(binding.root) { + class HistorySearchSuggestionViewHolder(val binding: ItemAutocompleteHistorySearchSuggestionBinding) : AutoCompleteViewHolder(binding.root) { fun bind( item: AutoCompleteHistorySearchSuggestion, immediateSearchListener: (AutoCompleteSuggestion) -> Unit, - editableSearchClickListener: (AutoCompleteSuggestion) -> Unit, longPressClickListener: (AutoCompleteSuggestion) -> Unit, - omnibarPosition: OmnibarPosition, ) = with(binding) { phrase.text = item.phrase - phraseOrUrlIndicator.setImageResource(R.drawable.ic_history) - - editQueryImage.setOnClickListener { editableSearchClickListener(item) } root.setOnClickListener { immediateSearchListener(item) } root.setOnLongClickListener { longPressClickListener(item) true } - if (omnibarPosition == OmnibarPosition.BOTTOM) { - editQueryImage.setImageResource(R.drawable.ic_autocomplete_down_20dp) - } + root.tag = OTHER_ITEM } } @@ -278,19 +297,14 @@ sealed class AutoCompleteViewHolder(itemView: View) : RecyclerView.ViewHolder(it fun bind( item: AutoCompleteBookmarkSuggestion, immediateSearchListener: (AutoCompleteSuggestion) -> Unit, - editableSearchClickListener: (AutoCompleteSuggestion) -> Unit, - omnibarPosition: OmnibarPosition, ) = with(binding) { title.text = item.title url.text = item.phrase bookmarkIndicator.setImageResource(if (item.isFavorite) R.drawable.ic_bookmark_favorite_20 else R.drawable.ic_bookmark_20) - goToBookmarkImage.setOnClickListener { editableSearchClickListener(item) } root.setOnClickListener { immediateSearchListener(item) } - if (omnibarPosition == OmnibarPosition.BOTTOM) { - goToBookmarkImage.setImageResource(R.drawable.ic_autocomplete_down_20dp) - } + root.tag = OTHER_ITEM } } @@ -298,23 +312,32 @@ sealed class AutoCompleteViewHolder(itemView: View) : RecyclerView.ViewHolder(it fun bind( item: AutoCompleteHistorySuggestion, immediateSearchListener: (AutoCompleteSuggestion) -> Unit, - editableSearchClickListener: (AutoCompleteSuggestion) -> Unit, longPressClickListener: (AutoCompleteSuggestion) -> Unit, - omnibarPosition: OmnibarPosition, ) = with(binding) { title.text = item.title url.text = item.phrase - goToSuggestionImage.setOnClickListener { editableSearchClickListener(item) } root.setOnClickListener { immediateSearchListener(item) } root.setOnLongClickListener { longPressClickListener(item) true } - if (omnibarPosition == OmnibarPosition.BOTTOM) { - goToSuggestionImage.setImageResource(R.drawable.ic_autocomplete_down_20dp) - } + root.tag = OTHER_ITEM + } + } + + class SwitchToTabSuggestionViewHolder(val binding: ItemAutocompleteSwitchToTabSuggestionBinding) : AutoCompleteViewHolder(binding.root) { + fun bind( + item: AutoCompleteSwitchToTabSuggestion, + immediateSearchListener: (AutoCompleteSuggestion) -> Unit, + ) = with(binding) { + title.text = item.title + url.text = root.context.getString(R.string.autocompleteSwitchToTab, item.phrase) + + root.setOnClickListener { immediateSearchListener(item) } + + root.tag = OTHER_ITEM } } @@ -332,6 +355,8 @@ sealed class AutoCompleteViewHolder(itemView: View) : RecyclerView.ViewHolder(it if (omnibarPosition == OmnibarPosition.BOTTOM) { binding.editQueryImage.setImageResource(R.drawable.ic_autocomplete_down_20dp) } + + binding.root.tag = OTHER_ITEM } } @@ -350,6 +375,8 @@ sealed class AutoCompleteViewHolder(itemView: View) : RecyclerView.ViewHolder(it ) binding.messageCta.onCloseButtonClicked { deleteClickListener(item) } binding.messageCta.onPrimaryActionClicked { openSettingsClickListener() } + + binding.root.tag = OTHER_ITEM } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt index 367727640ad6..76f183008781 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt @@ -245,4 +245,5 @@ sealed class Command { class SetBrowserBackground(@DrawableRes val backgroundRes: Int) : Command() class SetOnboardingDialogBackground(@DrawableRes val backgroundRes: Int) : Command() data class LaunchFireDialogFromOnboardingDialog(val onboardingCta: OnboardingDaxDialogCta) : Command() + data class SwitchToTab(val tabId: String) : Command() } diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index e19f7d63563b..f209c8d0e06a 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -198,6 +198,8 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { AUTOCOMPLETE_HISTORY_SEARCH_SELECTION("m_autocomplete_click_history_search"), AUTOCOMPLETE_HISTORY_SITE_SELECTION("m_autocomplete_click_history_site"), + AUTOCOMPLETE_SWITCH_TO_TAB_SELECTION("m_autocomplete_click_switch_to_tab"), + AUTOCOMPLETE_TOGGLED_OFF("m_autocomplete_recent_sites_toggled_off"), AUTOCOMPLETE_TOGGLED_ON("m_autocomplete_recent_sites_toggled_on"), @@ -218,6 +220,7 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { AUTOCOMPLETE_DISPLAYED_LOCAL_WEBSITE("m_autocomplete_displayed_website"), AUTOCOMPLETE_DISPLAYED_LOCAL_HISTORY("m_autocomplete_displayed_history_site"), AUTOCOMPLETE_DISPLAYED_LOCAL_HISTORY_SEARCH("m_autocomplete_displayed_history_search"), + AUTOCOMPLETE_DISPLAYED_LOCAL_SWITCH_TO_TAB("m_autocomplete_displayed_switch_to_tab"), AUTOCOMPLETE_RESULT_DELETED("m_autocomplete_result_deleted"), AUTOCOMPLETE_RESULT_DELETED_DAILY("m_autocomplete_result_deleted_daily"), diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt index d8c53d5e5692..c72306cd81dc 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -28,6 +28,7 @@ import android.view.inputmethod.EditorInfo import android.widget.TextView import android.widget.Toast import android.widget.Toast.LENGTH_SHORT +import androidx.core.content.ContextCompat import androidx.core.text.toSpannable import androidx.core.view.isVisible import androidx.core.view.postDelayed @@ -41,6 +42,7 @@ import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.R.string import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter +import com.duckduckgo.app.browser.autocomplete.SuggestionItemDecoration import com.duckduckgo.app.browser.databinding.ActivitySystemSearchBinding import com.duckduckgo.app.browser.databinding.IncludeQuickAccessItemsBinding import com.duckduckgo.app.browser.favicon.FaviconManager @@ -170,6 +172,10 @@ class SystemSearchActivity : DuckDuckGoActivity() { } } + fun onAutoCompleteSuggestionsChanged() { + configureAutoComplete() + } + private fun sendLaunchPixels(intent: Intent) { when { launchedFromAssist(intent) -> pixel.fire(AppPixelName.APP_ASSIST_LAUNCH) @@ -228,7 +234,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { binding.autocompleteSuggestions.layoutManager = LinearLayoutManager(this) autocompleteSuggestionsAdapter = BrowserAutoCompleteSuggestionsAdapter( immediateSearchClickListener = { - viewModel.userSubmittedAutocompleteResult(it.phrase) + viewModel.userSubmittedAutocompleteResult(it) }, editableSearchClickListener = { viewModel.onUserSelectedToEditQuery(it.phrase) @@ -243,6 +249,9 @@ class SystemSearchActivity : DuckDuckGoActivity() { omnibarPosition = settingsDataStore.omnibarPosition, ) binding.autocompleteSuggestions.adapter = autocompleteSuggestionsAdapter + binding.autocompleteSuggestions.addItemDecoration( + SuggestionItemDecoration(ContextCompat.getDrawable(this, R.drawable.suggestions_divider)!!), + ) binding.results.setOnScrollChangeListener( NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, _ -> @@ -405,7 +414,11 @@ class SystemSearchActivity : DuckDuckGoActivity() { } is LaunchBrowser -> { - launchBrowser(command) + launchBrowser(command.query) + } + + is LaunchBrowserAndSwitchToTab -> { + launchBrowser(command.query, command.tabId) } is LaunchDeviceApplication -> { @@ -480,6 +493,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { } private fun autocompleteItemRemoved() { + viewModel.onAutoCompleteSuggestionsChanged() showKeyboardAndRestorePosition() } @@ -551,8 +565,15 @@ class SystemSearchActivity : DuckDuckGoActivity() { finish() } - private fun launchBrowser(command: LaunchBrowser) { - startActivity(BrowserActivity.intent(context = this, queryExtra = command.query, interstitialScreen = true)) + private fun launchBrowser(query: String, openExistingTabId: String? = null) { + startActivity( + BrowserActivity.intent( + context = this, + queryExtra = query, + interstitialScreen = true, + openExistingTabId = openExistingTabId, + ), + ) finish() } diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index 4fa9f01abee0..dd64301c8252 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -27,6 +27,7 @@ import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteInAppMessageSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteSwitchToTabSuggestion import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.onboarding.store.AppStage @@ -47,17 +48,19 @@ import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark import com.duckduckgo.savedsites.api.models.SavedSite.Favorite import com.duckduckgo.savedsites.impl.SavedSitesPixelName import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment -import com.jakewharton.rxrelay2.PublishRelay -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -101,6 +104,7 @@ class SystemSearchViewModel @Inject constructor( object ClearInputText : Command() object LaunchDuckDuckGo : Command() data class LaunchBrowser(val query: String) : Command() + data class LaunchBrowserAndSwitchToTab(val query: String, val tabId: String) : Command() data class LaunchEditDialog(val savedSite: SavedSite) : Command() data class DeleteFavoriteConfirmation(val savedSite: SavedSite) : Command() data class DeleteSavedSiteConfirmation(val savedSite: SavedSite) : Command() @@ -118,9 +122,9 @@ class SystemSearchViewModel @Inject constructor( val command: SingleLiveEvent = SingleLiveEvent() @VisibleForTesting - val resultsPublishSubject = PublishRelay.create() + internal val resultsStateFlow = MutableStateFlow("") private var results = SystemSearchResult(AutoCompleteResult("", emptyList()), emptyList()) - private var resultsDisposable: Disposable? = null + private var resultsJob: Job? = null private var latestQuickAccessItems: Suggestions.QuickAccessItems = Suggestions.QuickAccessItems(emptyList()) private var hasUserSeenHistory = false @@ -175,24 +179,26 @@ class SystemSearchViewModel @Inject constructor( resultsViewState.value = latestQuickAccessItems } + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) private fun configureResults() { - resultsDisposable = resultsPublishSubject - .debounce(DEBOUNCE_TIME_MS, TimeUnit.MILLISECONDS) - .switchMap { buildResultsObservable(query = it) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { result -> - updateResults(result) - }, - { t: Throwable? -> Timber.w(t, "Failed to get search results") }, - ) + resultsJob?.cancel() + resultsJob = resultsStateFlow + .debounce(DEBOUNCE_TIME_MS) + .distinctUntilChanged() + .flatMapLatest { buildResultsFlow(query = it) } + .flowOn(dispatchers.io()) + .onEach { result -> + updateResults(result) + } + .flowOn(dispatchers.main()) + .catch { t: Throwable? -> Timber.w(t, "Failed to get search results") } + .launchIn(viewModelScope) } - private fun buildResultsObservable(query: String): Observable? { - return Observable.zip( + private fun buildResultsFlow(query: String): Flow { + return combine( autoComplete.autoComplete(query), - Observable.just(deviceAppLookup.query(query)), + flow { emit(deviceAppLookup.query(query)) }, ) { autocompleteResult: AutoCompleteResult, appsResult: List -> if (autocompleteResult.suggestions.contains(AutoCompleteInAppMessageSuggestion)) { hasUserSeenHistory = true @@ -233,7 +239,7 @@ class SystemSearchViewModel @Inject constructor( if (appSettingsPreferencesStore.autoCompleteSuggestionsEnabled) { val trimmedQuery = query.trim() - resultsPublishSubject.accept(trimmedQuery) + resultsStateFlow.value = trimmedQuery } } @@ -255,17 +261,19 @@ class SystemSearchViewModel @Inject constructor( ) } - is Suggestions.QuickAccessItems -> Suggestions.SystemSearchResultsViewState( - autocompleteResults = AutoCompleteResult(results.autocomplete.query, updatedSuggestions), - appResults = updatedApps, - ) + is Suggestions.QuickAccessItems -> { + Suggestions.SystemSearchResultsViewState( + autocompleteResults = AutoCompleteResult(results.autocomplete.query, updatedSuggestions), + appResults = updatedApps, + ) + } }, ) } private fun inputCleared() { if (appSettingsPreferencesStore.autoCompleteSuggestionsEnabled) { - resultsPublishSubject.accept("") + resultsStateFlow.value = "" } resetResultsState() } @@ -295,8 +303,15 @@ class SystemSearchViewModel @Inject constructor( } } - fun userSubmittedAutocompleteResult(query: String) { - command.value = Command.LaunchBrowser(query) + fun userSubmittedAutocompleteResult(suggestion: AutoCompleteSuggestion) { + when (suggestion) { + is AutoCompleteSwitchToTabSuggestion -> { + command.value = Command.LaunchBrowserAndSwitchToTab(suggestion.phrase, suggestion.tabId) + } + else -> { + command.value = Command.LaunchBrowser(suggestion.phrase) + } + } pixel.fire(INTERSTITIAL_LAUNCH_BROWSER_QUERY) } @@ -307,6 +322,10 @@ class SystemSearchViewModel @Inject constructor( } } + fun onAutoCompleteSuggestionsChanged() { + configureResults() + } + private fun showRemoveSearchSuggestionDialog(suggestion: AutoCompleteSuggestion) { appCoroutineScope.launch(dispatchers.main()) { command.value = Command.ShowRemoveSearchSuggestionDialog(suggestion) @@ -328,7 +347,7 @@ class SystemSearchViewModel @Inject constructor( else -> {} } withContext(dispatchers.main()) { - resultsPublishSubject.accept(omnibarText) + resultsStateFlow.value = omnibarText command.value = Command.AutocompleteItemRemoved } } @@ -352,8 +371,8 @@ class SystemSearchViewModel @Inject constructor( } override fun onCleared() { - resultsDisposable?.dispose() - resultsDisposable = null + resultsJob?.cancel() + resultsJob = null super.onCleared() } diff --git a/app/src/main/res/drawable/ic_autocomplete_20dp.xml b/app/src/main/res/drawable/ic_autocomplete_20dp.xml index 35043ec29bfd..f2c6e574b447 100644 --- a/app/src/main/res/drawable/ic_autocomplete_20dp.xml +++ b/app/src/main/res/drawable/ic_autocomplete_20dp.xml @@ -1,5 +1,5 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + android:pathData="M15.25,5a0.75,0.75 0,0 0,0 -1.5h-11a0.75,0.75 0,0 0,-0.75 0.75v11a0.75,0.75 0,0 0,1.5 0V6.06l10.22,10.22a0.75,0.75 0,1 0,1.06 -1.06L6.06,5h9.19Z" + android:fillColor="?attr/daxColorSecondaryText"/> diff --git a/app/src/main/res/drawable/ic_autocomplete_down_20dp.xml b/app/src/main/res/drawable/ic_autocomplete_down_20dp.xml index 2edaf5453ce0..91de2914d746 100644 --- a/app/src/main/res/drawable/ic_autocomplete_down_20dp.xml +++ b/app/src/main/res/drawable/ic_autocomplete_down_20dp.xml @@ -1,3 +1,19 @@ + + + android:fillColor="?attr/daxColorSecondaryText"/> diff --git a/app/src/main/res/drawable/ic_switch_to_tab_20.xml b/app/src/main/res/drawable/ic_switch_to_tab_20.xml new file mode 100644 index 000000000000..8860a3f5d23e --- /dev/null +++ b/app/src/main/res/drawable/ic_switch_to_tab_20.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/suggestions_divider.xml b/app/src/main/res/drawable/suggestions_divider.xml new file mode 100644 index 000000000000..c111e7320c8f --- /dev/null +++ b/app/src/main/res/drawable/suggestions_divider.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_autocomplete_bookmark_suggestion.xml b/app/src/main/res/layout/item_autocomplete_bookmark_suggestion.xml index ca16a97c84c0..1d8944727f2d 100644 --- a/app/src/main/res/layout/item_autocomplete_bookmark_suggestion.xml +++ b/app/src/main/res/layout/item_autocomplete_bookmark_suggestion.xml @@ -47,7 +47,7 @@ android:includeFontPadding="false" android:gravity="center_vertical|start" android:maxLines="1" - app:layout_constraintEnd_toStartOf="@id/goToBookmarkImage" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/bookmarkIndicator" app:layout_constraintTop_toTopOf="parent" tools:text="phrase or URL suggestion"/> @@ -62,23 +62,10 @@ android:ellipsize="end" android:gravity="center_vertical|start" android:maxLines="1" - app:layout_constraintEnd_toStartOf="@id/goToBookmarkImage" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/bookmarkIndicator" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/title" tools:text="phrase or URL suggestion"/> - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_autocomplete_default.xml b/app/src/main/res/layout/item_autocomplete_default.xml index 478d3ac282df..fd3368f919ae 100644 --- a/app/src/main/res/layout/item_autocomplete_default.xml +++ b/app/src/main/res/layout/item_autocomplete_default.xml @@ -59,7 +59,10 @@ android:layout_height="wrap_content" android:layout_marginStart="6dp" android:contentDescription="@string/editQueryBeforeSubmitting" - android:padding="3dp" + android:paddingStart="5dp" + android:paddingTop="3dp" + android:paddingEnd="1dp" + android:paddingBottom="3dp" android:src="@drawable/ic_autocomplete_20dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/item_autocomplete_history_search_suggestion.xml b/app/src/main/res/layout/item_autocomplete_history_search_suggestion.xml new file mode 100644 index 000000000000..93241ccfef51 --- /dev/null +++ b/app/src/main/res/layout/item_autocomplete_history_search_suggestion.xml @@ -0,0 +1,55 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/item_autocomplete_history_suggestion.xml b/app/src/main/res/layout/item_autocomplete_history_suggestion.xml index d801e97d9e61..c089a59245fe 100644 --- a/app/src/main/res/layout/item_autocomplete_history_suggestion.xml +++ b/app/src/main/res/layout/item_autocomplete_history_suggestion.xml @@ -48,7 +48,7 @@ android:gravity="center_vertical|start" android:maxLines="1" android:textAlignment="textStart" - app:layout_constraintEnd_toStartOf="@id/goToSuggestionImage" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/historyIndicator" app:layout_constraintTop_toTopOf="parent" tools:text="phrase or URL suggestion"/> @@ -64,23 +64,10 @@ android:gravity="center_vertical|start" android:maxLines="1" android:textAlignment="textStart" - app:layout_constraintEnd_toStartOf="@id/goToSuggestionImage" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/historyIndicator" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/title" tools:text="phrase or URL suggestion"/> - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_autocomplete_search_suggestion.xml b/app/src/main/res/layout/item_autocomplete_search_suggestion.xml index 4da36a46f166..7831b6c7045c 100644 --- a/app/src/main/res/layout/item_autocomplete_search_suggestion.xml +++ b/app/src/main/res/layout/item_autocomplete_search_suggestion.xml @@ -60,7 +60,10 @@ android:layout_marginStart="6dp" android:background="?selectableItemBackgroundBorderless" android:contentDescription="@string/editQueryBeforeSubmitting" - android:padding="3dp" + android:paddingStart="5dp" + android:paddingTop="3dp" + android:paddingEnd="1dp" + android:paddingBottom="3dp" android:src="@drawable/ic_autocomplete_20dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/item_autocomplete_switch_to_tab_suggestion.xml b/app/src/main/res/layout/item_autocomplete_switch_to_tab_suggestion.xml new file mode 100644 index 000000000000..ad935ae24a5a --- /dev/null +++ b/app/src/main/res/layout/item_autocomplete_switch_to_tab_suggestion.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_legacy_omnibar.xml b/app/src/main/res/layout/view_legacy_omnibar.xml index 2a94f1b7cf43..69399f5a9351 100644 --- a/app/src/main/res/layout/view_legacy_omnibar.xml +++ b/app/src/main/res/layout/view_legacy_omnibar.xml @@ -86,6 +86,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/keyline_1" + android:paddingStart="@dimen/keyline_0" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -178,7 +179,7 @@ android:imeOptions="flagNoExtractUi|actionGo|flagNoPersonalizedLearning" android:inputType="textUri|textNoSuggestions" android:maxLines="1" - android:paddingStart="5dp" + android:paddingStart="3dp" android:selectAllOnFocus="true" android:textColor="?attr/daxColorPrimaryText" android:textColorHighlight="?attr/daxOmnibarTextColorHighlight" diff --git a/app/src/main/res/layout/view_legacy_omnibar_bottom.xml b/app/src/main/res/layout/view_legacy_omnibar_bottom.xml index 9e26fdb6a8be..90cbcc385770 100644 --- a/app/src/main/res/layout/view_legacy_omnibar_bottom.xml +++ b/app/src/main/res/layout/view_legacy_omnibar_bottom.xml @@ -85,6 +85,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/keyline_1" + android:paddingStart="@dimen/keyline_0" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -177,7 +178,7 @@ android:imeOptions="flagNoExtractUi|actionGo|flagNoPersonalizedLearning" android:inputType="textUri|textNoSuggestions" android:maxLines="1" - android:paddingStart="5dp" + android:paddingStart="3dp" android:selectAllOnFocus="true" android:textColor="?attr/daxColorPrimaryText" android:textColorHighlight="?attr/daxOmnibarTextColorHighlight" diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 843340586255..7a580e8813f0 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -89,6 +89,7 @@ Премахване на предложение за търсене? Отмени Премахване + Превключване към раздел · %1$s Отваряне на външно приложение diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 3b0bbf8ad3a0..54b78a505191 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -89,6 +89,7 @@ Odebrat návrh vyhledávání? Zrušit Odstranit + Přepnout na kartu · %1$s Otevřít externí aplikaci diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 18cabcc6833d..526cc86d4a32 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -89,6 +89,7 @@ Fjern søgeforslag? Annuller Fjern + Skift til fane · %1$s Åbn ekstern app diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1dfb96f1b47e..34bfa38a2309 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -89,6 +89,7 @@ Suchvorschlag entfernen? Abbrechen Entfernen + Zu Tab wechseln · %1$s Externe App öffnen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 4b11b6eaca48..0cf780188a99 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -89,6 +89,7 @@ Αφαίρεση πρότασης αναζήτησης; Ακύρωση Αφαίρεση + Αλλαγή στην Καρτέλα · %1$s Άνοιγμα εξωτερικής εφαρμογής diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2d5e107d8e26..355b5d88921e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -89,6 +89,7 @@ ¿Eliminar sugerencia de búsqueda? Cancelar Eliminar + Cambiar a la pestaña · %1$s Abrir aplicación externa diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index c6b66ad5479e..b5db52e69f2a 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -89,6 +89,7 @@ Kas eemaldada otsingusoovitus? Loobu Eemaldage + Vaheta vahekaardile · %1$s Ava väline rakendus diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a1b527f5034a..08719ba1ab6f 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -89,6 +89,7 @@ Poistetaanko hakuehdotus? Peruuta Poista + Vaihda välilehdelle · %1$s Avaa ulkoinen sovellus diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f2e594f2f6b4..9d71ecaf916a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -89,6 +89,7 @@ Supprimer la suggestion de recherche ? Annuler Supprimer + Passer à l\'onglet · %1$s Ouvrir une application externe diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 880114c4c980..35d6c0c63603 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -89,6 +89,7 @@ Ukloni prijedlog za pretraživanje? Odustani Ukloni + Prebaci se na karticu · %1$s Otvori vanjsku aplikaciju diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index b5d76dbee45e..ad254e2708fe 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -89,6 +89,7 @@ Eltávolítod a keresési javaslatot? Mégsem Eltávolítás + Váltás lapra · %1$s Külső alkalmazás megnyitása diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 57041957ac9a..70c241dd4c40 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -89,6 +89,7 @@ Rimuovere il suggerimento di ricerca? Annulla Rimuovi + Passa alla scheda · %1$s Apri l\'app esterna diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index dd4c6d73f462..957034f55653 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -89,6 +89,7 @@ Pašalinti paieškos pasiūlymą? Atšaukti Pašalinti + Perjungti į skirtuką · %1$s Atidaryti išorinę programą diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index dd15f7c8c38e..36ae4d0b4ed0 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -89,6 +89,7 @@ Vai noņemt meklēšanas ieteikumu? Atcelt Noņemt + Pārslēgties uz cilni · %1$s Atvērt ārējo lietotni diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 909f5da4bb6e..c5ecd8008623 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -89,6 +89,7 @@ Vil du fjerne søkeforslaget? Avbryt Fjern + Bytt til fane · %1$s Åpne ekstern app diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 21bf0814b937..f5cbdfe24c12 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -89,6 +89,7 @@ Zoeksuggestie verwijderen? Annuleren Verwijderen + Overschakelen naar tabblad · %1$s Externe app openen diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 5661f1a7c7a9..8b7017990e9c 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -89,6 +89,7 @@ Usunąć sugestię wyszukiwania? Anuluj Usuń + Przełącz na kartę - %1$s Otwórz zewnętrzną aplikację diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 2f55db639d60..b4cc9e92fca5 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -89,6 +89,7 @@ Remover sugestão de pesquisa? Cancelar Remover + Mudar para separador · %1$s Abrir aplicação externa diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 933e7ba2db32..107b30ebf1ad 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -89,6 +89,7 @@ Dorești să elimini sugestia de căutare? Anulare Elimină + Comută la fila %1$s Deschide aplicația externă diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c967d8ec44b8..3c32edcd15da 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -89,6 +89,7 @@ Удалить поисковую рекомендацию? Отменить Удалить + Перейти на вкладку · %1$s Открыть внешнее приложение diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 20cf1cd168b7..8a99df2adbb6 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -89,6 +89,7 @@ Odstrániť návrh vyhľadávania? Zrušiť Odstrániť + Prepnúť na kartu · %1$s Otvoriť externú aplikáciu diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index ca4d6e055218..783bc98803e9 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -89,6 +89,7 @@ Želite odstraniti predlog iskanja? Prekliči Odstrani + Preklopi na zavihek · %1$s Odpri zunanjo aplikacijo diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 421be2ee8b78..866b1683df9b 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -89,6 +89,7 @@ Ta bort sökförslag? Avbryt Ta bort + Byt till flik · %1$s Öppna extern app diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 04d6ff1c5c38..b144f45b7e03 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -89,6 +89,7 @@ Arama önerisi kaldırılsın mı? Vazgeç Kaldır + Sekmeye Geç · %1$s Haricî Uygulamayı Aç diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4b5305daaeb2..499fcd9338f3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,6 +88,7 @@ Remove search suggestion? Cancel Remove + Switch to Tab · %1$s Open External App diff --git a/app/src/test/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt b/app/src/test/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt index 8393c69173d3..494be477bede 100644 --- a/app/src/test/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt +++ b/app/src/test/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt @@ -18,20 +18,24 @@ package com.duckduckgo.app.autocomplete.api import androidx.core.net.toUri import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult +import com.duckduckgo.app.autocomplete.AutocompleteTabsFeature import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion -import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteDefaultSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteInAppMessageSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteBookmarkSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteSwitchToTabSuggestion import com.duckduckgo.app.autocomplete.impl.AutoCompleteRepository import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.AppStage.NEW import com.duckduckgo.app.onboarding.store.UserStageStore +import com.duckduckgo.app.tabs.model.TabEntity +import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.formatters.time.DatabaseDateFormatter +import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.history.api.HistoryEntry.VisitedPage import com.duckduckgo.history.api.HistoryEntry.VisitedSERP import com.duckduckgo.history.api.NavigationHistory @@ -39,11 +43,10 @@ import com.duckduckgo.savedsites.api.SavedSitesRepository import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark import com.duckduckgo.savedsites.api.models.SavedSite.Favorite import com.duckduckgo.savedsites.api.models.SavedSitesNames -import io.reactivex.Observable -import io.reactivex.Single -import java.io.InterruptedIOException import java.time.LocalDateTime import java.util.UUID +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Before @@ -70,9 +73,18 @@ class AutoCompleteApiTest { @Mock private lateinit var mockAutoCompleteRepository: AutoCompleteRepository + @Mock + private lateinit var mockTabRepository: TabRepository + @Mock private lateinit var mockUserStageStore: UserStageStore + @Mock + private lateinit var mockAutocompleteTabsFeature: AutocompleteTabsFeature + + @Mock + private lateinit var mockToggle: Toggle + @get:Rule val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() @@ -81,49 +93,52 @@ class AutoCompleteApiTest { @Before fun before() { MockitoAnnotations.openMocks(this) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn(Single.just(listOf())) + whenever(mockTabRepository.flowTabs).thenReturn(flowOf(listOf(TabEntity("1", position = 1)))) + whenever(mockNavigationHistory.getHistory()).thenReturn(flowOf(emptyList())) runTest { whenever(mockUserStageStore.getUserAppStage()).thenReturn(NEW) } + whenever(mockAutocompleteTabsFeature.self()).thenReturn(mockToggle) + whenever(mockToggle.isEnabled()).thenReturn(true) testee = AutoCompleteApi( mockAutoCompleteService, mockSavedSitesRepository, mockNavigationHistory, RealAutoCompleteScorer(), mockAutoCompleteRepository, + mockTabRepository, mockUserStageStore, coroutineTestRule.testDispatcherProvider, + mockAutocompleteTabsFeature, ) } @Test - fun whenQueryIsBlankThenReturnAnEmptyList() { - val result = testee.autoComplete("").test() - val value = result.values()[0] as AutoCompleteResult + fun whenQueryIsBlankThenReturnAnEmptyList() = runTest { + val result = testee.autoComplete("") + val value = result.first() assertTrue(value.suggestions.isEmpty()) } @Test - fun whenReturnBookmarkSuggestionsThenPhraseIsURLBaseHost() { - runTest { - whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(emptyList())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn(Single.just(bookmarks())) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + fun whenReturnBookmarkSuggestionsThenPhraseIsURLBaseHost() = runTest { + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn(flowOf(bookmarks())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() - assertEquals("example.com", value.suggestions[0].phrase) - } + assertEquals("example.com", value.suggestions[0].phrase) } @Test - fun whenAutoCompleteDoesNotMatchAnySavedSiteReturnDefault() { - whenever(mockAutoCompleteService.autoComplete("wrong")).thenReturn(Observable.just(emptyList())) + fun whenAutoCompleteDoesNotMatchAnySavedSiteReturnDefault() = runTest { + whenever(mockAutoCompleteService.autoComplete("wrong")).thenReturn(emptyList()) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark( title = "title", @@ -132,32 +147,30 @@ class AutoCompleteApiTest { ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(listOf(favorite(title = "title")))) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(listOf(favorite(title = "title")))) - val result = testee.autoComplete("wrong").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("wrong") + val value = result.first() assertEquals(value.suggestions.first(), AutoCompleteDefaultSuggestion("wrong")) } @Test - fun whenAutoCompleteReturnsMultipleBookmarkAndFavoriteHitsThenBothShowBeforeSearchSuggestionsAndFavoritesShowFirst() { + fun whenAutoCompleteReturnsMultipleBookmarkAndFavoriteHitsThenBothShowBeforeSearchSuggestionsAndFavoritesShowFirst() = runTest { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( - Observable.just( - listOf( - AutoCompleteServiceRawResult("foo", isNav = false), - ), + listOf( + AutoCompleteServiceRawResult("foo", isNav = false), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( listOf( favorite(title = "title", url = "https://example.com"), ), ), ) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "title", url = "https://bar.com"), bookmark(title = "title", url = "https://baz.com"), @@ -165,8 +178,8 @@ class AutoCompleteApiTest { ), ) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -180,49 +193,46 @@ class AutoCompleteApiTest { } @Test - fun whenAutoCompleteReturnsMultipleBookmarkAndFavoriteHitsWithBookmarksAlsoInHistoryThenBookmarksShowBeforeSearchSuggestions() { + fun whenAutoCompleteReturnsMultipleTabAndBookmarkAndFavoriteHitsThenBothShowBeforeSearchSuggestionsAndFavoritesShowFirst() = runTest { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( - Observable.just( - listOf( - AutoCompleteServiceRawResult("foo", isNav = false), - ), + listOf( + AutoCompleteServiceRawResult("foo", isNav = false), ), ) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( listOf( - VisitedPage( - title = "title", - url = "https://bar.com".toUri(), - visits = listOf(LocalDateTime.now(), LocalDateTime.now(), LocalDateTime.now()), - ), + favorite(title = "title", url = "https://example.com"), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( - favorite(title = "title", url = "https://example.com"), + bookmark(title = "title", url = "https://bar.com"), + bookmark(title = "title", url = "https://baz.com"), ), ), ) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + + whenever(mockTabRepository.flowTabs).thenReturn( + flowOf( listOf( - bookmark(title = "title", url = "https://bar.com"), - bookmark(title = "title", url = "https://baz.com"), + TabEntity(tabId = "1", position = 1, title = "title", url = "https://bar.com"), + TabEntity(tabId = "2", position = 2, title = "title", url = "https://baz.com"), ), ), ) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( - AutoCompleteBookmarkSuggestion(phrase = "bar.com", "title", "https://bar.com", isFavorite = false), AutoCompleteBookmarkSuggestion(phrase = "example.com", "title", "https://example.com", isFavorite = true), + AutoCompleteSwitchToTabSuggestion(phrase = "bar.com", "title", "https://bar.com", tabId = "1"), AutoCompleteSearchSuggestion("foo", false), + AutoCompleteSwitchToTabSuggestion(phrase = "baz.com", title = "title", url = "https://baz.com", tabId = "2"), AutoCompleteBookmarkSuggestion(phrase = "baz.com", "title", "https://baz.com", isFavorite = false), ), value.suggestions, @@ -230,39 +240,215 @@ class AutoCompleteApiTest { } @Test - fun whenAutoCompleteReturnsHistoryItemsWithLessThan3VisitsButRootPageTheyShowBeforeSuggestions() { + fun whenAutoCompleteReturnsDuplicatedTabsAndBookmarkAndFavoriteHitsThenTabSuggestionsAreNotDuplicatedAndFirstTabPositionIsChosen() = + runTest { + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( + listOf( + AutoCompleteServiceRawResult("foo", isNav = false), + ), + ) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( + listOf( + favorite(title = "title", url = "https://example.com"), + ), + ), + ) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( + listOf( + bookmark(title = "title", url = "https://bar.com"), + bookmark(title = "title", url = "https://baz.com"), + ), + ), + ) + + whenever(mockTabRepository.flowTabs).thenReturn( + flowOf( + listOf( + TabEntity(tabId = "1", position = 1, title = "title", url = "https://bar.com"), + TabEntity(tabId = "2", position = 2, title = "title", url = "https://bar.com"), + TabEntity(tabId = "3", position = 3, title = "title", url = "https://bar.com"), + TabEntity(tabId = "4", position = 4, title = "title", url = "https://baz.com"), + TabEntity(tabId = "5", position = 5, title = "title", url = "https://baz.com"), + TabEntity(tabId = "6", position = 6, title = "title", url = "https://baz.com"), + ), + ), + ) + + val result = testee.autoComplete("title") + val value = result.first() + + assertEquals( + listOf( + AutoCompleteBookmarkSuggestion(phrase = "example.com", "title", "https://example.com", isFavorite = true), + AutoCompleteSwitchToTabSuggestion(phrase = "bar.com", "title", "https://bar.com", tabId = "1"), + AutoCompleteSearchSuggestion("foo", false), + AutoCompleteSwitchToTabSuggestion(phrase = "baz.com", title = "title", url = "https://baz.com", tabId = "4"), + AutoCompleteBookmarkSuggestion(phrase = "baz.com", "title", "https://baz.com", isFavorite = false), + ), + value.suggestions, + ) + } + + @Test + fun whenAutoCompleteReturnsMultipleVariousResultsThenOnlyMax12AreShown() = runTest { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( - Observable.just( + listOf( + AutoCompleteServiceRawResult("aaa", isNav = false), + AutoCompleteServiceRawResult("bbb", isNav = false), + AutoCompleteServiceRawResult("ccc", isNav = false), + AutoCompleteServiceRawResult("ddd", isNav = false), + AutoCompleteServiceRawResult("eee", isNav = false), + AutoCompleteServiceRawResult("fff", isNav = false), + AutoCompleteServiceRawResult("ggg", isNav = false), + AutoCompleteServiceRawResult("hhh", isNav = false), + ), + ) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( + listOf( + favorite(title = "title", url = "https://iii.com"), + ), + ), + ) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( + listOf( + bookmark(title = "title", url = "https://iii.com"), + bookmark(title = "title", url = "https://jjj.com"), + bookmark(title = "title", url = "https://kkk.com"), + ), + ), + ) + + whenever(mockTabRepository.flowTabs).thenReturn( + flowOf( + listOf( + TabEntity(tabId = "1", position = 1, title = "title", url = "https://lll.com"), + TabEntity(tabId = "2", position = 2, title = "title", url = "https://mmm.com"), + TabEntity(tabId = "3", position = 3, title = "title", url = "https://nnn.com"), + TabEntity(tabId = "4", position = 4, title = "title", url = "https://ooo.com"), + TabEntity(tabId = "5", position = 5, title = "title", url = "https://ppp.com"), + TabEntity(tabId = "6", position = 6, title = "title", url = "https://qqq.com"), + TabEntity(tabId = "6", position = 6, title = "title", url = "https://iii.com"), + TabEntity(tabId = "6", position = 6, title = "title", url = "https://jjj.com"), + ), + ), + ) + + val result = testee.autoComplete("title") + val value = result.first() + + assertEquals(12, value.suggestions.size) + assertEquals( + listOf( + AutoCompleteBookmarkSuggestion(phrase = "iii.com", title = "title", url = "https://iii.com", isFavorite = true), + AutoCompleteSwitchToTabSuggestion(phrase = "lll.com", title = "title", url = "https://lll.com", tabId = "1"), + AutoCompleteSearchSuggestion(phrase = "aaa", isUrl = false), + AutoCompleteSearchSuggestion(phrase = "bbb", isUrl = false), + AutoCompleteSearchSuggestion(phrase = "ccc", isUrl = false), + AutoCompleteSearchSuggestion(phrase = "ddd", isUrl = false), + AutoCompleteSearchSuggestion(phrase = "eee", isUrl = false), + AutoCompleteSwitchToTabSuggestion(phrase = "mmm.com", title = "title", url = "https://mmm.com", tabId = "2"), + AutoCompleteSwitchToTabSuggestion(phrase = "nnn.com", title = "title", url = "https://nnn.com", tabId = "3"), + AutoCompleteSwitchToTabSuggestion(phrase = "ooo.com", title = "title", url = "https://ooo.com", tabId = "4"), + AutoCompleteSwitchToTabSuggestion(phrase = "ppp.com", title = "title", url = "https://ppp.com", tabId = "5"), + AutoCompleteSwitchToTabSuggestion(phrase = "qqq.com", title = "title", url = "https://qqq.com", tabId = "6"), + ), + value.suggestions, + ) + } + + @Test + fun whenAutoCompleteReturnsMultipleBookmarkAndFavoriteHitsWithBookmarksAlsoInHistoryThenBookmarksShowBeforeSearchSuggestions() = + runTest { + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( listOf( AutoCompleteServiceRawResult("foo", isNav = false), ), + ) + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf( + listOf( + VisitedPage( + title = "title", + url = "https://bar.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now(), LocalDateTime.now()), + ), + ), + ), + ) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( + listOf( + favorite(title = "title", url = "https://example.com"), + ), + ), + ) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( + listOf( + bookmark(title = "title", url = "https://bar.com"), + bookmark(title = "title", url = "https://baz.com"), + ), + ), + ) + + val result = testee.autoComplete("title") + val value = result.first() + + assertEquals( + listOf( + AutoCompleteBookmarkSuggestion(phrase = "bar.com", "title", "https://bar.com", isFavorite = false), + AutoCompleteBookmarkSuggestion(phrase = "example.com", "title", "https://example.com", isFavorite = true), + AutoCompleteSearchSuggestion("foo", false), + AutoCompleteBookmarkSuggestion(phrase = "baz.com", "title", "https://baz.com", isFavorite = false), + ), + value.suggestions, + ) + } + + @Test + fun whenAutoCompleteReturnsHistoryItemsWithLessThan3VisitsButRootPageTheyShowBeforeSuggestions() = runTest { + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( + listOf( + AutoCompleteServiceRawResult("foo", isNav = false), ), ) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just( + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf( listOf( - VisitedPage(title = "title", url = "https://bar.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), - VisitedPage(title = "title", url = "https://foo.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), + VisitedPage( + title = "title", + url = "https://bar.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), + VisitedPage( + title = "title", + url = "https://foo.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( listOf( favorite(title = "title", url = "https://example.com"), ), ), ) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "title", url = "https://baz.com"), ), ), ) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -277,16 +463,14 @@ class AutoCompleteApiTest { } @Test - fun whenAutoCompleteReturnsDuplicateHistorySerpWithMoreThan3CombinedVisitsTheyShowBeforeSuggestions() { + fun whenAutoCompleteReturnsDuplicateHistorySerpWithMoreThan3CombinedVisitsTheyShowBeforeSuggestions() = runTest { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( - Observable.just( - listOf( - AutoCompleteServiceRawResult("foo", isNav = false), - ), + listOf( + AutoCompleteServiceRawResult("foo", isNav = false), ), ) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just( + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf( listOf( VisitedSERP( "https://duckduckgo.com?q=query".toUri(), @@ -303,11 +487,11 @@ class AutoCompleteApiTest { ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(listOf())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn(Single.just(listOf())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(listOf())) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn(flowOf(listOf())) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -319,27 +503,25 @@ class AutoCompleteApiTest { } @Test - fun whenAutoCompleteReturnsDuplicateHistorySerpWithLessThan3CombinedVisitsTheyDoNotShowBeforeSuggestions() { + fun whenAutoCompleteReturnsDuplicateHistorySerpWithLessThan3CombinedVisitsTheyDoNotShowBeforeSuggestions() = runTest { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( - Observable.just( - listOf( - AutoCompleteServiceRawResult("foo", isNav = false), - ), + listOf( + AutoCompleteServiceRawResult("foo", isNav = false), ), ) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just( + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf( listOf( VisitedSERP("https://duckduckgo.com?q=query".toUri(), "title", "query", visits = listOf(LocalDateTime.now())), VisitedSERP("https://duckduckgo.com?q=query&atb=1".toUri(), "title", "query", visits = listOf(LocalDateTime.now())), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(listOf())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn(Single.just(listOf())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(listOf())) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn(flowOf(listOf())) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -351,39 +533,45 @@ class AutoCompleteApiTest { } @Test - fun whenAutoCompleteReturnsHistoryItemsWithLessThan3VisitsAndNotRootPageTheyDoNotShowBeforeSuggestions() { + fun whenAutoCompleteReturnsHistoryItemsWithLessThan3VisitsAndNotRootPageTheyDoNotShowBeforeSuggestions() = runTest { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( - Observable.just( - listOf( - AutoCompleteServiceRawResult("foo", isNav = false), - ), + listOf( + AutoCompleteServiceRawResult("foo", isNav = false), ), ) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just( + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf( listOf( - VisitedPage(title = "title", url = "https://bar.com/test".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), - VisitedPage(title = "title", url = "https://foo.com/test".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), + VisitedPage( + title = "title", + url = "https://bar.com/test".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), + VisitedPage( + title = "title", + url = "https://foo.com/test".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( listOf( favorite(title = "title", url = "https://example.com"), ), ), ) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "title", url = "https://baz.com"), ), ), ) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -398,16 +586,14 @@ class AutoCompleteApiTest { } @Test - fun whenAutoCompleteReturnsMultipleFavoriteHitsLimitTopHitsTo2() { + fun whenAutoCompleteReturnsMultipleFavoriteHitsLimitTopHitsTo2() = runTest { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( - Observable.just( - listOf( - AutoCompleteServiceRawResult("foo", isNav = false), - ), + listOf( + AutoCompleteServiceRawResult("foo", isNav = false), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( listOf( favorite(title = "title", url = "https://example.com"), favorite(title = "title", url = "https://foo.com"), @@ -415,14 +601,14 @@ class AutoCompleteApiTest { ), ), ) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf(), ), ) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -436,10 +622,10 @@ class AutoCompleteApiTest { } @Test - fun whenAutoCompleteReturnsMultipleSavedSitesHitsThenShowFavoritesFirst() { - whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(emptyList())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + fun whenAutoCompleteReturnsMultipleSavedSitesHitsThenShowFavoritesFirst() = runTest { + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "title", url = "https://example.com"), bookmark(title = "title", url = "https://foo.com"), @@ -448,8 +634,8 @@ class AutoCompleteApiTest { ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( listOf( favorite(title = "title", url = "https://favexample.com"), favorite(title = "title", url = "https://favfoo.com"), @@ -459,8 +645,8 @@ class AutoCompleteApiTest { ), ) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertTrue((value.suggestions[0] as AutoCompleteBookmarkSuggestion).isFavorite) assertTrue((value.suggestions[1] as AutoCompleteBookmarkSuggestion).isFavorite) @@ -470,19 +656,17 @@ class AutoCompleteApiTest { } @Test - fun whenAutoCompleteReturnsDuplicatedItemsThenDedup() { + fun whenAutoCompleteReturnsDuplicatedItemsThenDedup() = runTest { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( - Observable.just( - listOf( - AutoCompleteServiceRawResult("example.com", false), - AutoCompleteServiceRawResult("foo.com", true), - AutoCompleteServiceRawResult("bar.com", true), - AutoCompleteServiceRawResult("baz.com", true), - ), + listOf( + AutoCompleteServiceRawResult("example.com", false), + AutoCompleteServiceRawResult("foo.com", true), + AutoCompleteServiceRawResult("bar.com", true), + AutoCompleteServiceRawResult("baz.com", true), ), ) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "title example", url = "https://example.com"), bookmark(title = "title foo", url = "https://foo.com/path/to/foo"), @@ -491,8 +675,8 @@ class AutoCompleteApiTest { ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( listOf( favorite(title = "title example", url = "https://example.com"), favorite(title = "title foo", url = "https://foo.com/path/to/foo"), @@ -502,8 +686,8 @@ class AutoCompleteApiTest { ), ) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -538,10 +722,10 @@ class AutoCompleteApiTest { } @Test - fun whenReturnOneBookmarkAndOneFavoriteSuggestionsThenShowBothFavoriteFirst() { - whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(emptyList())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + fun whenReturnOneBookmarkAndOneFavoriteSuggestionsThenShowBothFavoriteFirst() = runTest { + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark( title = "title", @@ -550,8 +734,8 @@ class AutoCompleteApiTest { ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( listOf( favorite( title = "title", @@ -561,8 +745,8 @@ class AutoCompleteApiTest { ), ) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -584,19 +768,17 @@ class AutoCompleteApiTest { } @Test - fun whenAutoCompleteReturnsDuplicatedItemsThenDedupConsideringQueryParams() { + fun whenAutoCompleteReturnsDuplicatedItemsThenDedupConsideringQueryParams() = runTest { whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( - Observable.just( - listOf( - AutoCompleteServiceRawResult("example.com", false), - AutoCompleteServiceRawResult("foo.com", true), - AutoCompleteServiceRawResult("bar.com", true), - AutoCompleteServiceRawResult("baz.com", true), - ), + listOf( + AutoCompleteServiceRawResult("example.com", false), + AutoCompleteServiceRawResult("foo.com", true), + AutoCompleteServiceRawResult("bar.com", true), + AutoCompleteServiceRawResult("baz.com", true), ), ) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "title foo", url = "https://foo.com?key=value"), bookmark(title = "title foo", url = "https://foo.com"), @@ -604,18 +786,14 @@ class AutoCompleteApiTest { ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( - AutoCompleteBookmarkSuggestion( - phrase = "foo.com?key=value", - "title foo", - "https://foo.com?key=value", - ), + AutoCompleteBookmarkSuggestion(phrase = "foo.com?key=value", "title foo", "https://foo.com?key=value"), AutoCompleteBookmarkSuggestion(phrase = "foo.com", "title foo", "https://foo.com"), AutoCompleteSearchSuggestion(phrase = "example.com", false), AutoCompleteSearchSuggestion(phrase = "baz.com", true), @@ -626,10 +804,10 @@ class AutoCompleteApiTest { } @Test - fun whenBookmarkTitleStartsWithQueryThenScoresHigher() { - whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(listOf())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + fun whenBookmarkTitleStartsWithQueryThenScoresHigher() = runTest { + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "the title example", url = "https://example.com"), bookmark(title = "the title foo", url = "https://foo.com/path/to/foo"), @@ -638,10 +816,10 @@ class AutoCompleteApiTest { ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals(AutoCompleteBookmarkSuggestion(phrase = "bar.com", "title bar", "https://bar.com"), value.suggestions[0]) assertEquals( @@ -655,10 +833,10 @@ class AutoCompleteApiTest { } @Test - fun whenSingleTokenQueryAndBookmarkDomainStartsWithItThenScoreHigher() { - whenever(mockAutoCompleteService.autoComplete("foo")).thenReturn(Observable.just(listOf())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + fun whenSingleTokenQueryAndBookmarkDomainStartsWithItThenScoreHigher() = runTest { + whenever(mockAutoCompleteService.autoComplete("foo")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "title example", url = "https://example.com"), bookmark(title = "title bar", url = "https://bar.com"), @@ -667,10 +845,10 @@ class AutoCompleteApiTest { ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete("foo").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("foo") + val value = result.first() assertEquals( listOf( @@ -681,10 +859,10 @@ class AutoCompleteApiTest { } @Test - fun whenSingleTokenQueryAndBookmarkReturnsDuplicatedItemsThenDedup() { - whenever(mockAutoCompleteService.autoComplete("cnn")).thenReturn(Observable.just(listOf())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + fun whenSingleTokenQueryAndBookmarkReturnsDuplicatedItemsThenDedup() = runTest { + whenever(mockAutoCompleteService.autoComplete("cnn")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "CNN international", url = "https://cnn.com"), bookmark(title = "CNN international", url = "https://cnn.com"), @@ -692,10 +870,10 @@ class AutoCompleteApiTest { ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete("cnn").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("cnn") + val value = result.first() assertEquals( listOf( @@ -711,20 +889,20 @@ class AutoCompleteApiTest { } @Test - fun whenSingleTokenQueryEndsWithSlashThenIgnoreItWhileMatching() { - whenever(mockAutoCompleteService.autoComplete("reddit.com/")).thenReturn(Observable.just(listOf())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + fun whenSingleTokenQueryEndsWithSlashThenIgnoreItWhileMatching() = runTest { + whenever(mockAutoCompleteService.autoComplete("reddit.com/")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "Reddit", url = "https://reddit.com"), bookmark(title = "Reddit - duckduckgo", url = "https://reddit.com/r/duckduckgo"), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete("reddit.com/").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("reddit.com/") + val value = result.first() assertEquals( listOf( @@ -740,20 +918,20 @@ class AutoCompleteApiTest { } @Test - fun whenSingleTokenQueryEndsWithMultipleSlashThenIgnoreThemWhileMatching() { - whenever(mockAutoCompleteService.autoComplete("reddit.com///")).thenReturn(Observable.just(listOf())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + fun whenSingleTokenQueryEndsWithMultipleSlashThenIgnoreThemWhileMatching() = runTest { + whenever(mockAutoCompleteService.autoComplete("reddit.com///")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "Reddit", url = "https://reddit.com"), bookmark(title = "Reddit - duckduckgo", url = "https://reddit.com/r/duckduckgo"), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete("reddit.com///").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("reddit.com///") + val value = result.first() assertEquals( listOf( @@ -769,20 +947,20 @@ class AutoCompleteApiTest { } @Test - fun whenSingleTokenQueryContainsMultipleSlashThenIgnoreThemWhileMatching() { - whenever(mockAutoCompleteService.autoComplete("reddit.com/r//")).thenReturn(Observable.just(listOf())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + fun whenSingleTokenQueryContainsMultipleSlashThenIgnoreThemWhileMatching() = runTest { + whenever(mockAutoCompleteService.autoComplete("reddit.com/r//")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "Reddit", url = "https://reddit.com"), bookmark(title = "Reddit - duckduckgo", url = "https://reddit.com/r/duckduckgo"), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete("reddit.com/r//").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("reddit.com/r//") + val value = result.first() assertEquals( listOf( @@ -797,26 +975,26 @@ class AutoCompleteApiTest { } @Test - fun whenSingleTokenQueryDomainContainsWwwThenResultMathUrl() { - whenever(mockAutoCompleteService.autoComplete("reddit")).thenReturn(Observable.just(listOf())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + fun whenSingleTokenQueryDomainContainsWwwThenResultMathUrl() = runTest { + whenever(mockAutoCompleteService.autoComplete("reddit")).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "Reddit", url = "https://www.reddit.com"), bookmark(title = "duckduckgo", url = "https://www.reddit.com/r/duckduckgo"), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete("reddit").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("reddit") + val value = result.first() assertEquals( listOf( - AutoCompleteBookmarkSuggestion(phrase = "www.reddit.com", "Reddit", "https://www.reddit.com"), + AutoCompleteBookmarkSuggestion(phrase = "reddit.com", "Reddit", "https://www.reddit.com"), AutoCompleteBookmarkSuggestion( - phrase = "www.reddit.com/r/duckduckgo", + phrase = "reddit.com/r/duckduckgo", "duckduckgo", "https://www.reddit.com/r/duckduckgo", ), @@ -826,11 +1004,11 @@ class AutoCompleteApiTest { } @Test - fun whenMultipleTokenQueryAndNoTokenMatchThenReturnDefault() { + fun whenMultipleTokenQueryAndNoTokenMatchThenReturnDefault() = runTest { val query = "example title foo" - whenever(mockAutoCompleteService.autoComplete(query)).thenReturn(Observable.just(listOf())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + whenever(mockAutoCompleteService.autoComplete(query)).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf( bookmark(title = "title example", url = "https://example.com"), bookmark(title = "title bar", url = "https://bar.com"), @@ -839,75 +1017,83 @@ class AutoCompleteApiTest { ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete(query).test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete(query) + val value = result.first() assertEquals(listOf(AutoCompleteDefaultSuggestion("example title foo")), value.suggestions) } @Test - fun whenMultipleTokenQueryAndMultipleMatchesThenReturnCorrectScore() { - runTest { - val query = "title foo" - whenever(mockAutoCompleteService.autoComplete(query)).thenReturn(Observable.just(listOf())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( - listOf( - bookmark(title = "title example", url = "https://example.com"), - bookmark(title = "title bar", url = "https://bar.com"), - bookmark(title = "the title foo", url = "https://foo.com"), - bookmark(title = "title foo baz", url = "https://baz.com"), - ), + fun whenMultipleTokenQueryAndMultipleMatchesThenReturnCorrectScore() = runTest { + val query = "title foo" + whenever(mockAutoCompleteService.autoComplete(query)).thenReturn(emptyList()) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( + listOf( + bookmark(title = "title example", url = "https://example.com"), + bookmark(title = "title bar", url = "https://bar.com"), + bookmark(title = "the title foo", url = "https://foo.com"), + bookmark(title = "title foo baz", url = "https://baz.com"), ), - ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + ), + ) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete(query).test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete(query) + val value = result.first() - assertEquals( - listOf( - AutoCompleteBookmarkSuggestion(phrase = "baz.com", "title foo baz", "https://baz.com"), - AutoCompleteBookmarkSuggestion(phrase = "foo.com", "the title foo", "https://foo.com"), - ), - value.suggestions, - ) - } + assertEquals( + listOf( + AutoCompleteBookmarkSuggestion(phrase = "baz.com", "title foo baz", "https://baz.com"), + AutoCompleteBookmarkSuggestion(phrase = "foo.com", "the title foo", "https://foo.com"), + ), + value.suggestions, + ) } @Test - fun whenAutoCompleteQueryIsCapitalizedButResultsAreNotThenIgnoreCapitalization() { + fun whenAutoCompleteQueryIsCapitalizedButResultsAreNotThenIgnoreCapitalization() = runTest { whenever(mockAutoCompleteService.autoComplete("Title")).thenReturn( - Observable.just( - listOf( - AutoCompleteServiceRawResult("foo", isNav = false), - ), + listOf( + AutoCompleteServiceRawResult("foo", isNav = false), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( listOf(), ), ) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( listOf(), ), ) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just( + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf( listOf( - VisitedPage(title = "Title", url = "https://example.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), - VisitedPage(title = "Title", url = "https://foo.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), - VisitedPage(title = "Title", url = "https://bar.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), + VisitedPage( + title = "Title", + url = "https://example.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), + VisitedPage( + title = "Title", + url = "https://foo.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), + VisitedPage( + title = "Title", + url = "https://bar.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), ), ), ) - val result = testee.autoComplete("Title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("Title") + val value = result.first() assertEquals( listOf( @@ -921,48 +1107,56 @@ class AutoCompleteApiTest { } @Test - fun whenAutoCompleteQueryIsNotCapitalizedButResultsAreThenIgnoreCapitalization() { - runTest { - whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( - Observable.just( - listOf( - AutoCompleteServiceRawResult("foo", isNav = false), + fun whenAutoCompleteQueryIsNotCapitalizedButResultsAreThenIgnoreCapitalization() = runTest { + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn( + listOf( + AutoCompleteServiceRawResult("foo", isNav = false), + ), + ) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn( + flowOf( + listOf(), + ), + ) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn( + flowOf( + listOf(), + ), + ) + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf( + listOf( + VisitedPage( + title = "Title", + url = "https://example.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), ), - ), - ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn( - Single.just( - listOf(), - ), - ) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn( - Single.just( - listOf(), - ), - ) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just( - listOf( - VisitedPage(title = "Title", url = "https://example.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), - VisitedPage(title = "Title", url = "https://foo.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), - VisitedPage(title = "Title", url = "https://bar.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), + VisitedPage( + title = "Title", + url = "https://foo.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), + VisitedPage( + title = "Title", + url = "https://bar.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), ), ), - ) + ), + ) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() - assertEquals( - listOf( - AutoCompleteHistorySuggestion(phrase = "example.com", "Title", "https://example.com", isAllowedInTopHits = true), - AutoCompleteHistorySuggestion(phrase = "foo.com", "Title", "https://foo.com", isAllowedInTopHits = true), - AutoCompleteSearchSuggestion("foo", false), - AutoCompleteHistorySuggestion(phrase = "bar.com", "Title", "https://bar.com", isAllowedInTopHits = true), - ), - value.suggestions, - ) - } + assertEquals( + listOf( + AutoCompleteHistorySuggestion(phrase = "example.com", "Title", "https://example.com", isAllowedInTopHits = true), + AutoCompleteHistorySuggestion(phrase = "foo.com", "Title", "https://foo.com", isAllowedInTopHits = true), + AutoCompleteSearchSuggestion("foo", false), + AutoCompleteHistorySuggestion(phrase = "bar.com", "Title", "https://bar.com", isAllowedInTopHits = true), + ), + value.suggestions, + ) } @Test @@ -970,21 +1164,29 @@ class AutoCompleteApiTest { runTest { whenever(mockAutoCompleteRepository.countHistoryInAutoCompleteIAMShown()).thenReturn(0) whenever(mockAutoCompleteRepository.wasHistoryInAutoCompleteIAMDismissed()).thenReturn(false) - whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(listOf())) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just( + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(emptyList()) + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf( listOf( - VisitedPage(title = "title", url = "https://bar.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), - VisitedPage(title = "title", url = "https://foo.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), + VisitedPage( + title = "title", + url = "https://bar.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), + VisitedPage( + title = "title", + url = "https://foo.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn(flowOf(emptyList())) whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.ESTABLISHED) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -1002,21 +1204,29 @@ class AutoCompleteApiTest { runTest { whenever(mockAutoCompleteRepository.countHistoryInAutoCompleteIAMShown()).thenReturn(0) whenever(mockAutoCompleteRepository.wasHistoryInAutoCompleteIAMDismissed()).thenReturn(false) - whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(listOf())) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just( + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(emptyList()) + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf( listOf( - VisitedPage(title = "title", url = "https://bar.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), - VisitedPage(title = "title", url = "https://foo.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), + VisitedPage( + title = "title", + url = "https://bar.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), + VisitedPage( + title = "title", + url = "https://foo.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn(flowOf(emptyList())) whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -1033,20 +1243,28 @@ class AutoCompleteApiTest { runTest { whenever(mockAutoCompleteRepository.countHistoryInAutoCompleteIAMShown()).thenReturn(3) whenever(mockAutoCompleteRepository.wasHistoryInAutoCompleteIAMDismissed()).thenReturn(false) - whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(listOf())) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just( + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(emptyList()) + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf( listOf( - VisitedPage(title = "title", url = "https://bar.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), - VisitedPage(title = "title", url = "https://foo.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), + VisitedPage( + title = "title", + url = "https://bar.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), + VisitedPage( + title = "title", + url = "https://foo.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -1063,20 +1281,28 @@ class AutoCompleteApiTest { runTest { whenever(mockAutoCompleteRepository.countHistoryInAutoCompleteIAMShown()).thenReturn(0) whenever(mockAutoCompleteRepository.wasHistoryInAutoCompleteIAMDismissed()).thenReturn(true) - whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(Observable.just(listOf())) - whenever(mockNavigationHistory.getHistorySingle()).thenReturn( - Single.just( + whenever(mockAutoCompleteService.autoComplete("title")).thenReturn(emptyList()) + whenever(mockNavigationHistory.getHistory()).thenReturn( + flowOf( listOf( - VisitedPage(title = "title", url = "https://bar.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), - VisitedPage(title = "title", url = "https://foo.com".toUri(), visits = listOf(LocalDateTime.now(), LocalDateTime.now())), + VisitedPage( + title = "title", + url = "https://bar.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), + VisitedPage( + title = "title", + url = "https://foo.com".toUri(), + visits = listOf(LocalDateTime.now(), LocalDateTime.now()), + ), ), ), ) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete("title").test() - val value = result.values()[0] as AutoCompleteResult + val result = testee.autoComplete("title") + val value = result.first() assertEquals( listOf( @@ -1098,32 +1324,39 @@ class AutoCompleteApiTest { } @Test - fun whenInterruptedIOExceptionThenReturnEmptyObservable() { + fun whenOtherExceptionThenReturnDefaultSuggestion() = runTest { val query = "example title foo" - whenever(mockAutoCompleteService.autoComplete(query)).thenReturn(Observable.error(InterruptedIOException())) + whenever(mockAutoCompleteService.autoComplete(query)).thenThrow(RuntimeException()) - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn(Single.just(emptyList())) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) + whenever(mockSavedSitesRepository.getBookmarks()).thenReturn(flowOf(emptyList())) + whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - val result = testee.autoComplete(query).test() + val result = testee.autoComplete(query) + val value = result.first() - result.assertNoValues() - result.assertComplete() + assertEquals(listOf(AutoCompleteDefaultSuggestion(query)), value.suggestions) } @Test - fun whenOtherExceptionThenReturnDefaultSuggestion() { - val query = "example title foo" - whenever(mockAutoCompleteService.autoComplete(query)).thenReturn(Observable.error(RuntimeException())) - - whenever(mockSavedSitesRepository.getBookmarksObservable()).thenReturn(Single.just(emptyList())) - whenever(mockSavedSitesRepository.getFavoritesObservable()).thenReturn(Single.just(emptyList())) - - val result = testee.autoComplete(query).test() - val value = result.values()[0] as AutoCompleteResult - - assertEquals(listOf(AutoCompleteDefaultSuggestion(query)), value.suggestions) - result.assertComplete() + fun whenFormatIfUrlCalledOnStringThenTheStringHasExpectedPrefixAndSuffixRemoved() { + assertEquals("example.com", "example.com".formatIfUrl()) + assertEquals("example.com", "example.com/".formatIfUrl()) + assertEquals("example.com", "www.example.com".formatIfUrl()) + assertEquals("example.com", "www.example.com/".formatIfUrl()) + assertEquals("example.com", "https://example.com".formatIfUrl()) + assertEquals("example.com", "https://example.com/".formatIfUrl()) + assertEquals("example.com", "https://www.example.com/".formatIfUrl()) + assertEquals("example.com", "https://www.example.com".formatIfUrl()) + assertEquals("example.com", "http://example.com".formatIfUrl()) + assertEquals("example.com", "http://example.com/".formatIfUrl()) + assertEquals("example.com", "http://www.example.com/".formatIfUrl()) + assertEquals("example.com", "http://www.example.com".formatIfUrl()) + assertEquals("example.com/path?query1=1&query2=1", "example.com/path?query1=1&query2=1".formatIfUrl()) + assertEquals("example.com/path?query1=1&query2=1", "www.example.com/path?query1=1&query2=1".formatIfUrl()) + assertEquals("example.com/path?query1=1&query2=1", "http://example.com/path?query1=1&query2=1".formatIfUrl()) + assertEquals("example.com/path?query1=1&query2=1", "http://www.example.com/path?query1=1&query2=1".formatIfUrl()) + assertEquals("example.com/path?query1=1&query2=1", "https://example.com/path?query1=1&query2=1".formatIfUrl()) + assertEquals("example.com/path?query1=1&query2=1", "https://www.example.com/path?query1=1&query2=1".formatIfUrl()) } private fun favorite( diff --git a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index feef944ee93b..acaab6d828d8 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -267,6 +267,15 @@ class BrowserViewModelTest { verify(mockOmnibarEntryConverter).convertQueryToUrl("query") } + @Test + fun whenOnTabSelectedCalledWithTabIdThenSelectTabWithTheSameId() = runTest { + val tabId = "tabId" + + testee.onTabSelected(tabId) + + verify(mockTabRepository).select(tabId) + } + private fun configureSkipUrlConversionInNewTabState(enabled: Boolean) { skipUrlConversionOnNewTabFeature.self().setRawStoredState(State(enable = enabled)) } diff --git a/app/src/test/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt index 4f46aa2b2b61..a96807506251 100644 --- a/app/src/test/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.systemsearch import android.content.Intent import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import com.duckduckgo.app.autocomplete.api.AutoComplete import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult @@ -25,6 +26,7 @@ import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.A import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteSwitchToTabSuggestion import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter.QuickAccessFavorite import com.duckduckgo.app.onboarding.store.* import com.duckduckgo.app.pixels.AppPixelName.* @@ -44,8 +46,6 @@ import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.savedsites.api.SavedSitesRepository import com.duckduckgo.savedsites.api.models.SavedSite.Favorite import com.duckduckgo.savedsites.impl.SavedSitesPixelName -import io.reactivex.Observable -import io.reactivex.observers.TestObserver import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.* @@ -81,8 +81,8 @@ class SystemSearchViewModelTest { @Before fun setup() { - whenever(mockAutoComplete.autoComplete(QUERY)).thenReturn(Observable.just(autocompleteQueryResult)) - whenever(mockAutoComplete.autoComplete(BLANK_QUERY)).thenReturn(Observable.just(autocompleteBlankResult)) + whenever(mockAutoComplete.autoComplete(QUERY)).thenReturn(flowOf(autocompleteQueryResult)) + whenever(mockAutoComplete.autoComplete(BLANK_QUERY)).thenReturn(flowOf(autocompleteBlankResult)) whenever(mockDeviceAppLookup.query(QUERY)).thenReturn(appQueryResult) whenever(mockDeviceAppLookup.query(BLANK_QUERY)).thenReturn(appBlankResult) whenever(mocksavedSitesRepository.getFavorites()).thenReturn(flowOf()) @@ -169,10 +169,14 @@ class SystemSearchViewModelTest { fun whenUserUpdatesQueryThenViewStateUpdated() = runTest { testee.userUpdatedQuery(QUERY) - val newViewState = testee.resultsViewState.value as SystemSearchResultsViewState - assertNotNull(newViewState) - assertEquals(appQueryResult, newViewState.appResults) - assertEquals(autocompleteQueryResult, newViewState.autocompleteResults) + val observer = Observer { state -> + val newViewState = state as SystemSearchResultsViewState + assertNotNull(newViewState) + assertEquals(appQueryResult, newViewState.appResults) + assertEquals(autocompleteQueryResult, newViewState.autocompleteResults) + } + + testee.resultsViewState.observeAndSkipFirstEvent(observer) } @Test @@ -180,10 +184,14 @@ class SystemSearchViewModelTest { testee.userUpdatedQuery(QUERY) testee.userUpdatedQuery("$QUERY ") - val newViewState = testee.resultsViewState.value as SystemSearchResultsViewState - assertNotNull(newViewState) - assertEquals(appQueryResult, newViewState.appResults) - assertEquals(autocompleteQueryResult, newViewState.autocompleteResults) + val observer = Observer { state -> + val newViewState = state as SystemSearchResultsViewState + assertNotNull(newViewState) + assertEquals(appQueryResult, newViewState.appResults) + assertEquals(autocompleteQueryResult, newViewState.autocompleteResults) + } + + testee.resultsViewState.observeAndSkipFirstEvent(observer) } @Test @@ -191,10 +199,14 @@ class SystemSearchViewModelTest { doReturn(true).whenever(mockSettingsStore).autoCompleteSuggestionsEnabled testee.userUpdatedQuery(QUERY) - val newViewState = testee.resultsViewState.value as SystemSearchResultsViewState - assertNotNull(newViewState) - assertEquals(appQueryResult, newViewState.appResults) - assertEquals(autocompleteQueryResult, newViewState.autocompleteResults) + val observer = Observer { state -> + val newViewState = state as SystemSearchResultsViewState + assertNotNull(newViewState) + assertEquals(appQueryResult, newViewState.appResults) + assertEquals(autocompleteQueryResult, newViewState.autocompleteResults) + } + + testee.resultsViewState.observeAndSkipFirstEvent(observer) } @Test @@ -252,12 +264,24 @@ class SystemSearchViewModelTest { @Test fun whenUserSubmitsAutocompleteResultThenBrowserLaunchedAndPixelSent() { - testee.userSubmittedAutocompleteResult(AUTOCOMPLETE_RESULT) + testee.userSubmittedAutocompleteResult(AutoCompleteSearchSuggestion(phrase = AUTOCOMPLETE_RESULT, isUrl = false)) verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) assertEquals(Command.LaunchBrowser(AUTOCOMPLETE_RESULT), commandCaptor.lastValue) verify(mockPixel).fire(INTERSTITIAL_LAUNCH_BROWSER_QUERY) } + @Test + fun whenUserSubmitsAutocompleteResultToOpenInTabThenBrowserLaunchedAndPixelSent() { + val phrase = "phrase" + val tabId = "tabId" + + testee.userSubmittedAutocompleteResult(AutoCompleteSwitchToTabSuggestion(phrase, "title", "https://example.com", tabId)) + + verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertEquals(Command.LaunchBrowserAndSwitchToTab(phrase, tabId), commandCaptor.lastValue) + verify(mockPixel).fire(INTERSTITIAL_LAUNCH_BROWSER_QUERY) + } + @Test fun whenUserSelectsAppResultThenAppLaunchedAndPixelSent() { testee.userSelectedApp(deviceApp) @@ -515,15 +539,11 @@ class SystemSearchViewModelTest { val suggestion = AutoCompleteHistorySuggestion(phrase = "phrase", title = "title", url = "url", isAllowedInTopHits = false) val omnibarText = "foo" - val testObserver = TestObserver.create() - testee.resultsPublishSubject.subscribe(testObserver) - testee.onRemoveSearchSuggestionConfirmed(suggestion, omnibarText) verify(mockPixel).fire(AUTOCOMPLETE_RESULT_DELETED) verify(mockPixel).fire(AUTOCOMPLETE_RESULT_DELETED_DAILY, type = Daily()) verify(mockHistory).removeHistoryEntryByUrl(suggestion.url) - testObserver.assertValue(omnibarText) assertCommandIssued() } @@ -532,15 +552,11 @@ class SystemSearchViewModelTest { val suggestion = AutoCompleteHistorySearchSuggestion(phrase = "phrase", isAllowedInTopHits = false) val omnibarText = "foo" - val testObserver = TestObserver.create() - testee.resultsPublishSubject.subscribe(testObserver) - testee.onRemoveSearchSuggestionConfirmed(suggestion, omnibarText) verify(mockPixel).fire(AUTOCOMPLETE_RESULT_DELETED) verify(mockPixel).fire(AUTOCOMPLETE_RESULT_DELETED_DAILY, type = Daily()) verify(mockHistory).removeHistoryEntryByQuery(suggestion.phrase) - testObserver.assertValue(omnibarText) assertCommandIssued() } @@ -565,6 +581,18 @@ class SystemSearchViewModelTest { } } + private fun MutableLiveData.observeAndSkipFirstEvent(observer: Observer) { + var skipFirstEvent = true + observeForever { value -> + if (skipFirstEvent) { + skipFirstEvent = false + return@observeForever + } + observer.onChanged(value) + removeObserver(observer) + } + } + companion object { const val QUERY = "abc" const val BLANK_QUERY = "" diff --git a/duckplayer/duckplayer-impl/src/main/res/values-et/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-et/strings-duckplayer.xml index 0646765d3cc2..87eba68c6974 100644 --- a/duckplayer/duckplayer-impl/src/main/res/values-et/strings-duckplayer.xml +++ b/duckplayer/duckplayer-impl/src/main/res/values-et/strings-duckplayer.xml @@ -16,7 +16,7 @@ --> - + Salvesta Loobu Duck Player @@ -25,13 +25,17 @@ Alati Mitte kunagi Küsi iga kord - Kas avada YouTube\'i videod Duck Playeris? + Kas avada Youtube\'i videod Duck Playeris? Sulge - Kas näed YouTube\'i kasutades massiliselt reklaame? Mitte Duck Playeriga! - Kas näed YouTube\'i kasutades massiliselt reklaame? Proovi Duck Playerit! + Kas näed Youtube\'i kasutades massiliselt reklaame? Mitte Duck Playeriga! + Kas näed Youtube\'i kasutades massiliselt reklaame? Proovi Duck Playerit! Duck Player võimaldab sul DuckDuckGo-s YouTube\'i vaadata ilma sihitud reklaamideta ja see, mida vaatad, ei mõjuta sinu soovitusi. Sain aru! Duck Player ei ole saadaval Duck Playeri funktsionaalsust on mõjutanud hiljutised YouTube\'i muudatused. Töötame nende probleemide lahendamise nimel ja täname sind mõistva suhtumise eest. Loe edasi + + Kas avada Youtube\'i videod Duck Playeris? + + Ava Duck Player uues vahekaardis \ No newline at end of file diff --git a/history/history-api/build.gradle b/history/history-api/build.gradle index 2c106fe2f5d1..07d8f6f12693 100644 --- a/history/history-api/build.gradle +++ b/history/history-api/build.gradle @@ -34,8 +34,6 @@ dependencies { implementation KotlinX.coroutines.core implementation AndroidX.appCompat - implementation "io.reactivex.rxjava2:rxjava:_" - coreLibraryDesugaring Android.tools.desugarJdkLibs } diff --git a/history/history-api/src/main/java/com/duckduckgo/history/api/NavigationHistory.kt b/history/history-api/src/main/java/com/duckduckgo/history/api/NavigationHistory.kt index c9761e27c08e..1fdc00fd3b67 100644 --- a/history/history-api/src/main/java/com/duckduckgo/history/api/NavigationHistory.kt +++ b/history/history-api/src/main/java/com/duckduckgo/history/api/NavigationHistory.kt @@ -16,7 +16,7 @@ package com.duckduckgo.history.api -import io.reactivex.Single +import kotlinx.coroutines.flow.Flow interface NavigationHistory { @@ -30,10 +30,9 @@ interface NavigationHistory { /** * Retrieves all [HistoryEntry]. - * @return [Single] of all [HistoryEntry]. + * @return [Flow] of all [HistoryEntry]. */ - @Deprecated("RxJava is deprecated, except for Auto-Complete") - fun getHistorySingle(): Single> + fun getHistory(): Flow> /** * Clears all history entries. diff --git a/history/history-impl/build.gradle b/history/history-impl/build.gradle index 860483f56087..b3cbb6918f33 100644 --- a/history/history-impl/build.gradle +++ b/history/history-impl/build.gradle @@ -42,7 +42,6 @@ dependencies { implementation Google.dagger implementation "com.squareup.logcat:logcat:_" - implementation "io.reactivex.rxjava2:rxjava:_" implementation AndroidX.room.runtime implementation AndroidX.room.rxJava2 implementation AndroidX.room.ktx diff --git a/history/history-impl/src/main/java/com/duckduckgo/history/impl/HistoryRepository.kt b/history/history-impl/src/main/java/com/duckduckgo/history/impl/HistoryRepository.kt index 4c9e547b6d6a..a484df6b8db8 100644 --- a/history/history-impl/src/main/java/com/duckduckgo/history/impl/HistoryRepository.kt +++ b/history/history-impl/src/main/java/com/duckduckgo/history/impl/HistoryRepository.kt @@ -20,14 +20,16 @@ import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.history.api.HistoryEntry import com.duckduckgo.history.impl.store.HistoryDao import com.duckduckgo.history.impl.store.HistoryDataStore -import io.reactivex.Single import java.time.LocalDateTime import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext interface HistoryRepository { - fun getHistoryObservable(): Single> + fun getHistory(): Flow> suspend fun saveToHistory( url: String, @@ -60,21 +62,11 @@ class RealHistoryRepository( private var cachedHistoryEntries: List? = null - override fun getHistoryObservable(): Single> { - return if (cachedHistoryEntries != null) { - Single.just(cachedHistoryEntries) - } else { - Single.create { emitter -> - appCoroutineScope.launch(dispatcherProvider.io()) { - try { - emitter.onSuccess(fetchAndCacheHistoryEntries()) - } catch (e: Exception) { - emitter.onError(e) - } - } - } - } - } + override fun getHistory(): Flow> = runCatching { + flow { + emit(cachedHistoryEntries ?: fetchAndCacheHistoryEntries()) + }.flowOn(dispatcherProvider.io()) + }.getOrElse { flowOf(emptyList()) } override suspend fun saveToHistory( url: String, diff --git a/history/history-impl/src/main/java/com/duckduckgo/history/impl/RealNavigationHistory.kt b/history/history-impl/src/main/java/com/duckduckgo/history/impl/RealNavigationHistory.kt index 1a22ef02db8e..8986d6d0b03a 100644 --- a/history/history-impl/src/main/java/com/duckduckgo/history/impl/RealNavigationHistory.kt +++ b/history/history-impl/src/main/java/com/duckduckgo/history/impl/RealNavigationHistory.kt @@ -24,8 +24,9 @@ import com.duckduckgo.history.api.HistoryEntry import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.history.impl.remoteconfig.HistoryFeature import com.squareup.anvil.annotations.ContributesBinding -import io.reactivex.Single import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking interface InternalNavigationHistory : NavigationHistory { @@ -54,9 +55,9 @@ class RealNavigationHistory @Inject constructor( historyRepository.saveToHistory(url, title, query, query != null) } - override fun getHistorySingle(): Single> { + override fun getHistory(): Flow> { val isHistoryUserEnabled = runBlocking(dispatcherProvider.io()) { isHistoryUserEnabled() } - return if (isHistoryFeatureAvailable() && isHistoryUserEnabled) historyRepository.getHistoryObservable() else Single.just(emptyList()) + return if (isHistoryFeatureAvailable() && isHistoryUserEnabled) historyRepository.getHistory() else flowOf(emptyList()) } override suspend fun clearHistory() { diff --git a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 70265ac755b5..c8c240bee445 100644 --- a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -31,9 +31,11 @@ interface Pixel { const val BOOKMARK_CAPABLE = "bc" const val FAVORITE_CAPABLE = "fc" const val HISTORY_CAPABLE = "hc" + const val SWITCH_TO_TAB_CAPABLE = "switch_to_tab_capable" const val SHOWED_BOOKMARKS = "sb" const val SHOWED_FAVORITES = "sf" const val SHOWED_HISTORY = "sh" + const val SHOWED_SWITCH_TO_TAB = "showed_switch_to_tab" const val DEFAULT_BROWSER_BEHAVIOUR_TRIGGERED = "bt" const val DEFAULT_BROWSER_SET_FROM_ONBOARDING = "fo" const val DEFAULT_BROWSER_SET_ORIGIN = "dbo" From 030b78b989f2fb24bad38b9763cfc4e9e26c5385 Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Wed, 23 Oct 2024 13:00:33 +0100 Subject: [PATCH 3/3] Aligned icons and text. Updated alignment. --- app/src/main/res/layout/activity_system_search.xml | 5 ++++- app/src/main/res/layout/include_omnibar_toolbar_mockup.xml | 1 + .../res/layout/item_autocomplete_bookmark_suggestion.xml | 7 ++++--- app/src/main/res/layout/item_autocomplete_default.xml | 5 +++-- .../layout/item_autocomplete_history_search_suggestion.xml | 5 +++-- .../res/layout/item_autocomplete_history_suggestion.xml | 7 ++++--- .../res/layout/item_autocomplete_search_suggestion.xml | 5 +++-- .../layout/item_autocomplete_switch_to_tab_suggestion.xml | 7 ++++--- app/src/main/res/layout/view_legacy_omnibar.xml | 3 +-- app/src/main/res/layout/view_legacy_omnibar_bottom.xml | 3 +-- 10 files changed, 28 insertions(+), 20 deletions(-) diff --git a/app/src/main/res/layout/activity_system_search.xml b/app/src/main/res/layout/activity_system_search.xml index 0ec87195df6e..be909aafe628 100644 --- a/app/src/main/res/layout/activity_system_search.xml +++ b/app/src/main/res/layout/activity_system_search.xml @@ -60,7 +60,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:importantForAccessibility="no" - android:padding="4dp" + android:paddingTop="4dp" + android:paddingBottom="4dp" + android:paddingStart="3dp" + android:paddingEnd="7dp" app:srcCompat="@drawable/ic_ddg_logo" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/include_omnibar_toolbar_mockup.xml b/app/src/main/res/layout/include_omnibar_toolbar_mockup.xml index 51726fa04ec8..39b7874283a4 100644 --- a/app/src/main/res/layout/include_omnibar_toolbar_mockup.xml +++ b/app/src/main/res/layout/include_omnibar_toolbar_mockup.xml @@ -53,6 +53,7 @@ android:layout_height="match_parent" android:gravity="start|center" android:maxLines="1" + android:paddingStart="5dp" android:paddingEnd="8dp" android:text="@string/omnibarInputHint" android:textColor="?attr/daxColorSecondaryText" /> diff --git a/app/src/main/res/layout/item_autocomplete_bookmark_suggestion.xml b/app/src/main/res/layout/item_autocomplete_bookmark_suggestion.xml index 1d8944727f2d..cef4e1181b72 100644 --- a/app/src/main/res/layout/item_autocomplete_bookmark_suggestion.xml +++ b/app/src/main/res/layout/item_autocomplete_bookmark_suggestion.xml @@ -25,7 +25,7 @@ android:background="?attr/selectableItemBackground" android:paddingTop="@dimen/keyline_2" android:paddingEnd="@dimen/keyline_4" - android:paddingStart="20dp" + android:paddingStart="@dimen/keyline_4" android:paddingBottom="@dimen/keyline_2"> @@ -31,6 +31,7 @@ android:id="@+id/phraseOrUrlIndicator" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_margin="@dimen/keyline_0" android:importantForAccessibility="no" android:src="@drawable/ic_history" app:layout_constraintBottom_toBottomOf="parent" @@ -41,7 +42,7 @@ android:id="@+id/phrase" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="10dp" + android:layout_marginStart="15dp" android:ellipsize="end" android:gravity="center_vertical|start" android:includeFontPadding="false" diff --git a/app/src/main/res/layout/item_autocomplete_history_suggestion.xml b/app/src/main/res/layout/item_autocomplete_history_suggestion.xml index c089a59245fe..aacebfefe64b 100644 --- a/app/src/main/res/layout/item_autocomplete_history_suggestion.xml +++ b/app/src/main/res/layout/item_autocomplete_history_suggestion.xml @@ -25,13 +25,14 @@ android:background="?attr/selectableItemBackground" android:paddingTop="@dimen/keyline_2" android:paddingEnd="@dimen/keyline_4" - android:paddingStart="20dp" + android:paddingStart="@dimen/keyline_4" android:paddingBottom="@dimen/keyline_2"> @@ -31,6 +31,7 @@ android:id="@+id/switchToTabIndicator" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_margin="@dimen/keyline_0" android:importantForAccessibility="no" android:src="@drawable/ic_switch_to_tab_20" app:layout_constraintBottom_toBottomOf="parent" @@ -41,7 +42,7 @@ android:id="@+id/title" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="10dp" + android:layout_marginStart="15dp" android:ellipsize="end" android:gravity="center_vertical|start" android:includeFontPadding="false" @@ -55,7 +56,7 @@ android:id="@+id/url" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="10dp" + android:layout_marginStart="15dp" android:ellipsize="end" android:gravity="center_vertical|start" android:maxLines="1" diff --git a/app/src/main/res/layout/view_legacy_omnibar.xml b/app/src/main/res/layout/view_legacy_omnibar.xml index 69399f5a9351..2a94f1b7cf43 100644 --- a/app/src/main/res/layout/view_legacy_omnibar.xml +++ b/app/src/main/res/layout/view_legacy_omnibar.xml @@ -86,7 +86,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/keyline_1" - android:paddingStart="@dimen/keyline_0" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -179,7 +178,7 @@ android:imeOptions="flagNoExtractUi|actionGo|flagNoPersonalizedLearning" android:inputType="textUri|textNoSuggestions" android:maxLines="1" - android:paddingStart="3dp" + android:paddingStart="5dp" android:selectAllOnFocus="true" android:textColor="?attr/daxColorPrimaryText" android:textColorHighlight="?attr/daxOmnibarTextColorHighlight" diff --git a/app/src/main/res/layout/view_legacy_omnibar_bottom.xml b/app/src/main/res/layout/view_legacy_omnibar_bottom.xml index 90cbcc385770..9e26fdb6a8be 100644 --- a/app/src/main/res/layout/view_legacy_omnibar_bottom.xml +++ b/app/src/main/res/layout/view_legacy_omnibar_bottom.xml @@ -85,7 +85,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/keyline_1" - android:paddingStart="@dimen/keyline_0" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -178,7 +177,7 @@ android:imeOptions="flagNoExtractUi|actionGo|flagNoPersonalizedLearning" android:inputType="textUri|textNoSuggestions" android:maxLines="1" - android:paddingStart="3dp" + android:paddingStart="5dp" android:selectAllOnFocus="true" android:textColor="?attr/daxColorPrimaryText" android:textColorHighlight="?attr/daxOmnibarTextColorHighlight"