diff --git a/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt b/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt index d38b7c99887b..ea4a2e784167 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApiTest.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.autocomplete.api +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.nhaarman.mockitokotlin2.verify @@ -59,7 +60,7 @@ class AutoCompleteApiTest { @Test fun whenQueryIsBlankThenReturnAnEmptyList() { val result = testee.autoComplete("").test() - val value = result.values()[0] as AutoCompleteApi.AutoCompleteResult + val value = result.values()[0] as AutoCompleteResult assertTrue(value.suggestions.isEmpty()) } @@ -70,7 +71,7 @@ class AutoCompleteApiTest { whenever(mockBookmarksDao.bookmarksByQuery(anyString())).thenReturn(Single.just(listOf(BookmarkEntity(0, "title", "https://example.com")))) val result = testee.autoComplete("foo").test() - val value = result.values()[0] as AutoCompleteApi.AutoCompleteResult + val value = result.values()[0] as AutoCompleteResult assertSame("https://example.com", value.suggestions[0].phrase) } 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 d260c40b4ed9..e2f199b4ab9e 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -30,9 +30,10 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.InstantSchedulersRule import com.duckduckgo.app.ValueCaptorObserver +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.AutoCompleteSearchSuggestion import com.duckduckgo.app.autocomplete.api.AutoCompleteApi -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion.AutoCompleteSearchSuggestion import com.duckduckgo.app.autocomplete.api.AutoCompleteService import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao @@ -227,7 +228,7 @@ class BrowserTabViewModelTest { siteFactory = siteFactory, tabRepository = mockTabsRepository, networkLeaderboardDao = mockNetworkLeaderboardDao, - autoCompleteApi = mockAutoCompleteApi, + autoComplete = mockAutoCompleteApi, appSettingsPreferencesStore = mockSettingsStore, bookmarksDao = mockBookmarksDao, longPressHandler = mockLongPressHandler, @@ -1263,16 +1264,17 @@ class BrowserTabViewModelTest { @Test fun whenBookmarkSuggestionSubmittedThenAutoCompleteBookmarkSelectionPixelSent() = runBlocking { whenever(mockBookmarksDao.hasBookmarks()).thenReturn(true) - testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteApi.AutoCompleteResult("", emptyList(), true)) - testee.fireAutocompletePixel(AutoCompleteBookmarkSuggestion("example", "Example", "https://example.com")) - + val suggestion = AutoCompleteBookmarkSuggestion("example", "Example", "https://example.com") + testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", listOf(suggestion))) + testee.fireAutocompletePixel(suggestion) verify(mockPixel).fire(Pixel.PixelName.AUTOCOMPLETE_BOOKMARK_SELECTION, pixelParams(showedBookmarks = true, bookmarkCapable = true)) } @Test fun whenSearchSuggestionSubmittedWithBookmarksThenAutoCompleteSearchSelectionPixelSent() = runBlocking { whenever(mockBookmarksDao.hasBookmarks()).thenReturn(true) - testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteApi.AutoCompleteResult("", emptyList(), true)) + val suggestions = listOf(AutoCompleteSearchSuggestion("", false), AutoCompleteBookmarkSuggestion("", "", "")) + testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", suggestions)) testee.fireAutocompletePixel(AutoCompleteSearchSuggestion("example", false)) verify(mockPixel).fire(Pixel.PixelName.AUTOCOMPLETE_SEARCH_SELECTION, pixelParams(showedBookmarks = true, bookmarkCapable = true)) @@ -1281,7 +1283,7 @@ class BrowserTabViewModelTest { @Test fun whenSearchSuggestionSubmittedWithoutBookmarksThenAutoCompleteSearchSelectionPixelSent() = runBlocking { whenever(mockBookmarksDao.hasBookmarks()).thenReturn(false) - testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteApi.AutoCompleteResult("", emptyList(), false)) + testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", emptyList())) testee.fireAutocompletePixel(AutoCompleteSearchSuggestion("example", false)) verify(mockPixel).fire(Pixel.PixelName.AUTOCOMPLETE_SEARCH_SELECTION, pixelParams(showedBookmarks = false, bookmarkCapable = false)) diff --git a/app/src/androidTest/java/com/duckduckgo/app/di/TestAppComponent.kt b/app/src/androidTest/java/com/duckduckgo/app/di/TestAppComponent.kt index f6dddb05c423..3d603ff6b78f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/di/TestAppComponent.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/di/TestAppComponent.kt @@ -53,6 +53,7 @@ import javax.inject.Singleton NetworkModule::class, StoreModule::class, JsonModule::class, + SystemComponentsModule::class, BrowserModule::class, BrowserAutoCompleteModule::class, HttpsUpgraderModule::class, diff --git a/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/TrackerNetworksViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/TrackerNetworksViewModelTest.kt index 203e52860d0e..acd0cd116cc1 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/TrackerNetworksViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/TrackerNetworksViewModelTest.kt @@ -108,7 +108,7 @@ class TrackerNetworksViewModelTest { assertEquals(1, result[Entity.MINOR_ENTITY_B]?.count()) } - private fun site(url: String = "", networkCount: Int = 0, trackingEvents: List = emptyList()): Site { + private fun site(url: String = "", trackingEvents: List = emptyList()): Site { val site: Site = mock() whenever(site.url).thenReturn(url) whenever(site.uri).thenReturn(Uri.parse(url)) diff --git a/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt index 2310d6ea5f8d..875f960e26c6 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt @@ -53,7 +53,7 @@ class VariantManagerTest { fun conceptTestNoCtaVariantIsInactiveAndHasSuppressCtaFeatures() { val variant = variants.firstOrNull { it.key == "md" } assertEqualsDouble(0.0, variant!!.weight) - assertEquals(2, variant!!.features.size) + assertEquals(2, variant.features.size) assertTrue(variant.hasFeature(SuppressWidgetCta)) assertTrue(variant.hasFeature(SuppressDefaultBrowserCta)) } @@ -62,12 +62,13 @@ class VariantManagerTest { fun conceptTestExperimentalVariantIsInactiveAndHasConceptTestAndSuppressCtaFeatures() { val variant = variants.firstOrNull { it.key == "me" } assertEqualsDouble(0.0, variant!!.weight) - assertEquals(3, variant!!.features.size) + assertEquals(3, variant.features.size) assertTrue(variant.hasFeature(ConceptTest)) assertTrue(variant.hasFeature(SuppressWidgetCta)) assertTrue(variant.hasFeature(SuppressDefaultBrowserCta)) } + // CTA on Concept Test experiments @Test @@ -81,7 +82,7 @@ class VariantManagerTest { fun insertCtaConceptTestVariantIsActiveAndHasConceptTestAndSuppressCtaFeatures() { val variant = variants.firstOrNull { it.key == "mv" } assertEqualsDouble(1.0, variant!!.weight) - assertEquals(3, variant!!.features.size) + assertEquals(3, variant.features.size) assertTrue(variant.hasFeature(ConceptTest)) assertTrue(variant.hasFeature(SuppressWidgetCta)) assertTrue(variant.hasFeature(SuppressDefaultBrowserCta)) @@ -91,7 +92,7 @@ class VariantManagerTest { fun insertCtaConceptTestWithAllCtaExperimentalVariantIsActiveAndHasConceptTestAndSuppressCtaFeatures() { val variant = variants.firstOrNull { it.key == "mz" } assertEqualsDouble(1.0, variant!!.weight) - assertEquals(2, variant!!.features.size) + assertEquals(2, variant.features.size) assertTrue(variant.hasFeature(ConceptTest)) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/DeviceAppLookupTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/DeviceAppLookupTest.kt new file mode 100644 index 000000000000..a61b36edc89d --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/DeviceAppLookupTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2020 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.systemsearch + +import android.content.Intent +import com.duckduckgo.app.systemsearch.DeviceAppLookupTest.AppName.DDG_MOVIES +import com.duckduckgo.app.systemsearch.DeviceAppLookupTest.AppName.DDG_MUSIC +import com.duckduckgo.app.systemsearch.DeviceAppLookupTest.AppName.FILES +import com.duckduckgo.app.systemsearch.DeviceAppLookupTest.AppName.LIVE_DDG +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class DeviceAppLookupTest { + + private val mockAppProvider: DeviceAppListProvider = mock() + + private val testee = InstalledDeviceAppLookup(mockAppProvider) + + @Test + fun whenQueryMatchesWordInShortNameThenMatchesAreReturned() { + whenever(mockAppProvider.get()).thenReturn(apps) + val result = testee.query("DDG") + assertEquals(3, result.size) + assertEquals(DDG_MOVIES, result[0].shortName) + assertEquals(DDG_MUSIC, result[1].shortName) + assertEquals(LIVE_DDG, result[2].shortName) + } + + @Test + fun whenQueryMatchesWordPrefixInShortNameThenMatchesAreReturned() { + whenever(mockAppProvider.get()).thenReturn(apps) + val result = testee.query("DDG") + assertEquals(3, result.size) + assertEquals(DDG_MOVIES, result[0].shortName) + assertEquals(DDG_MUSIC, result[1].shortName) + assertEquals(LIVE_DDG, result[2].shortName) + } + + @Test + fun whenQueryMatchesPastShortNameWordBoundaryToNextPrefixThenMatchesAreReturned() { + whenever(mockAppProvider.get()).thenReturn(apps) + val result = testee.query("DDG M") + assertEquals(2, result.size) + assertEquals(DDG_MOVIES, result[0].shortName) + assertEquals(DDG_MUSIC, result[1].shortName) + } + + @Test + fun whenQueryMatchesWordPrefixInShortNameWithDifferentCaseThenMatchesAreReturned() { + whenever(mockAppProvider.get()).thenReturn(apps) + val result = testee.query("ddg") + assertEquals(3, result.size) + assertEquals(DDG_MOVIES, result[0].shortName) + assertEquals(DDG_MUSIC, result[1].shortName) + assertEquals(LIVE_DDG, result[2].shortName) + } + + @Test + fun whenQueryMatchesMiddleOrSuffixOfAppNameWordThenNoAppsReturned() { + whenever(mockAppProvider.get()).thenReturn(apps) + val result = testee.query("DG") + assertTrue(result.isEmpty()) + } + + @Test + fun whenQueryDoesNotMatchAnyPartOfAppNameThenNoAppsReturned() { + whenever(mockAppProvider.get()).thenReturn(apps) + val result = testee.query("nonmatching") + assertTrue(result.isEmpty()) + } + + @Test + fun whenQueryIsEmptyThenNoAppsReturned() { + whenever(mockAppProvider.get()).thenReturn(apps) + val result = testee.query("") + assertTrue(result.isEmpty()) + } + + @Test + fun whenAppsListIsEmptyThenNoAppsReturned() { + whenever(mockAppProvider.get()).thenReturn(noApps) + val result = testee.query("DDG") + assertTrue(result.isEmpty()) + } + + object AppName { + const val DDG_MOVIES = "DDG Movies" + const val DDG_MUSIC = "DDG Music" + const val LIVE_DDG = "Live DDG" + const val FILES = "Files" + } + + companion object { + val noApps = emptyList() + + val apps = listOf( + DeviceApp(DDG_MOVIES, "", Intent()), + DeviceApp(DDG_MUSIC, "", Intent()), + DeviceApp(FILES, "", Intent()), + DeviceApp(LIVE_DDG, "", Intent()) + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt new file mode 100644 index 000000000000..1aa7a0d4da24 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2020 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.systemsearch + +import android.content.Intent +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Observer +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.InstantSchedulersRule +import com.duckduckgo.app.autocomplete.api.AutoComplete +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion +import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command +import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.LaunchDuckDuckGo +import com.nhaarman.mockitokotlin2.argumentCaptor +import com.nhaarman.mockitokotlin2.atLeastOnce +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import io.reactivex.Observable +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.runBlockingTest +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.verify + +class SystemSearchViewModelTest { + + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val schedulers = InstantSchedulersRule() + + @get:Rule + var coroutineRule = CoroutineTestRule() + + private val mockDeviceAppLookup: DeviceAppLookup = mock() + private val mockAutoComplete: AutoComplete = mock() + + private val commandObserver: Observer = mock() + private val commandCaptor = argumentCaptor() + + private lateinit var testee: SystemSearchViewModel + + @Before + fun setup() { + whenever(mockAutoComplete.autoComplete(QUERY)).thenReturn(Observable.just(autocompleteQueryResult)) + whenever(mockAutoComplete.autoComplete(BLANK_QUERY)).thenReturn(Observable.just(autocompleteBlankResult)) + whenever(mockDeviceAppLookup.query(QUERY)).thenReturn(appQueryResult) + whenever(mockDeviceAppLookup.query(BLANK_QUERY)).thenReturn(appBlankResult) + testee = SystemSearchViewModel(mockAutoComplete, mockDeviceAppLookup, coroutineRule.testDispatcherProvider) + testee.command.observeForever(commandObserver) + } + + @After + fun tearDown() { + testee.command.removeObserver(commandObserver) + } + + @Test + fun whenUserUpdatesQueryThenViewStateUpdated() = ruleRunBlockingTest { + testee.userUpdatedQuery(QUERY) + + val newViewState = testee.viewState.value + assertNotNull(newViewState) + assertEquals(QUERY, newViewState?.queryText) + assertEquals(appQueryResult, newViewState?.appResults) + assertEquals(autocompleteQueryResult, newViewState?.autocompleteResults) + } + + @Test + fun whenUserClearsQueryThenViewStateReset() = ruleRunBlockingTest { + testee.userUpdatedQuery(QUERY) + testee.userClearedQuery() + + val newViewState = testee.viewState.value + assertNotNull(newViewState) + assertTrue(newViewState!!.queryText.isEmpty()) + assertTrue(newViewState.appResults.isEmpty()) + assertEquals(AutoCompleteResult("", emptyList()), newViewState.autocompleteResults) + } + + @Test + fun whenUsersUpdatesWithBlankQueryThenViewStateReset() = ruleRunBlockingTest { + testee.userUpdatedQuery(QUERY) + testee.userUpdatedQuery(BLANK_QUERY) + + val newViewState = testee.viewState.value + assertNotNull(newViewState) + assertTrue(newViewState!!.queryText.isEmpty()) + assertTrue(newViewState.appResults.isEmpty()) + assertEquals(AutoCompleteResult("", emptyList()), newViewState.autocompleteResults) + } + + @Test + fun whenUserSubmitsQueryThenBrowserLaunched() { + testee.userSubmittedQuery(QUERY) + verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertEquals(Command.LaunchBrowser(QUERY), commandCaptor.lastValue) + } + + @Test + fun whenUserSubmitsAutocompleteResultThenBrowserLaunched() { + testee.userSubmittedAutocompleteResult(AUTOCOMPLETE_RESULT) + verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertEquals(Command.LaunchBrowser(AUTOCOMPLETE_RESULT), commandCaptor.lastValue) + } + + @Test + fun whenUserSelectsAppResultThenAppLaunched() { + testee.userSelectedApp(deviceApp) + verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertEquals(Command.LaunchDeviceApplication(deviceApp), commandCaptor.lastValue) + } + + @Test + fun whenUserTapsDaxThenAppLaunched() { + testee.userTappedDax() + verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertTrue(commandCaptor.lastValue is LaunchDuckDuckGo) + } + + @Test + fun whenUserSelectsAppThatCannotBeFoundThenAppsRefreshedAndUserMessageShown() { + testee.appNotFound(deviceApp) + verify(mockDeviceAppLookup).refreshAppList() + verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertEquals(Command.ShowAppNotFoundMessage(deviceApp.shortName), commandCaptor.lastValue) + } + + private fun ruleRunBlockingTest(block: suspend TestCoroutineScope.() -> Unit) = + coroutineRule.testDispatcher.runBlockingTest(block) + + companion object { + const val QUERY = "abc" + const val BLANK_QUERY = "" + const val AUTOCOMPLETE_RESULT = "autocomplete result" + val deviceApp = DeviceApp("", "", Intent()) + val autocompleteQueryResult = AutoCompleteResult(QUERY, listOf(AutoCompleteSearchSuggestion(QUERY, false))) + val autocompleteBlankResult = AutoCompleteResult(BLANK_QUERY, emptyList()) + val appQueryResult = listOf(deviceApp) + val appBlankResult = emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e75ad3ed1256..dc3b5a6e08f6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,28 @@ + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + data class AutoCompleteResult( + val query: String, + val suggestions: List + ) + + sealed class AutoCompleteSuggestion(val phrase: String) { + class AutoCompleteSearchSuggestion(phrase: String, val isUrl: Boolean) : + AutoCompleteSuggestion(phrase) + + class AutoCompleteBookmarkSuggestion(phrase: String, val title: String, val url: String) : + AutoCompleteSuggestion(phrase) + } +} + +class AutoCompleteApi @Inject constructor( private val autoCompleteService: AutoCompleteService, private val bookmarksDao: BookmarksDao -) { +) : AutoComplete { - fun autoComplete(query: String): Observable { + override fun autoComplete(query: String): Observable { if (query.isBlank()) { - return Observable.just(AutoCompleteResult(query = query, suggestions = emptyList(), hasBookmarks = false)) + return Observable.just(AutoCompleteResult(query = query, suggestions = emptyList())) } return getAutoCompleteBookmarkResults(query).zipWith( @@ -42,8 +58,7 @@ open class AutoCompleteApi @Inject constructor( BiFunction { bookmarksResults, searchResults -> AutoCompleteResult( query = query, - suggestions = (bookmarksResults + searchResults).distinct(), - hasBookmarks = bookmarksResults.isNotEmpty() + suggestions = (bookmarksResults + searchResults).distinct() ) } ) @@ -68,18 +83,4 @@ open class AutoCompleteApi @Inject constructor( .toList() .onErrorReturn { emptyList() } .toObservable() - - data class AutoCompleteResult( - val query: String, - val suggestions: List, - val hasBookmarks: Boolean - ) - - sealed class AutoCompleteSuggestion(val phrase: String, val suggestionType: Int) { - class AutoCompleteSearchSuggestion(phrase: String, val isUrl: Boolean) : - AutoCompleteSuggestion(phrase, SUGGESTION_TYPE) - - class AutoCompleteBookmarkSuggestion(phrase: String, val title: String, val url: String) : - AutoCompleteSuggestion(phrase, BOOKMARK_TYPE) - } } \ No newline at end of file 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 c8b878ffff59..2c69e6d7298f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -196,13 +196,6 @@ class BrowserActivity : DuckDuckGoActivity(), CoroutineScope by MainScope() { Toast.makeText(applicationContext, R.string.fireDataCleared, Toast.LENGTH_LONG).show() } - if (launchedFromWidget(intent)) { - Timber.w("new tab requested from widget") - pixel.fire(Pixel.PixelName.WIDGET_LAUNCHED) - launch { viewModel.onNewTabRequested() } - return - } - if (launchNewSearch(intent)) { Timber.w("new tab requested") launch { viewModel.onNewTabRequested() } @@ -264,12 +257,8 @@ class BrowserActivity : DuckDuckGoActivity(), CoroutineScope by MainScope() { } } - private fun launchedFromWidget(intent: Intent): Boolean { - return intent.getBooleanExtra(WIDGET_SEARCH_EXTRA, false) - } - private fun launchNewSearch(intent: Intent): Boolean { - return intent.getBooleanExtra(NEW_SEARCH_EXTRA, false) || intent.action == Intent.ACTION_ASSIST || intent.action == NEW_SEARCH_ACTION + return intent.getBooleanExtra(NEW_SEARCH_EXTRA, false) } fun launchPrivacyDashboard() { @@ -338,20 +327,16 @@ class BrowserActivity : DuckDuckGoActivity(), CoroutineScope by MainScope() { context: Context, queryExtra: String? = null, newSearch: Boolean = false, - widgetSearch: Boolean = false, launchedFromFireAction: Boolean = false ): Intent { val intent = Intent(context, BrowserActivity::class.java) intent.putExtra(EXTRA_TEXT, queryExtra) intent.putExtra(NEW_SEARCH_EXTRA, newSearch) - intent.putExtra(WIDGET_SEARCH_EXTRA, widgetSearch) intent.putExtra(LAUNCHED_FROM_FIRE_EXTRA, launchedFromFireAction) return intent } - const val NEW_SEARCH_ACTION = "com.duckduckgo.mobile.android.NEW_SEARCH" const val NEW_SEARCH_EXTRA = "NEW_SEARCH_EXTRA" - const val WIDGET_SEARCH_EXTRA = "WIDGET_SEARCH_EXTRA" const val PERFORM_FIRE_ON_ENTRY_EXTRA = "PERFORM_FIRE_ON_ENTRY_EXTRA" const val LAUNCHED_FROM_FIRE_EXTRA = "LAUNCHED_FROM_FIRE_EXTRA" const val LAUNCH_FROM_DEFAULT_BROWSER_DIALOG = "LAUNCH_FROM_DEFAULT_BROWSER_DIALOG" 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 fd06e3a2e5f2..2674a328c910 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -52,7 +52,7 @@ import androidx.core.view.postDelayed import androidx.fragment.app.Fragment import androidx.lifecycle.* import androidx.recyclerview.widget.LinearLayoutManager -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment import com.duckduckgo.app.browser.BrowserTabViewModel.* import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter @@ -71,11 +71,7 @@ import com.duckduckgo.app.browser.tabpreview.WebViewPreviewGenerator import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment import com.duckduckgo.app.browser.useragent.UserAgentProvider -import com.duckduckgo.app.cta.ui.Cta -import com.duckduckgo.app.cta.ui.HomePanelCta -import com.duckduckgo.app.cta.ui.CtaViewModel -import com.duckduckgo.app.cta.ui.DaxBubbleCta -import com.duckduckgo.app.cta.ui.DaxDialogCta +import com.duckduckgo.app.cta.ui.* import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.global.device.DeviceInfo import com.duckduckgo.app.global.view.* @@ -92,9 +88,9 @@ import com.duckduckgo.app.widget.ui.AddWidgetInstructionsActivity import com.duckduckgo.widget.SearchWidgetLight import com.google.android.material.snackbar.Snackbar import dagger.android.support.AndroidSupportInjection -import kotlinx.android.synthetic.main.include_dax_dialog_cta.* import kotlinx.android.synthetic.main.fragment_browser_tab.* import kotlinx.android.synthetic.main.include_cta_buttons.view.* +import kotlinx.android.synthetic.main.include_dax_dialog_cta.* import kotlinx.android.synthetic.main.include_find_in_page.* import kotlinx.android.synthetic.main.include_new_browser_tab.* import kotlinx.android.synthetic.main.include_omnibar_toolbar.* @@ -1157,8 +1153,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { if (viewState.showSuggestions) { autoCompleteSuggestionsList.show() - val results = viewState.searchResults.suggestions - autoCompleteSuggestionsAdapter.updateData(results) + autoCompleteSuggestionsAdapter.updateData(viewState.searchResults.query, viewState.searchResults.suggestions) } else { autoCompleteSuggestionsList.gone() } 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 25fcacf8945e..003c0e672cdd 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -29,12 +29,15 @@ import android.webkit.WebView import androidx.annotation.AnyThread import androidx.annotation.VisibleForTesting import androidx.core.net.toUri -import androidx.lifecycle.* -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteResult -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion.AutoCompleteSearchSuggestion +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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.AutoCompleteSearchSuggestion import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment.EditBookmarkListener @@ -53,8 +56,8 @@ import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.session.WebViewSessionStorage import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment.HttpAuthenticationListener import com.duckduckgo.app.cta.ui.Cta -import com.duckduckgo.app.cta.ui.HomePanelCta import com.duckduckgo.app.cta.ui.CtaViewModel +import com.duckduckgo.app.cta.ui.HomePanelCta import com.duckduckgo.app.global.* import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory @@ -74,6 +77,7 @@ import com.duckduckgo.app.trackerdetection.model.TrackingEvent import com.duckduckgo.app.usage.search.SearchCountDao import com.jakewharton.rxrelay2.PublishRelay import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -89,7 +93,7 @@ class BrowserTabViewModel( private val tabRepository: TabRepository, private val networkLeaderboardDao: NetworkLeaderboardDao, private val bookmarksDao: BookmarksDao, - private val autoCompleteApi: AutoCompleteApi, + private val autoComplete: AutoComplete, private val appSettingsPreferencesStore: SettingsDataStore, private val longPressHandler: LongPressHandler, private val webViewSessionStorage: WebViewSessionStorage, @@ -155,7 +159,7 @@ class BrowserTabViewModel( data class AutoCompleteViewState( val showSuggestions: Boolean = false, - val searchResults: AutoCompleteResult = AutoCompleteResult("", emptyList(), false) + val searchResults: AutoCompleteResult = AutoCompleteResult("", emptyList()) ) sealed class Command { @@ -216,6 +220,7 @@ class BrowserTabViewModel( get() = site?.title private val autoCompletePublishSubject = PublishRelay.create() + private var autoCompleteDisposable: Disposable? = null private var site: Site? = null private lateinit var tabId: String private var webNavigationState: WebNavigationState? = null @@ -265,9 +270,9 @@ class BrowserTabViewModel( @SuppressLint("CheckResult") private fun configureAutoComplete() { - autoCompletePublishSubject + autoCompleteDisposable = autoCompletePublishSubject .debounce(300, TimeUnit.MILLISECONDS) - .switchMap { autoCompleteApi.autoComplete(it) } + .switchMap { autoComplete.autoComplete(it) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ result -> @@ -278,13 +283,15 @@ class BrowserTabViewModel( private fun onAutoCompleteResultReceived(result: AutoCompleteResult) { val results = result.suggestions.take(6) val currentViewState = currentAutoCompleteViewState() - autoCompleteViewState.value = currentViewState.copy(searchResults = AutoCompleteResult(result.query, results, result.hasBookmarks)) + autoCompleteViewState.value = currentViewState.copy(searchResults = AutoCompleteResult(result.query, results)) } @VisibleForTesting public override fun onCleared() { - super.onCleared() buildingSiteFactoryJob?.cancel() + autoCompleteDisposable?.dispose() + autoCompleteDisposable = null + super.onCleared() } fun registerWebViewListener(browserWebViewClient: BrowserWebViewClient, browserChromeClient: BrowserChromeClient) { @@ -315,8 +322,9 @@ class BrowserTabViewModel( val hasBookmarks = withContext(dispatchers.io()) { bookmarksDao.hasBookmarks() } + val hasBookmarkResults = currentViewState.searchResults.suggestions.any { it is AutoCompleteBookmarkSuggestion } val params = mapOf( - PixelParameter.SHOWED_BOOKMARKS to currentViewState.searchResults.hasBookmarks.toString(), + PixelParameter.SHOWED_BOOKMARKS to hasBookmarkResults.toString(), PixelParameter.BOOKMARK_CAPABLE to hasBookmarks.toString() ) val pixelName = when (suggestion) { @@ -624,7 +632,7 @@ class BrowserTabViewModel( // determine if empty list to be shown, or existing search results val autoCompleteSearchResults = if (query.isBlank()) { - AutoCompleteResult(query, emptyList(), false) + AutoCompleteResult(query, emptyList()) } else { currentAutoCompleteViewState().searchResults } 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 a8e557d78767..57f9246c14f6 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 @@ -19,8 +19,12 @@ package com.duckduckgo.app.browser.autocomplete import android.view.ViewGroup import androidx.annotation.UiThread import androidx.recyclerview.widget.RecyclerView -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion 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.EMPTY_TYPE +import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter.Type.SUGGESTION_TYPE class BrowserAutoCompleteSuggestionsAdapter( private val immediateSearchClickListener: (AutoCompleteSuggestion) -> Unit, @@ -32,16 +36,18 @@ class BrowserAutoCompleteSuggestionsAdapter( SUGGESTION_TYPE to SearchSuggestionViewHolderFactory(), BOOKMARK_TYPE to BookmarkSuggestionViewHolderFactory() ) - private val suggestions: MutableList = ArrayList() + + private var phrase = "" + private var suggestions: List = emptyList() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AutoCompleteViewHolder = viewHolderFactoryMap.getValue(viewType).onCreateViewHolder(parent) override fun getItemViewType(position: Int): Int { - return if (suggestions.isEmpty()) { - EMPTY_TYPE - } else { - suggestions[position].suggestionType + return when { + suggestions.isEmpty() -> EMPTY_TYPE + suggestions[position] is AutoCompleteBookmarkSuggestion -> BOOKMARK_TYPE + else -> SUGGESTION_TYPE } } @@ -59,22 +65,21 @@ class BrowserAutoCompleteSuggestionsAdapter( } override fun getItemCount(): Int { - if (suggestions.isNotEmpty()) { - return suggestions.size + if (suggestions.isEmpty() && phrase.isNotBlank()) { + return 1 // No suggestions message } - - // if there are no suggestions, we'll use a recycler row to display "no suggestions" - return 1 + return suggestions.size } @UiThread - fun updateData(newSuggestions: List) { - suggestions.clear() - suggestions.addAll(newSuggestions) + fun updateData(newPhrase: String, newSuggestions: List) { + if (phrase == newPhrase && suggestions == newSuggestions) return + phrase = newPhrase + suggestions = newSuggestions notifyDataSetChanged() } - companion object { + object Type { const val EMPTY_TYPE = 1 const val SUGGESTION_TYPE = 2 const val BOOKMARK_TYPE = 3 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 479d4e92c993..0193fef22b89 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 @@ -20,13 +20,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion.AutoCompleteSearchSuggestion +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.AutoCompleteSearchSuggestion import com.duckduckgo.app.browser.R -import com.duckduckgo.app.browser.autocomplete.AutoCompleteViewHolder.BookmarkSuggestionViewHolder -import com.duckduckgo.app.browser.autocomplete.AutoCompleteViewHolder.EmptySuggestionViewHolder -import com.duckduckgo.app.browser.autocomplete.AutoCompleteViewHolder.SearchSuggestionViewHolder +import com.duckduckgo.app.browser.autocomplete.AutoCompleteViewHolder.* import kotlinx.android.synthetic.main.item_autocomplete_bookmark_suggestion.view.* import kotlinx.android.synthetic.main.item_autocomplete_search_suggestion.view.* diff --git a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt index 892625c2662b..42baf5b56286 100644 --- a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt @@ -45,6 +45,7 @@ import com.duckduckgo.app.privacy.ui.ScorecardActivity import com.duckduckgo.app.privacy.ui.TrackerNetworksActivity import com.duckduckgo.app.settings.SettingsActivity import com.duckduckgo.app.survey.ui.SurveyActivity +import com.duckduckgo.app.systemsearch.SystemSearchActivity import com.duckduckgo.app.tabs.ui.TabSwitcherActivity import com.duckduckgo.app.widget.ui.AddWidgetInstructionsActivity import dagger.Module @@ -68,6 +69,10 @@ abstract class AndroidBindingModule { @ContributesAndroidInjector abstract fun onboardingActivityExperiment(): OnboardingActivityExperiment + @ActivityScoped + @ContributesAndroidInjector + abstract fun systemSearchActivity(): SystemSearchActivity + @ActivityScoped @ContributesAndroidInjector abstract fun browserActivity(): BrowserActivity diff --git a/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt b/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt index cb4ed9885946..b1cad5b41cdd 100644 --- a/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt +++ b/app/src/main/java/com/duckduckgo/app/di/AppComponent.kt @@ -51,6 +51,7 @@ import javax.inject.Singleton DatabaseModule::class, DaoModule::class, JsonModule::class, + SystemComponentsModule::class, BrowserModule::class, BrowserAutoCompleteModule::class, HttpsUpgraderModule::class, diff --git a/app/src/main/java/com/duckduckgo/app/di/AppConfigurationDownloadModule.kt b/app/src/main/java/com/duckduckgo/app/di/AppConfigurationDownloadModule.kt index 21bfd61ddaa3..6f945479773c 100644 --- a/app/src/main/java/com/duckduckgo/app/di/AppConfigurationDownloadModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/AppConfigurationDownloadModule.kt @@ -16,7 +16,6 @@ package com.duckduckgo.app.di -import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.httpsupgrade.api.HttpsUpgradeDataDownloader import com.duckduckgo.app.job.AppConfigurationDownloader import com.duckduckgo.app.job.ConfigurationDownloader diff --git a/app/src/main/java/com/duckduckgo/app/di/PlayStoreReferralModule.kt b/app/src/main/java/com/duckduckgo/app/di/PlayStoreReferralModule.kt index 9cea5aaf7a72..94d324a3bf1c 100644 --- a/app/src/main/java/com/duckduckgo/app/di/PlayStoreReferralModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/PlayStoreReferralModule.kt @@ -27,10 +27,6 @@ import javax.inject.Singleton @Module class PlayStoreReferralModule { - @Singleton - @Provides - fun packageManager(context: Context) = context.packageManager - @Provides fun appInstallationReferrerParser(): AppInstallationReferrerParser { return QueryParamReferrerParser() diff --git a/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt b/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt new file mode 100644 index 000000000000..57eb757ffd17 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 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.di + +import android.content.Context +import android.content.pm.PackageManager +import com.duckduckgo.app.systemsearch.DeviceAppListProvider +import com.duckduckgo.app.systemsearch.DeviceAppLookup +import com.duckduckgo.app.systemsearch.InstalledDeviceAppListProvider +import com.duckduckgo.app.systemsearch.InstalledDeviceAppLookup +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +open class SystemComponentsModule { + + @Singleton + @Provides + fun packageManager(context: Context): PackageManager = context.packageManager + + @Singleton + @Provides + fun deviceAppsListProvider(packageManager: PackageManager): DeviceAppListProvider = InstalledDeviceAppListProvider(packageManager) + + @Provides + @Singleton + fun deviceAppLookup(deviceAppListProvider: DeviceAppListProvider): DeviceAppLookup = InstalledDeviceAppLookup(deviceAppListProvider) +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index 0b43cbc7c090..27816f997c1f 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -63,6 +63,8 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.app.survey.db.SurveyDao import com.duckduckgo.app.survey.ui.SurveyViewModel +import com.duckduckgo.app.systemsearch.DeviceAppLookup +import com.duckduckgo.app.systemsearch.SystemSearchViewModel import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel import com.duckduckgo.app.usage.search.SearchCountDao @@ -85,6 +87,7 @@ class ViewModelFactory @Inject constructor( private val bookmarksDao: BookmarksDao, private val surveyDao: SurveyDao, private val autoCompleteApi: AutoCompleteApi, + private val deviceAppLookup: DeviceAppLookup, private val appSettingsPreferencesStore: SettingsDataStore, private val webViewLongPressHandler: LongPressHandler, private val defaultBrowserDetector: DefaultBrowserDetector, @@ -110,6 +113,7 @@ class ViewModelFactory @Inject constructor( with(modelClass) { when { isAssignableFrom(LaunchViewModel::class.java) -> LaunchViewModel(onboardingStore, appInstallationReferrerStateListener) + isAssignableFrom(SystemSearchViewModel::class.java) -> SystemSearchViewModel(autoCompleteApi, deviceAppLookup) isAssignableFrom(OnboardingViewModel::class.java) -> onboardingViewModel() isAssignableFrom(BrowserViewModel::class.java) -> browserViewModel() isAssignableFrom(BrowserTabViewModel::class.java) -> browserTabViewModel() @@ -174,7 +178,7 @@ class ViewModelFactory @Inject constructor( tabRepository = tabRepository, networkLeaderboardDao = networkLeaderboardDao, bookmarksDao = bookmarksDao, - autoCompleteApi = autoCompleteApi, + autoComplete = autoCompleteApi, appSettingsPreferencesStore = appSettingsPreferencesStore, longPressHandler = webViewLongPressHandler, webViewSessionStorage = webViewSessionStorage, diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index e9520dc14e71..89f2a923f999 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -92,7 +92,13 @@ interface Pixel { WIDGET_LEGACY_CTA_DISMISSED("m_wlc_d"), WIDGETS_ADDED(pixelName = "m_w_a"), WIDGETS_DELETED(pixelName = "m_w_d"), - WIDGET_LAUNCHED(pixelName = "m_w_l"), + + APP_WIDGET_LAUNCH(pixelName = "m_w_l"), + APP_ASSIST_LAUNCH(pixelName = "m_a_l"), + APP_SYSTEM_SEARCH_BOX_LAUNCH(pixelName = "m_ssb_l"), + INTERSTITIAL_LAUNCH_BROWSER_QUERY(pixelName = "m_i_lbq"), + INTERSTITIAL_LAUNCH_DEVICE_APP(pixelName = "m_i_sda"), + INTERSTITIAL_LAUNCH_DAX(pixelName = "m_i_ld"), LONG_PRESS("mlp"), LONG_PRESS_DOWNLOAD_IMAGE("mlp_i"), diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt new file mode 100644 index 000000000000..832eb0422d0a --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 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.systemsearch + +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.PackageManager.GET_META_DATA +import android.graphics.drawable.Drawable +import androidx.annotation.WorkerThread +import kotlin.text.RegexOption.IGNORE_CASE + +data class DeviceApp( + val shortName: String, + val packageName: String, + val launchIntent: Intent, + private var icon: Drawable? = null +) { + fun retrieveIcon(packageManager: PackageManager): Drawable { + return icon ?: packageManager.getApplicationIcon(packageName).also { + icon = it + } + } +} + +interface DeviceAppLookup { + @WorkerThread + fun query(query: String): List + + @WorkerThread + fun refreshAppList() +} + +class InstalledDeviceAppLookup(private val appListProvider: DeviceAppListProvider) : DeviceAppLookup { + + private var apps: List? = null + + @WorkerThread + override fun query(query: String): List { + if (query.isBlank()) return emptyList() + + if (apps == null) { + refreshAppList() + } + + val wordPrefixMatchingRegex = ".*\\b${query}.*".toRegex(IGNORE_CASE) + return apps!!.filter { + it.shortName.matches(wordPrefixMatchingRegex) + } + } + + @WorkerThread + override fun refreshAppList() { + apps = appListProvider.get() + } +} + +interface DeviceAppListProvider { + @WorkerThread + fun get(): List +} + +class InstalledDeviceAppListProvider(private val packageManager: PackageManager) : DeviceAppListProvider { + + @WorkerThread + override fun get(): List { + + val appsInfo = packageManager.getInstalledApplications(GET_META_DATA) + + return appsInfo.map { + val packageName = it.packageName + val launchIntent = packageManager.getLaunchIntentForPackage(packageName) ?: return@map null + val shortName = it.loadLabel(packageManager).toString() + return@map DeviceApp(shortName, packageName, launchIntent) + }.filterNotNull() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt new file mode 100644 index 000000000000..45b1cabe484d --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 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.systemsearch + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.UiThread +import androidx.recyclerview.widget.RecyclerView +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.systemsearch.DeviceAppSuggestionsAdapter.DeviceAppViewHolder +import kotlinx.android.synthetic.main.item_device_app_suggestion.view.* + +class DeviceAppSuggestionsAdapter( + private val clickListener: (DeviceApp) -> Unit +) : RecyclerView.Adapter() { + private var deviceApps: List = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceAppViewHolder { + val root = LayoutInflater.from(parent.context).inflate(R.layout.item_device_app_suggestion, parent, false) + return DeviceAppViewHolder(root, root.icon, root.title) + } + + override fun onBindViewHolder(holder: DeviceAppViewHolder, position: Int) { + val app = deviceApps[position] + holder.title.text = app.shortName + holder.root.setOnClickListener { + clickListener(app) + } + val drawable = app.retrieveIcon(holder.icon.context.packageManager) + holder.icon.setImageDrawable(drawable) + } + + override fun getItemCount(): Int { + return deviceApps.size + } + + @UiThread + fun updateData(newDeviceApps: List) { + if (deviceApps == newDeviceApps) return + deviceApps = newDeviceApps + notifyDataSetChanged() + } + + class DeviceAppViewHolder( + val root: View, + var icon: ImageView, + val title: TextView + ) : RecyclerView.ViewHolder(root) + +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt new file mode 100644 index 000000000000..2c56d8c7025c --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2020 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.systemsearch + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.view.KeyEvent +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import android.widget.Toast +import android.widget.Toast.LENGTH_SHORT +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import com.duckduckgo.app.browser.BrowserActivity +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter +import com.duckduckgo.app.browser.omnibar.OmnibarScrolling +import com.duckduckgo.app.global.DuckDuckGoActivity +import com.duckduckgo.app.global.view.TextChangedWatcher +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelName +import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.* +import com.duckduckgo.app.systemsearch.SystemSearchViewModel.SystemSearchViewState +import kotlinx.android.synthetic.main.activity_system_search.* +import javax.inject.Inject + +class SystemSearchActivity : DuckDuckGoActivity() { + + @Inject + lateinit var pixel: Pixel + + @Inject + lateinit var omnibarScrolling: OmnibarScrolling + + private val viewModel: SystemSearchViewModel by bindViewModel() + private lateinit var autocompleteSuggestionsAdapter: BrowserAutoCompleteSuggestionsAdapter + private lateinit var deviceAppSuggestionsAdapter: DeviceAppSuggestionsAdapter + + private val textChangeWatcher = object : TextChangedWatcher() { + override fun afterTextChanged(editable: Editable) { + showOmnibar() + viewModel.userUpdatedQuery(omnibarTextInput.text.toString()) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_system_search) + configureObservers() + configureAutoComplete() + configureDeviceAppSuggestions() + configureDaxButton() + configureOmnibar() + configureTextInput() + intent?.let { sendLaunchPixels(it) } + } + + override fun onNewIntent(newIntent: Intent?) { + super.onNewIntent(newIntent) + viewModel.resetState() + newIntent?.let { sendLaunchPixels(it) } + } + + private fun sendLaunchPixels(intent: Intent) { + when { + launchedFromAssist(intent) -> pixel.fire(PixelName.APP_ASSIST_LAUNCH) + launchedFromWidget(intent) -> pixel.fire(PixelName.APP_WIDGET_LAUNCH) + launchedFromSystemSearchBox(intent) -> pixel.fire(PixelName.APP_SYSTEM_SEARCH_BOX_LAUNCH) + } + } + + private fun configureObservers() { + viewModel.viewState.observe(this, Observer { + it?.let { renderViewState(it) } + }) + viewModel.command.observe(this, Observer { + processCommand(it) + }) + } + + private fun configureAutoComplete() { + autocompleteSuggestions.layoutManager = LinearLayoutManager(this) + autocompleteSuggestionsAdapter = BrowserAutoCompleteSuggestionsAdapter( + immediateSearchClickListener = { + viewModel.userSubmittedAutocompleteResult(it.phrase) + }, + editableSearchClickListener = { + viewModel.userUpdatedQuery(it.phrase) + } + ) + autocompleteSuggestions.adapter = autocompleteSuggestionsAdapter + } + + private fun configureDeviceAppSuggestions() { + deviceAppSuggestions.layoutManager = LinearLayoutManager(this) + deviceAppSuggestionsAdapter = DeviceAppSuggestionsAdapter { + viewModel.userSelectedApp(it) + } + deviceAppSuggestions.adapter = deviceAppSuggestionsAdapter + } + + private fun configureDaxButton() { + logo.setOnClickListener { + viewModel.userTappedDax() + } + } + + private fun configureOmnibar() { + results.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + val scrollable = results.maxScrollAmount > MINIMUM_SCROLL_HEIGHT + if (scrollable) { + omnibarScrolling.enableOmnibarScrolling(toolbar) + } else { + showOmnibar() + omnibarScrolling.disableOmnibarScrolling(toolbar) + } + } + } + + private fun configureTextInput() { + omnibarTextInput.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, keyEvent -> + if (actionId == EditorInfo.IME_ACTION_GO || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER) { + viewModel.userSubmittedQuery(omnibarTextInput.text.toString()) + return@OnEditorActionListener true + } + false + }) + + omnibarTextInput.removeTextChangedListener(textChangeWatcher) + omnibarTextInput.addTextChangedListener(textChangeWatcher) + clearTextButton.setOnClickListener { viewModel.userClearedQuery() } + } + + private fun renderViewState(viewState: SystemSearchViewState) { + if (omnibarTextInput.text.toString() != viewState.queryText) { + omnibarTextInput.setText(viewState.queryText) + omnibarTextInput.setSelection(viewState.queryText.length) + } + + deviceLabel.isVisible = viewState.appResults.isNotEmpty() + autocompleteSuggestionsAdapter.updateData(viewState.autocompleteResults.query, viewState.autocompleteResults.suggestions) + deviceAppSuggestionsAdapter.updateData(viewState.appResults) + } + + private fun processCommand(command: SystemSearchViewModel.Command) { + when (command) { + is LaunchDuckDuckGo -> { + launchDuckDuckGo() + } + is LaunchBrowser -> { + launchBrowser(command) + } + is LaunchDeviceApplication -> { + launchDeviceApp(command) + } + is ShowAppNotFoundMessage -> { + Toast.makeText(this, R.string.systemSearchAppNotFound, LENGTH_SHORT).show() + } + } + } + + private fun launchDuckDuckGo() { + pixel.fire(PixelName.INTERSTITIAL_LAUNCH_DAX) + startActivity(BrowserActivity.intent(this)) + finish() + } + + private fun launchBrowser(command: LaunchBrowser) { + pixel.fire(PixelName.INTERSTITIAL_LAUNCH_BROWSER_QUERY) + startActivity(BrowserActivity.intent(this, command.query)) + finish() + } + + private fun launchDeviceApp(command: LaunchDeviceApplication) { + pixel.fire(PixelName.INTERSTITIAL_LAUNCH_DEVICE_APP) + try { + startActivity(command.deviceApp.launchIntent) + finish() + } catch (error: ActivityNotFoundException) { + viewModel.appNotFound(command.deviceApp) + } + } + + private fun showOmnibar() { + results.scrollTo(0, 0) + appBarLayout.setExpanded(true) + } + + private fun launchedFromSystemSearchBox(intent: Intent): Boolean { + return intent.action == NEW_SEARCH_ACTION + } + + private fun launchedFromAssist(intent: Intent): Boolean { + return intent.action == Intent.ACTION_ASSIST + } + + private fun launchedFromWidget(intent: Intent): Boolean { + return intent.getBooleanExtra(WIDGET_SEARCH_EXTRA, false) + } + + companion object { + + const val WIDGET_SEARCH_EXTRA = "WIDGET_SEARCH_EXTRA" + const val NEW_SEARCH_ACTION = "com.duckduckgo.mobile.android.NEW_SEARCH" + const val MINIMUM_SCROLL_HEIGHT = 86 // enough space for blank "no suggestion" and padding + + fun intent(context: Context, widgetSearch: Boolean = false): Intent { + val intent = Intent(context, SystemSearchActivity::class.java) + intent.putExtra(WIDGET_SEARCH_EXTRA, widgetSearch) + return intent + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt new file mode 100644 index 000000000000..3cc6b48b392b --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2020 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.systemsearch + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.duckduckgo.app.autocomplete.api.AutoComplete +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult +import com.duckduckgo.app.global.DefaultDispatcherProvider +import com.duckduckgo.app.global.DispatcherProvider +import com.duckduckgo.app.global.SingleLiveEvent +import com.jakewharton.rxrelay2.PublishRelay +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.concurrent.TimeUnit + +class SystemSearchViewModel( + private val autoComplete: AutoComplete, + private val deviceAppLookup: DeviceAppLookup, + private val dispatchers: DispatcherProvider = DefaultDispatcherProvider() +) : ViewModel() { + + data class SystemSearchViewState( + val queryText: String = "", + val autocompleteResults: AutoCompleteResult = AutoCompleteResult("", emptyList()), + val appResults: List = emptyList() + ) + + sealed class Command { + object LaunchDuckDuckGo : Command() + data class LaunchBrowser(val query: String) : Command() + data class LaunchDeviceApplication(val deviceApp: DeviceApp) : Command() + data class ShowAppNotFoundMessage(val appName: String) : Command() + } + + val viewState: MutableLiveData = MutableLiveData() + val command: SingleLiveEvent = SingleLiveEvent() + + private val autoCompletePublishSubject = PublishRelay.create() + private var autocompleteResults: AutoCompleteResult = AutoCompleteResult("", emptyList()) + private var autoCompleteDisposable: Disposable? = null + + private var appsJob: Job? = null + private var appResults: List = emptyList() + + init { + resetState() + configureAutoComplete() + } + + private fun currentViewState(): SystemSearchViewState = viewState.value!! + + fun resetState() { + autocompleteResults = AutoCompleteResult("", emptyList()) + appsJob?.cancel() + appResults = emptyList() + viewState.value = SystemSearchViewState() + } + + private fun configureAutoComplete() { + autoCompleteDisposable = autoCompletePublishSubject + .debounce(DEBOUNCE_TIME_MS, TimeUnit.MILLISECONDS) + .switchMap { autoComplete.autoComplete(it) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ result -> + updateAutocompleteResult(result) + }, { t: Throwable? -> Timber.w(t, "Failed to get search results") }) + } + + fun userUpdatedQuery(query: String) { + + appsJob?.cancel() + + val trimmedQuery = query.trim() + + if (trimmedQuery == currentViewState().queryText) { + return + } + + if (trimmedQuery.isBlank()) { + userClearedQuery() + return + } + + viewState.value = currentViewState().copy(queryText = trimmedQuery) + autoCompletePublishSubject.accept(trimmedQuery) + + appsJob = viewModelScope.launch(dispatchers.io()) { + updateAppResults(deviceAppLookup.query(query)) + } + } + + private fun updateAppResults(results: List) { + appResults = results + refreshViewStateResults() + } + + private fun updateAutocompleteResult(results: AutoCompleteResult) { + autocompleteResults = results + refreshViewStateResults() + } + + private fun refreshViewStateResults() { + val hasMultiResults = autocompleteResults.suggestions.isNotEmpty() && appResults.isNotEmpty() + val fullSuggestions = autocompleteResults.suggestions + val updatedSuggestions = if (hasMultiResults) fullSuggestions.take(RESULTS_MAX_RESULTS_PER_GROUP) else fullSuggestions + val updatedApps = if (hasMultiResults) appResults.take(RESULTS_MAX_RESULTS_PER_GROUP) else appResults + + viewState.postValue( + currentViewState().copy( + autocompleteResults = AutoCompleteResult(autocompleteResults.query, updatedSuggestions), + appResults = updatedApps + ) + ) + } + + fun userTappedDax() { + command.value = Command.LaunchDuckDuckGo + } + + fun userClearedQuery() { + autoCompletePublishSubject.accept("") + resetState() + } + + fun userSubmittedQuery(query: String) { + command.value = Command.LaunchBrowser(query) + } + + fun userSubmittedAutocompleteResult(query: String) { + command.value = Command.LaunchBrowser(query) + } + + fun userSelectedApp(app: DeviceApp) { + command.value = Command.LaunchDeviceApplication(app) + } + + fun appNotFound(app: DeviceApp) { + command.value = Command.ShowAppNotFoundMessage(app.shortName) + deviceAppLookup.refreshAppList() + } + + override fun onCleared() { + autoCompleteDisposable?.dispose() + autoCompleteDisposable = null + super.onCleared() + } + + companion object { + private const val DEBOUNCE_TIME_MS = 200L + private const val RESULTS_MAX_RESULTS_PER_GROUP = 4 + } +} diff --git a/app/src/main/java/com/duckduckgo/widget/SearchWidget.kt b/app/src/main/java/com/duckduckgo/widget/SearchWidget.kt index 7add3b665d6a..ca1c04baa2d8 100644 --- a/app/src/main/java/com/duckduckgo/widget/SearchWidget.kt +++ b/app/src/main/java/com/duckduckgo/widget/SearchWidget.kt @@ -22,13 +22,13 @@ import android.appwidget.AppWidgetProvider import android.content.Context import android.content.Intent import android.widget.RemoteViews -import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.DuckDuckGoApplication import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.WIDGETS_ADDED import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.WIDGETS_DELETED +import com.duckduckgo.app.systemsearch.SystemSearchActivity import com.duckduckgo.app.widget.ui.AppWidgetCapabilities import javax.inject.Inject @@ -77,7 +77,7 @@ open class SearchWidget(val layoutId: Int = R.layout.search_widget) : AppWidgetP } private fun buildPendingIntent(context: Context): PendingIntent { - val intent = BrowserActivity.intent(context, widgetSearch = true) + val intent = SystemSearchActivity.intent(context, widgetSearch = true) return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } diff --git a/app/src/main/res/layout/activity_system_search.xml b/app/src/main/res/layout/activity_system_search.xml new file mode 100644 index 000000000000..e198aa6f7f1a --- /dev/null +++ b/app/src/main/res/layout/activity_system_search.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_browser_tab.xml b/app/src/main/res/layout/fragment_browser_tab.xml index 479479d79644..24aa0a1e9a27 100644 --- a/app/src/main/res/layout/fragment_browser_tab.xml +++ b/app/src/main/res/layout/fragment_browser_tab.xml @@ -44,7 +44,6 @@ android:backgroundTint="?attr/colorPrimary" android:clipToPadding="false" android:elevation="4dp" - android:paddingBottom="14dp" app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" 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 fa52a0699d55..e98849b64c26 100644 --- a/app/src/main/res/layout/item_autocomplete_bookmark_suggestion.xml +++ b/app/src/main/res/layout/item_autocomplete_bookmark_suggestion.xml @@ -19,7 +19,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_marginTop="14dp"> + android:layout_marginBottom="8dp" + android:layout_marginTop="6dp"> + android:layout_marginBottom="22dp" + android:layout_marginTop="8dp"> + android:layout_marginBottom="8dp" + android:layout_marginTop="6dp"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 131b1c9c876c..0b2b65db23fa 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -18,6 +18,10 @@ "The webpage could not be displayed." "Reload" + + From this device + Application could not be found + "Welcome to DuckDuckGo!"
Not to worry! Searching and browsing privately is easier than you think.]]>
@@ -48,4 +52,5 @@ Hide remaining tips? There are only a few, and we tried to make them informative. Hide tips forever - \ No newline at end of file + + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09be0970cc8d..4b47c497e0ab 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jul 03 20:57:57 BST 2019 +#Wed Jan 29 21:45:05 GMT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip