Skip to content

Commit bf0dba3

Browse files
committed
Merge branch 'release/5.30.0'
2 parents 277e3cb + b10d2f4 commit bf0dba3

34 files changed

+769
-141
lines changed

app/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ android {
1919
versionName buildVersionName()
2020
testInstrumentationRunner "com.duckduckgo.app.TestRunner"
2121
archivesBaseName = "duckduckgo-$versionName"
22+
vectorDrawables.useSupportLibrary = true
2223

2324
javaCompileOptions {
2425
annotationProcessorOptions {
@@ -85,7 +86,7 @@ ext {
8586
fragmentKtx = "1.0.0"
8687
constraintLayout = "2.0.0-beta1"
8788
lifecycle = "2.1.0-alpha04"
88-
room = "2.1.0-alpha05"
89+
room = "2.1.0"
8990
workManager = "2.0.0"
9091
legacySupport = "1.0.0"
9192
espressoCore = "3.1.1"
@@ -153,6 +154,8 @@ dependencies {
153154

154155
// Room
155156
implementation "androidx.room:room-runtime:$room"
157+
implementation "androidx.room:room-rxjava2:$room"
158+
implementation "androidx.room:room-ktx:$room"
156159
kapt "androidx.room:room-compiler:$room"
157160
testImplementation "androidx.room:room-testing:$room"
158161
androidTestImplementation "androidx.room:room-testing:$room"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright (c) 2019 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.autocomplete.api
18+
19+
import com.duckduckgo.app.bookmarks.db.BookmarkEntity
20+
import com.duckduckgo.app.bookmarks.db.BookmarksDao
21+
import com.duckduckgo.app.settings.db.SettingsDataStore
22+
import com.nhaarman.mockitokotlin2.never
23+
import com.nhaarman.mockitokotlin2.verify
24+
import com.nhaarman.mockitokotlin2.whenever
25+
import io.reactivex.Observable
26+
import io.reactivex.Single
27+
import org.junit.Test
28+
29+
import org.junit.Assert.*
30+
import org.junit.Before
31+
import org.mockito.ArgumentMatchers.anyString
32+
import org.mockito.Mock
33+
import org.mockito.MockitoAnnotations
34+
35+
class AutoCompleteApiTest {
36+
37+
@Mock
38+
private lateinit var mockAutoCompleteService: AutoCompleteService
39+
40+
@Mock
41+
private lateinit var mockBookmarksDao: BookmarksDao
42+
43+
@Mock
44+
private lateinit var mockSettingsDataStore: SettingsDataStore
45+
46+
private lateinit var testee: AutoCompleteApi
47+
48+
@Before
49+
fun before() {
50+
MockitoAnnotations.initMocks(this)
51+
testee = AutoCompleteApi(mockAutoCompleteService, mockBookmarksDao, mockSettingsDataStore)
52+
}
53+
54+
@Test
55+
fun whenBookmarkSuggestionsAreDisabledThenDoNotGetBookmarksFromDAO() {
56+
whenever(mockSettingsDataStore.bookmarksAutoCompleteSuggestionsEnabled).thenReturn(false)
57+
whenever(mockAutoCompleteService.autoComplete("foo")).thenReturn(Observable.just(emptyList()))
58+
59+
testee.autoComplete("foo")
60+
61+
verify(mockBookmarksDao, never()).bookmarksByQuery(anyString())
62+
}
63+
64+
@Test
65+
fun whenBookmarkSuggestionsAreEnabledThenGetBookmarksFromDAO() {
66+
whenever(mockSettingsDataStore.bookmarksAutoCompleteSuggestionsEnabled).thenReturn(true)
67+
whenever(mockAutoCompleteService.autoComplete("foo")).thenReturn(Observable.just(emptyList()))
68+
whenever(mockBookmarksDao.bookmarksByQuery(anyString())).thenReturn(Single.just(emptyList()))
69+
70+
testee.autoComplete("foo")
71+
72+
verify(mockBookmarksDao).bookmarksByQuery("%foo%")
73+
}
74+
75+
@Test
76+
fun whenQueryIsBlankThenReturnAnEmptyList() {
77+
val result = testee.autoComplete("").test()
78+
val value = result.values()[0] as AutoCompleteApi.AutoCompleteResult
79+
80+
assertTrue(value.suggestions.isEmpty())
81+
}
82+
83+
@Test
84+
fun whenReturnBookmarkSuggestionsThenPhraseIsSameAsURL() {
85+
whenever(mockSettingsDataStore.bookmarksAutoCompleteSuggestionsEnabled).thenReturn(true)
86+
whenever(mockAutoCompleteService.autoComplete("foo")).thenReturn(Observable.just(emptyList()))
87+
whenever(mockBookmarksDao.bookmarksByQuery(anyString())).thenReturn(Single.just(listOf(BookmarkEntity(0, "title", "https://example.com"))))
88+
89+
val result = testee.autoComplete("foo").test()
90+
val value = result.values()[0] as AutoCompleteApi.AutoCompleteResult
91+
92+
assertSame("https://example.com", value.suggestions[0].phrase)
93+
}
94+
}

app/src/androidTest/java/com/duckduckgo/app/bookmarks/db/BookmarksDaoTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.room.Room
2121
import androidx.test.platform.app.InstrumentationRegistry
2222
import com.duckduckgo.app.blockingObserve
2323
import com.duckduckgo.app.global.db.AppDatabase
24+
import kotlinx.coroutines.runBlocking
2425
import org.junit.After
2526
import org.junit.Assert.*
2627
import org.junit.Before
@@ -73,4 +74,16 @@ class BookmarksDaoTest {
7374
assertTrue(list!!.isEmpty())
7475
}
7576

77+
@Test
78+
fun whenBookmarksExistThenReturnTrue() = runBlocking {
79+
val bookmark = BookmarkEntity(id = 1, title = "title", url = "www.example.com")
80+
dao.insert(bookmark)
81+
assertTrue(dao.hasBookmarks())
82+
}
83+
84+
@Test
85+
fun whenBookmarkAreEmptyThenReturnFalse() = runBlocking {
86+
assertFalse(dao.hasBookmarks())
87+
}
88+
7689
}

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import com.duckduckgo.app.global.install.AppInstallStore
5252
import com.duckduckgo.app.global.model.SiteFactory
5353
import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao
5454
import com.duckduckgo.app.privacy.model.PrivacyPractices
55+
import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion.*
5556
import com.duckduckgo.app.privacy.store.PrevalenceStore
5657
import com.duckduckgo.app.settings.db.SettingsDataStore
5758
import com.duckduckgo.app.statistics.api.StatisticsUpdater
@@ -202,7 +203,8 @@ class BrowserTabViewModelTest {
202203
faviconDownloader = mockFaviconDownloader,
203204
addToHomeCapabilityDetector = mockAddToHomeCapabilityDetector,
204205
ctaViewModel = ctaViewModel,
205-
searchCountDao = mockSearchCountDao
206+
searchCountDao = mockSearchCountDao,
207+
pixel = mockPixel
206208
)
207209

208210
testee.loadData("abc", null, false)
@@ -1026,6 +1028,50 @@ class BrowserTabViewModelTest {
10261028
verify(mockHandler, atLeastOnce()).proceed(username, password)
10271029
}
10281030

1031+
@Test
1032+
fun whenBookmarkSuggestionSubmittedThenAutoCompleteBookmarkSelectionPixelSent() = runBlocking {
1033+
whenever(bookmarksDao.hasBookmarks()).thenReturn(true)
1034+
testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteApi.AutoCompleteResult("", emptyList(), true))
1035+
testee.fireAutocompletePixel(AutoCompleteBookmarkSuggestion("example", "Example", "https://example.com"))
1036+
1037+
verify(mockPixel).fire(Pixel.PixelName.AUTOCOMPLETE_BOOKMARK_SELECTION, pixelParams(showedBookmarks = true, bookmarkCapable = true))
1038+
}
1039+
1040+
@Test
1041+
fun whenSearchSuggestionSubmittedWithBookmarksThenAutoCompleteSearchSelectionPixelSent() = runBlocking {
1042+
whenever(bookmarksDao.hasBookmarks()).thenReturn(true)
1043+
testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteApi.AutoCompleteResult("", emptyList(), true))
1044+
testee.fireAutocompletePixel(AutoCompleteSearchSuggestion("example", false))
1045+
1046+
verify(mockPixel).fire(Pixel.PixelName.AUTOCOMPLETE_SEARCH_SELECTION, pixelParams(showedBookmarks = true, bookmarkCapable = true))
1047+
}
1048+
1049+
@Test
1050+
fun whenSearchSuggestionSubmittedWithoutBookmarksThenAutoCompleteSearchSelectionPixelSent() = runBlocking {
1051+
whenever(bookmarksDao.hasBookmarks()).thenReturn(false)
1052+
testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteApi.AutoCompleteResult("", emptyList(), false))
1053+
testee.fireAutocompletePixel(AutoCompleteSearchSuggestion("example", false))
1054+
1055+
verify(mockPixel).fire(Pixel.PixelName.AUTOCOMPLETE_SEARCH_SELECTION, pixelParams(showedBookmarks = false, bookmarkCapable = false))
1056+
}
1057+
1058+
@Test
1059+
fun whenUserSelectToEditQueryThenMoveCaretToTheEnd() {
1060+
testee.onUserSelectedToEditQuery("foo")
1061+
assertTrue(omnibarViewState().shouldMoveCaretToEnd)
1062+
}
1063+
1064+
@Test
1065+
fun whenUserSubmitsQueryThenCaretDoesNotMoveToTheEnd() {
1066+
testee.onUserSubmittedQuery("foo")
1067+
assertFalse(omnibarViewState().shouldMoveCaretToEnd)
1068+
}
1069+
1070+
private fun pixelParams(showedBookmarks: Boolean, bookmarkCapable: Boolean) = mapOf(
1071+
Pixel.PixelParameter.SHOWED_BOOKMARKS to showedBookmarks.toString(),
1072+
Pixel.PixelParameter.BOOKMARK_CAPABLE to bookmarkCapable.toString()
1073+
)
1074+
10291075
private fun setBrowserShowing(isBrowsing: Boolean) {
10301076
testee.browserViewState.value = browserViewState().copy(browserShowing = isBrowsing)
10311077
}

app/src/androidTest/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,17 @@ class SpecialUrlDetectorImplTest {
139139
val type = testee.determineType("foo site:duckduckgo.com") as SearchQuery
140140
assertEquals("foo site:duckduckgo.com", type.query)
141141
}
142+
143+
@Test
144+
fun whenUrlIsJavascriptSchemeThenWebSearchTypeDetected() {
145+
val expected = SearchQuery::class
146+
val actual = testee.determineType("javascript:alert(0)")
147+
assertEquals(expected, actual::class)
148+
}
149+
150+
@Test
151+
fun whenUrlIsJavascriptSchemeThenFullQueryRetained() {
152+
val type = testee.determineType("javascript:alert(0)") as SearchQuery
153+
assertEquals("javascript:alert(0)", type.query)
154+
}
142155
}

app/src/androidTest/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,5 +212,29 @@ class SettingsViewModelTest {
212212
assertEquals(expectedStartString, latestViewState().version)
213213
}
214214

215+
@Test
216+
fun whenBookmarksSuggestionsSwitchedOnThenDataStoreIsUpdated() {
217+
testee.onBookmarksAutocompleteSettingChanged(true)
218+
verify(mockAppSettingsDataStore).bookmarksAutoCompleteSuggestionsEnabled = true
219+
}
220+
221+
@Test
222+
fun whenBookmarksSuggestionsSwitchedOffThenDataStoreIsUpdated() {
223+
testee.onBookmarksAutocompleteSettingChanged(false)
224+
verify(mockAppSettingsDataStore).bookmarksAutoCompleteSuggestionsEnabled = false
225+
}
226+
227+
@Test
228+
fun whenBookmarksSuggestionsToggledOffThenBookmarksDisabledPixelIsSent() {
229+
testee.onBookmarksAutocompleteSettingChanged(false)
230+
verify(mockPixel).fire(Pixel.PixelName.BOOKMARKS_IN_AUTOCOMPLETE_DISABLED)
231+
}
232+
233+
@Test
234+
fun whenBookmarksSuggestionsToggledOnThenBookmarksEnabledPixelIsSent() {
235+
testee.onBookmarksAutocompleteSettingChanged(true)
236+
verify(mockPixel).fire(Pixel.PixelName.BOOKMARKS_IN_AUTOCOMPLETE_ENABLED)
237+
}
238+
215239
private fun latestViewState() = testee.viewState.value!!
216240
}

app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApi.kt

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,80 @@
1616

1717
package com.duckduckgo.app.autocomplete.api
1818

19+
import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion
20+
import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion.AutoCompleteSearchSuggestion
21+
import com.duckduckgo.app.bookmarks.db.BookmarksDao
22+
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Companion.BOOKMARK_TYPE
23+
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Companion.SUGGESTION_TYPE
1924
import com.duckduckgo.app.global.UriString
25+
import com.duckduckgo.app.settings.db.SettingsDataStore
2026
import io.reactivex.Observable
27+
import io.reactivex.functions.BiFunction
2128
import javax.inject.Inject
2229

30+
open class AutoCompleteApi @Inject constructor(
31+
private val autoCompleteService: AutoCompleteService,
32+
private val bookmarksDao: BookmarksDao,
33+
private val settingsDataStore: SettingsDataStore
34+
) {
2335

24-
open class AutoCompleteApi @Inject constructor(private val autoCompleteService: AutoCompleteService) {
25-
26-
fun autoComplete(query: String): Observable<AutoCompleteApi.AutoCompleteResult> {
36+
fun autoComplete(query: String): Observable<AutoCompleteResult> {
2737

2838
if (query.isBlank()) {
29-
return Observable.just(AutoCompleteResult(query, emptyList()))
39+
return Observable.just(AutoCompleteResult(query = query, suggestions = emptyList(), hasBookmarks = false))
40+
}
41+
42+
return bookmarksObservable(query).zipWith(
43+
getAutoCompleteSearchResults(query),
44+
BiFunction { bookmarksResults, searchResults ->
45+
AutoCompleteResult(
46+
query = query,
47+
suggestions = (bookmarksResults + searchResults).distinct(),
48+
hasBookmarks = bookmarksResults.isNotEmpty()
49+
)
50+
}
51+
)
52+
}
53+
54+
private fun bookmarksObservable(query: String): Observable<List<AutoCompleteBookmarkSuggestion>> {
55+
return if (settingsDataStore.bookmarksAutoCompleteSuggestionsEnabled) {
56+
getAutoCompleteBookmarkResults(query)
57+
} else {
58+
Observable.just(emptyList())
3059
}
60+
}
3161

32-
return autoCompleteService.autoComplete(query)
62+
private fun getAutoCompleteSearchResults(query: String) =
63+
autoCompleteService.autoComplete(query)
3364
.flatMapIterable { it }
34-
.map { AutoCompleteSuggestion(it.phrase, UriString.isWebUrl(it.phrase)) }
65+
.map {
66+
AutoCompleteSearchSuggestion(phrase = it.phrase, isUrl = UriString.isWebUrl(it.phrase))
67+
}
68+
.toList()
69+
.onErrorReturn { emptyList() }
70+
.toObservable()
71+
72+
private fun getAutoCompleteBookmarkResults(query: String) =
73+
bookmarksDao.bookmarksByQuery("%$query%")
74+
.flattenAsObservable { it }
75+
.map {
76+
AutoCompleteBookmarkSuggestion(phrase = it.url, title = it.title ?: "", url = it.url)
77+
}
3578
.toList()
3679
.onErrorReturn { emptyList() }
37-
.map { AutoCompleteResult(query = query, suggestions = it) }
3880
.toObservable()
39-
}
4081

4182
data class AutoCompleteResult(
4283
val query: String,
43-
val suggestions: List<AutoCompleteSuggestion>
84+
val suggestions: List<AutoCompleteSuggestion>,
85+
val hasBookmarks: Boolean
4486
)
4587

46-
data class AutoCompleteSuggestion(val phrase: String, val isUrl: Boolean)
88+
sealed class AutoCompleteSuggestion(val phrase: String, val suggestionType: Int) {
89+
class AutoCompleteSearchSuggestion(phrase: String, val isUrl: Boolean) :
90+
AutoCompleteSuggestion(phrase, SUGGESTION_TYPE)
4791

92+
class AutoCompleteBookmarkSuggestion(phrase: String, val title: String, val url: String) :
93+
AutoCompleteSuggestion(phrase, BOOKMARK_TYPE)
94+
}
4895
}

app/src/main/java/com/duckduckgo/app/bookmarks/db/BookmarksDao.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.duckduckgo.app.bookmarks.db
1818

1919
import androidx.lifecycle.LiveData
2020
import androidx.room.*
21+
import io.reactivex.Single
2122

2223
@Dao
2324
interface BookmarksDao {
@@ -34,4 +35,9 @@ interface BookmarksDao {
3435
@Update
3536
fun update(bookmarkEntity: BookmarkEntity)
3637

38+
@Query("select * from bookmarks WHERE title LIKE :query OR url LIKE :query ")
39+
fun bookmarksByQuery(query: String): Single<List<BookmarkEntity>>
40+
41+
@Query("select CAST(COUNT(*) AS BIT) from bookmarks")
42+
suspend fun hasBookmarks(): Boolean
3743
}

0 commit comments

Comments
 (0)