From 165adc0e73f4e14015b2a9cd1cd2942df144661d Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Thu, 30 Jan 2020 00:18:25 +0000 Subject: [PATCH 01/25] Update to latest tooling --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From b0a920c720a1ab90542b9ed82fe1ce6a03e45853 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Thu, 30 Jan 2020 00:18:54 +0000 Subject: [PATCH 02/25] Remove unused parameter --- .../duckduckgo/app/privacy/ui/TrackerNetworksViewModelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)) From 84cdb3a3ddba47fc544b5e53b85b1d945ba1cd27 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Thu, 30 Jan 2020 00:19:49 +0000 Subject: [PATCH 03/25] Remove unneeded unwrapping as per lint warning --- .../duckduckgo/app/statistics/VariantManagerTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 c2ae6f9c7063..d7f039bfd193 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt @@ -55,7 +55,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)) } @@ -64,7 +64,7 @@ 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)) @@ -76,14 +76,14 @@ class VariantManagerTest { fun ctaControlVariantIsActiveAndHasNoFeatures() { val variant = variants.firstOrNull { it.key == "mq" } assertEqualsDouble(1.0, variant!!.weight) - assertEquals(0, variant!!.features.size) + assertEquals(0, variant.features.size) } @Test fun ctaSuppressDefaultBrowserVariantIsActiveAndHasSuppressDefaultBrowserFeature() { val variant = variants.firstOrNull { it.key == "mr" } assertEqualsDouble(1.0, variant!!.weight) - assertEquals(1, variant!!.features.size) + assertEquals(1, variant.features.size) assertTrue(variant.hasFeature(SuppressDefaultBrowserCta)) } @@ -91,7 +91,7 @@ class VariantManagerTest { fun ctaSuppressWidgetVariantIsActiveAndHasSuppressWidgetCtaFeature() { val variant = variants.firstOrNull { it.key == "ms" } assertEqualsDouble(1.0, variant!!.weight) - assertEquals(1, variant!!.features.size) + assertEquals(1, variant.features.size) assertTrue(variant.hasFeature(SuppressWidgetCta)) } @@ -99,7 +99,7 @@ class VariantManagerTest { fun ctaSuppressAllVariantIsActiveAndHasSuppressCtaFeatures() { val variant = variants.firstOrNull { it.key == "mt" } assertEqualsDouble(1.0, variant!!.weight) - assertEquals(2, variant!!.features.size) + assertEquals(2, variant.features.size) assertTrue(variant.hasFeature(SuppressDefaultBrowserCta)) assertTrue(variant.hasFeature(SuppressWidgetCta)) } From 75ae65e1e29cd43c18c049f6de964bab01762986 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Thu, 30 Jan 2020 00:23:19 +0000 Subject: [PATCH 04/25] Remove duplicate data and presentation logic from AutoCompleteApi --- .../app/browser/BrowserTabViewModelTest.kt | 11 ++++++----- .../app/autocomplete/api/AutoCompleteApi.kt | 14 ++++++-------- .../duckduckgo/app/browser/BrowserTabViewModel.kt | 9 +++++---- .../BrowserAutoCompleteSuggestionsAdapter.kt | 9 +++++---- 4 files changed, 22 insertions(+), 21 deletions(-) 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 3fdcd34a35c2..ea9e70aad842 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -1254,16 +1254,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 = AutoCompleteApi.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 = AutoCompleteApi.AutoCompleteResult("", suggestions)) testee.fireAutocompletePixel(AutoCompleteSearchSuggestion("example", false)) verify(mockPixel).fire(Pixel.PixelName.AUTOCOMPLETE_SEARCH_SELECTION, pixelParams(showedBookmarks = true, bookmarkCapable = true)) @@ -1272,7 +1273,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 = AutoCompleteApi.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/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApi.kt b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApi.kt index dfcc344dc1c2..974f6dd60223 100644 --- a/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApi.kt +++ b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApi.kt @@ -34,7 +34,7 @@ open class AutoCompleteApi @Inject constructor( 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 +42,7 @@ open class AutoCompleteApi @Inject constructor( BiFunction { bookmarksResults, searchResults -> AutoCompleteResult( query = query, - suggestions = (bookmarksResults + searchResults).distinct(), - hasBookmarks = bookmarksResults.isNotEmpty() + suggestions = (bookmarksResults + searchResults).distinct() ) } ) @@ -71,15 +70,14 @@ open class AutoCompleteApi @Inject constructor( data class AutoCompleteResult( val query: String, - val suggestions: List, - val hasBookmarks: Boolean + val suggestions: List ) - sealed class AutoCompleteSuggestion(val phrase: String, val suggestionType: Int) { + sealed class AutoCompleteSuggestion(val phrase: String) { class AutoCompleteSearchSuggestion(phrase: String, val isUrl: Boolean) : - AutoCompleteSuggestion(phrase, SUGGESTION_TYPE) + AutoCompleteSuggestion(phrase) class AutoCompleteBookmarkSuggestion(phrase: String, val title: String, val url: String) : - AutoCompleteSuggestion(phrase, BOOKMARK_TYPE) + AutoCompleteSuggestion(phrase) } } \ No newline at end of file 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 596639c9ae8c..f452ad5eda6e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -155,7 +155,7 @@ class BrowserTabViewModel( data class AutoCompleteViewState( val showSuggestions: Boolean = false, - val searchResults: AutoCompleteResult = AutoCompleteResult("", emptyList(), false) + val searchResults: AutoCompleteResult = AutoCompleteResult("", emptyList()) ) sealed class Command { @@ -278,7 +278,7 @@ 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 @@ -315,8 +315,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) { @@ -619,7 +620,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..22d402100fc2 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,6 +20,7 @@ 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.AutoCompleteApi.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion import com.duckduckgo.app.browser.autocomplete.AutoCompleteViewHolder.EmptySuggestionViewHolder class BrowserAutoCompleteSuggestionsAdapter( @@ -38,10 +39,10 @@ class BrowserAutoCompleteSuggestionsAdapter( 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 } } From 3271174d4f58788f94456105cebcfa88772cfc2b Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Thu, 30 Jan 2020 00:29:35 +0000 Subject: [PATCH 05/25] Initial interstitial screen --- app/src/main/AndroidManifest.xml | 5 + .../duckduckgo/app/browser/BrowserActivity.kt | 14 -- .../duckduckgo/app/di/AndroidBindingModule.kt | 5 + .../duckduckgo/app/global/ViewModelFactory.kt | 2 + .../app/systemsearch/SystemSearchActivity.kt | 66 ++++++++ .../app/systemsearch/SystemSearchViewModel.kt | 51 +++++++ .../com/duckduckgo/widget/SearchWidget.kt | 4 +- .../res/layout/activity_system_search.xml | 142 ++++++++++++++++++ 8 files changed, 273 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt create mode 100644 app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt create mode 100644 app/src/main/res/layout/activity_system_search.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e75ad3ed1256..32a04b68dea3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,11 @@ + + + LaunchViewModel(onboardingStore, appInstallationReferrerStateListener) + isAssignableFrom(SystemSearchViewModel::class.java) -> SystemSearchViewModel(autoCompleteApi) isAssignableFrom(OnboardingViewModel::class.java) -> onboardingViewModel() isAssignableFrom(BrowserViewModel::class.java) -> browserViewModel() isAssignableFrom(BrowserTabViewModel::class.java) -> browserTabViewModel() 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..a9a119647470 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -0,0 +1,66 @@ +/* + * 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.Context +import android.content.Intent +import android.os.Bundle +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.global.DuckDuckGoActivity +import com.duckduckgo.app.statistics.pixels.Pixel +import timber.log.Timber +import javax.inject.Inject + +class SystemSearchActivity : DuckDuckGoActivity() { + + private val viewModel: SystemSearchViewModel by bindViewModel() + + @Inject + lateinit var pixel: Pixel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_system_search) + } + + override fun onNewIntent(newIntent: Intent?) { + super.onNewIntent(newIntent) + Timber.i("onNewIntent: $newIntent") + + val intent = newIntent ?: return + if (launchedFromWidget(intent)) { + Timber.w("System search launched from widget") + pixel.fire(Pixel.PixelName.WIDGET_LAUNCHED) + return + } + } + + private fun launchedFromWidget(intent: Intent): Boolean { + return intent.getBooleanExtra(WIDGET_SEARCH_EXTRA, false) + } + + companion object { + + fun intent(context: Context, widgetSearch: Boolean = false): Intent { + val intent = Intent(context, SystemSearchActivity::class.java) + intent.putExtra(WIDGET_SEARCH_EXTRA, widgetSearch) + return intent + } + + const val WIDGET_SEARCH_EXTRA = "WIDGET_SEARCH_EXTRA" + } +} \ 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..b53f62a5aae4 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -0,0 +1,51 @@ +/* + * 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.annotation.SuppressLint +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.duckduckgo.app.autocomplete.api.AutoCompleteApi +import com.jakewharton.rxrelay2.PublishRelay +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.util.concurrent.TimeUnit + +class SystemSearchViewModel( + private val autoCompleteApi: AutoCompleteApi +) : ViewModel() { + + private val autoCompletePublishSubject = PublishRelay.create() + private val autoCompleteResults: MutableLiveData = MutableLiveData() + + @SuppressLint("CheckResult") + private fun configureAutoComplete() { + autoCompleteResults.value = AutoCompleteApi.AutoCompleteResult("", emptyList()) + + autoCompletePublishSubject + .debounce(300, TimeUnit.MILLISECONDS) + .switchMap { autoCompleteApi.autoComplete(it) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ result -> + autoCompleteResults.value = result + }, { t: Throwable? -> Timber.w(t, "Failed to get search results") }) + } + + +} 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..96d57c7b69d5 --- /dev/null +++ b/app/src/main/res/layout/activity_system_search.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From b4365ba4dd12090340121580f1fbb70b439c6daa Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 3 Feb 2020 20:44:03 +0000 Subject: [PATCH 06/25] Add app list to view --- .../com/duckduckgo/app/di/TestAppComponent.kt | 1 + app/src/main/AndroidManifest.xml | 31 +++-- .../app/autocomplete/api/AutoCompleteApi.kt | 2 - .../duckduckgo/app/browser/BrowserActivity.kt | 3 +- .../BrowserAutoCompleteSuggestionsAdapter.kt | 16 ++- .../com/duckduckgo/app/di/AppComponent.kt | 1 + .../app/di/PlayStoreReferralModule.kt | 4 - .../app/di/SystemComponentsModule.kt | 36 ++++++ .../duckduckgo/app/global/ViewModelFactory.kt | 4 +- .../duckduckgo/app/statistics/pixels/Pixel.kt | 5 +- .../DeviceAppSuggestionsAdapter.kt | 67 ++++++++++ .../app/systemsearch/DeviceInstalledApps.kt | 81 ++++++++++++ .../app/systemsearch/SystemSearchActivity.kt | 122 ++++++++++++++++-- .../app/systemsearch/SystemSearchViewModel.kt | 79 +++++++++++- .../res/layout/activity_system_search.xml | 89 ++++++++----- .../item_autocomplete_bookmark_suggestion.xml | 3 +- .../item_autocomplete_no_suggestions.xml | 4 +- .../item_autocomplete_search_suggestion.xml | 3 +- .../res/layout/item_device_app_suggestion.xml | 55 ++++++++ .../main/res/values/string-untranslated.xml | 4 + 20 files changed, 529 insertions(+), 81 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt create mode 100644 app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt create mode 100644 app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt create mode 100644 app/src/main/res/layout/item_device_app_suggestion.xml 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 11b1ed319f4c..a718beebec91 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/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 32a04b68dea3..dc3b5a6e08f6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -48,7 +48,24 @@ + android:label="@string/appName" + android:launchMode="singleTask" + android:documentLaunchMode="intoExisting" + android:stateNotNeeded="true" + android:autoRemoveFromRecents="true"> + + + + + + + + + + + + + - - - - - - - - - - - - Unit, - private val editableSearchClickListener: (AutoCompleteSuggestion) -> Unit -) : RecyclerView.Adapter() { + private val editableSearchClickListener: (AutoCompleteSuggestion) -> Unit, + private val showsMessageOnNoSuggestions: Boolean = true + ) : RecyclerView.Adapter() { private val viewHolderFactoryMap: Map = mapOf( EMPTY_TYPE to EmptySuggestionViewHolderFactory(), @@ -63,19 +67,19 @@ class BrowserAutoCompleteSuggestionsAdapter( if (suggestions.isNotEmpty()) { return suggestions.size } - - // if there are no suggestions, we'll use a recycler row to display "no suggestions" - return 1 + return if (showsMessageOnNoSuggestions) 1 else 0 } @UiThread fun updateData(newSuggestions: List) { + if (suggestions == newSuggestions) return + suggestions.clear() suggestions.addAll(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/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/PlayStoreReferralModule.kt b/app/src/main/java/com/duckduckgo/app/di/PlayStoreReferralModule.kt index 59cc1a5d9fd5..f0d3e77d36df 100644 --- a/app/src/main/java/com/duckduckgo/app/di/PlayStoreReferralModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/PlayStoreReferralModule.kt @@ -28,10 +28,6 @@ import javax.inject.Singleton @Module class PlayStoreReferralModule { - @Singleton - @Provides - fun packageManager(context: Context) = context.packageManager - @Provides fun appInstallationReferrerParser(pixel: Pixel): AppInstallationReferrerParser { return QueryParamReferrerParser(pixel) 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..9770840aba1d --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt @@ -0,0 +1,36 @@ +/* + * 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.DeviceAppsLookup +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +open class SystemComponentsModule { + + @Singleton + @Provides + fun packageManager(context: Context) = context.packageManager + + @Provides + @Singleton + fun deviceAppsLookup(packageManager: PackageManager): DeviceAppsLookup = DeviceAppsLookup(packageManager) +} \ 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 5cd5b5579c25..7a4144beda4b 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,7 @@ 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.DeviceAppsLookup import com.duckduckgo.app.systemsearch.SystemSearchViewModel import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel @@ -86,6 +87,7 @@ class ViewModelFactory @Inject constructor( private val bookmarksDao: BookmarksDao, private val surveyDao: SurveyDao, private val autoCompleteApi: AutoCompleteApi, + private val deviceAppsLookup: DeviceAppsLookup, private val appSettingsPreferencesStore: SettingsDataStore, private val webViewLongPressHandler: LongPressHandler, private val defaultBrowserDetector: DefaultBrowserDetector, @@ -111,7 +113,7 @@ class ViewModelFactory @Inject constructor( with(modelClass) { when { isAssignableFrom(LaunchViewModel::class.java) -> LaunchViewModel(onboardingStore, appInstallationReferrerStateListener) - isAssignableFrom(SystemSearchViewModel::class.java) -> SystemSearchViewModel(autoCompleteApi) + isAssignableFrom(SystemSearchViewModel::class.java) -> SystemSearchViewModel(autoCompleteApi, deviceAppsLookup) isAssignableFrom(OnboardingViewModel::class.java) -> onboardingViewModel() isAssignableFrom(BrowserViewModel::class.java) -> browserViewModel() isAssignableFrom(BrowserTabViewModel::class.java) -> browserTabViewModel() 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 5e059a5cecc8..24a69190091e 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 @@ -95,7 +95,10 @@ 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"), + + WIDGET_LAUNCH(pixelName = "m_w_l"), + ASSIST_LAUNCH(pixelName = "m_a_l"), + GOOGLE_BAR_LAUNCH(pixelName = "m_gb_l"), LONG_PRESS("mlp"), LONG_PRESS_DOWNLOAD_IMAGE("mlp_i"), 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..b034f1eb5c66 --- /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.shortActivityName + holder.root.setOnClickListener { + clickListener(app) + } + val drawable = holder.icon.context.packageManager.getApplicationIcon(app.packageName) + 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/DeviceInstalledApps.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt new file mode 100644 index 000000000000..0a7867cb6b11 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt @@ -0,0 +1,81 @@ +/* + * 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.ApplicationInfo +import android.content.pm.PackageManager +import timber.log.Timber +import java.util.* + +data class DeviceApp( + val shortActivityName: String, + val fullActivityName: String, + val packageName: String, + val launchIntent: Intent +) + +class DeviceAppsLookup(private val packageManager: PackageManager) { + + private val allApps by lazy { all() } + + fun query(query: String): List { + + if (query.isBlank()) return emptyList() + + return allApps.filter { + it.shortActivityName.contains(query, ignoreCase = true) + } + } + + private fun all(): List { + val intent = Intent(Intent.ACTION_MAIN) + val resInfos = packageManager.queryIntentActivities(intent, 0) + val mainPackages = mutableSetOf() + val appInfos = mutableListOf() + + for (resInfo in resInfos) { + val packageName = resInfo.activityInfo.packageName + mainPackages.add(packageName) + } + + for (packageName in mainPackages) { + appInfos.add(packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)) + } + + Collections.sort(appInfos, ApplicationInfo.DisplayNameComparator(packageManager)) + + Timber.i("Found ${appInfos.size} matching apps") + + val appsList: List = appInfos.map { + val shortName = packageManager.getApplicationLabel(it).toString() + val packageName = it.packageName + val fullActivityName = packageManager.getApplicationInfo(packageName, 0).className + val icon = it.icon + Timber.i("Short name: $shortName, package: $packageName, full activity name: $fullActivityName") + + val launchIntent = packageManager.getLaunchIntentForPackage(packageName) + if (fullActivityName == null) return@map null + if (launchIntent == null) return@map null + return@map DeviceApp(shortName, fullActivityName, packageName, launchIntent) + + }.filterNotNull() + + Timber.i("Found ${appsList.size} matching Activities") + return appsList + } +} \ 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 index a9a119647470..84309b39aa60 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -19,48 +19,154 @@ package com.duckduckgo.app.systemsearch 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 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.global.DuckDuckGoActivity +import com.duckduckgo.app.global.view.TextChangedWatcher import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.LaunchApplication +import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.LaunchBrowser +import com.duckduckgo.app.systemsearch.SystemSearchViewModel.SystemSearchViewState +import kotlinx.android.synthetic.main.activity_system_search.* import timber.log.Timber import javax.inject.Inject class SystemSearchActivity : DuckDuckGoActivity() { - private val viewModel: SystemSearchViewModel by bindViewModel() - @Inject lateinit var pixel: Pixel + 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) { + viewModel.userUpdatedQuery(omnibarTextInput.text.toString()) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_system_search) + configureObservers() + configureAutoComplete() + configureDeviceAppSuggestions() + configureTextInput() } override fun onNewIntent(newIntent: Intent?) { super.onNewIntent(newIntent) Timber.i("onNewIntent: $newIntent") + viewModel.resetViewState() val intent = newIntent ?: return - if (launchedFromWidget(intent)) { - Timber.w("System search launched from widget") - pixel.fire(Pixel.PixelName.WIDGET_LAUNCHED) - return + when { + launchedFromAssist(intent) -> pixel.fire(Pixel.PixelName.ASSIST_LAUNCH) + launchedFromWidget(intent) -> pixel.fire(Pixel.PixelName.WIDGET_LAUNCH) + launchedFromAppBar(intent) -> pixel.fire(Pixel.PixelName.GOOGLE_BAR_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.userSubmittedQuery(it.phrase) + }, + editableSearchClickListener = { + viewModel.userUpdatedQuery(it.phrase) + }, + showsMessageOnNoSuggestions = false + ) + autocompleteSuggestions.adapter = autocompleteSuggestionsAdapter + } + + private fun configureDeviceAppSuggestions() { + deviceAppSuggestions.layoutManager = LinearLayoutManager(this) + deviceAppSuggestionsAdapter = DeviceAppSuggestionsAdapter { + viewModel.userSelectedApp(it) + } + deviceAppSuggestions.adapter = deviceAppSuggestionsAdapter + } + + 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) + } + autocompleteSuggestionsAdapter.updateData(viewState.autocompleteResults) + deviceLabel.isVisible = viewState.appResults.isNotEmpty() + deviceAppSuggestionsAdapter.updateData(viewState.appResults) + } + + private fun processCommand(command: SystemSearchViewModel.Command) { + when (command) { + is LaunchBrowser -> { + startActivity(BrowserActivity.intent(this, command.query)) + finish() + + } + is LaunchApplication -> { + startActivity(command.intent) + finish() + } + } + } + + private fun launchedFromAppBar(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" + fun intent(context: Context, widgetSearch: Boolean = false): Intent { val intent = Intent(context, SystemSearchActivity::class.java) intent.putExtra(WIDGET_SEARCH_EXTRA, widgetSearch) return intent } - - const val WIDGET_SEARCH_EXTRA = "WIDGET_SEARCH_EXTRA" } } \ 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 index b53f62a5aae4..f8ad727a4f80 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -16,10 +16,11 @@ package com.duckduckgo.app.systemsearch -import android.annotation.SuppressLint +import android.content.Intent import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.duckduckgo.app.autocomplete.api.AutoCompleteApi +import com.duckduckgo.app.global.SingleLiveEvent import com.jakewharton.rxrelay2.PublishRelay import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers @@ -27,25 +28,89 @@ import timber.log.Timber import java.util.concurrent.TimeUnit class SystemSearchViewModel( - private val autoCompleteApi: AutoCompleteApi + private val autoCompleteApi: AutoCompleteApi, + private val deviceAppsLookup: DeviceAppsLookup ) : ViewModel() { + data class SystemSearchViewState( + val queryText: String = "", + val autocompleteResults: List = emptyList(), + val appResults: List = emptyList() + ) + + sealed class Command { + data class LaunchBrowser(val query: String) : Command() + data class LaunchApplication(val intent: Intent) : Command() + } + + val viewState: MutableLiveData = MutableLiveData() + val command: SingleLiveEvent = SingleLiveEvent() + private val autoCompletePublishSubject = PublishRelay.create() - private val autoCompleteResults: MutableLiveData = MutableLiveData() + private var appResults: List = emptyList() + private var autocompleteResults: List = emptyList() - @SuppressLint("CheckResult") - private fun configureAutoComplete() { - autoCompleteResults.value = AutoCompleteApi.AutoCompleteResult("", emptyList()) + init { + resetViewState() + configureAutoComplete() + } + private fun currentViewState(): SystemSearchViewState = viewState.value!! + + fun resetViewState() { + viewState.value = SystemSearchViewState() + } + + private fun configureAutoComplete() { autoCompletePublishSubject .debounce(300, TimeUnit.MILLISECONDS) .switchMap { autoCompleteApi.autoComplete(it) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ result -> - autoCompleteResults.value = result + updateAutocompleteResult(result) }, { t: Throwable? -> Timber.w(t, "Failed to get search results") }) } + fun userUpdatedQuery(query: String) { + val trimmedQuery = query.trim() + if (trimmedQuery != currentViewState().queryText) { + viewState.value = currentViewState().copy(queryText = trimmedQuery) + updateAppResults(deviceAppsLookup.query(query)) + autoCompletePublishSubject.accept(trimmedQuery) + } + } + + private fun updateAppResults(results: List) { + appResults = results + refreshViewStateResults() + } + + private fun updateAutocompleteResult(results: AutoCompleteApi.AutoCompleteResult) { + autocompleteResults = results.suggestions + refreshViewStateResults() + } + + private fun refreshViewStateResults() { + val hasAllResults = autocompleteResults.isNotEmpty() && appResults.isNotEmpty() + val newSuggestions = if (hasAllResults) autocompleteResults.take(4) else autocompleteResults + val newApps = if (hasAllResults) appResults.take(4) else appResults + viewState.value = currentViewState().copy( + autocompleteResults = newSuggestions, + appResults = newApps + ) + } + fun userClearedQuery() { + viewState.value = currentViewState().copy(queryText = "", appResults = emptyList()) + autoCompletePublishSubject.accept("") + } + + fun userSubmittedQuery(query: String) { + command.value = Command.LaunchBrowser(query) + } + + fun userSelectedApp(app: DeviceApp) { + command.value = Command.LaunchApplication(app.launchIntent) + } } diff --git a/app/src/main/res/layout/activity_system_search.xml b/app/src/main/res/layout/activity_system_search.xml index 96d57c7b69d5..de04e345654e 100644 --- a/app/src/main/res/layout/activity_system_search.xml +++ b/app/src/main/res/layout/activity_system_search.xml @@ -14,28 +14,28 @@ ~ limitations under the License. --> - + android:layout_height="match_parent"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:theme="@style/OmnibarToolbarTheme"> + android:layout_marginEnd="16dp" + android:layout_marginBottom="8dp"> - @@ -77,8 +74,8 @@ style="@style/Base.V7.Widget.AppCompat.EditText" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" android:background="@android:color/transparent" android:fontFamily="sans-serif-medium" android:hint="@string/omnibarInputHint" @@ -89,11 +86,11 @@ android:textColor="?attr/omnibarTextColor" android:textColorHint="?attr/omnibarHintColor" android:textCursorDrawable="@drawable/text_cursor" - android:textSize="13sp" + android:textSize="16sp" android:textStyle="normal" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/clearTextButton" - app:layout_constraintStart_toStartOf="parent" + app:layout_constraintStart_toEndOf="@id/logo" app:layout_constraintTop_toTopOf="parent" tools:text="https://duckduckgo.com/?q=areallylongexampleexample"> @@ -123,20 +120,46 @@ + + + + + app:layout_constraintTop_toBottomOf="@id/deviceLabel" + tools:itemCount="4" + tools:listitem="@layout/item_device_app_suggestion" /> - \ No newline at end of file + \ 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 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="8dp" + 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 10b236b0f556..dab776bb76ed 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -18,6 +18,9 @@ "The webpage could not be displayed." "Reload" + + From this device: + Scanning %s. @@ -52,4 +55,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 From 45676b365c83a209553e96fd474f808bc6bbe977 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Sun, 9 Feb 2020 19:16:16 +0000 Subject: [PATCH 07/25] Tidy up device lookup and move off main thread --- .../DeviceAppSuggestionsAdapter.kt | 2 +- .../app/systemsearch/DeviceInstalledApps.kt | 52 +++++++------------ .../app/systemsearch/SystemSearchViewModel.kt | 15 ++++-- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt index b034f1eb5c66..55f9f45874c1 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt @@ -39,7 +39,7 @@ class DeviceAppSuggestionsAdapter( override fun onBindViewHolder(holder: DeviceAppViewHolder, position: Int) { val app = deviceApps[position] - holder.title.text = app.shortActivityName + holder.title.text = app.shortName holder.root.setOnClickListener { clickListener(app) } diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt index 0a7867cb6b11..666df22e9cb2 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt @@ -17,14 +17,13 @@ package com.duckduckgo.app.systemsearch import android.content.Intent -import android.content.pm.ApplicationInfo +import android.content.pm.ApplicationInfo.DisplayNameComparator import android.content.pm.PackageManager -import timber.log.Timber -import java.util.* +import androidx.annotation.WorkerThread data class DeviceApp( - val shortActivityName: String, - val fullActivityName: String, + val shortName: String, + val longName: String, val packageName: String, val launchIntent: Intent ) @@ -33,49 +32,34 @@ class DeviceAppsLookup(private val packageManager: PackageManager) { private val allApps by lazy { all() } + @WorkerThread fun query(query: String): List { if (query.isBlank()) return emptyList() return allApps.filter { - it.shortActivityName.contains(query, ignoreCase = true) + it.shortName.contains(query, ignoreCase = true) } } + @WorkerThread private fun all(): List { - val intent = Intent(Intent.ACTION_MAIN) - val resInfos = packageManager.queryIntentActivities(intent, 0) - val mainPackages = mutableSetOf() - val appInfos = mutableListOf() - for (resInfo in resInfos) { - val packageName = resInfo.activityInfo.packageName - mainPackages.add(packageName) - } - - for (packageName in mainPackages) { - appInfos.add(packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)) - } - - Collections.sort(appInfos, ApplicationInfo.DisplayNameComparator(packageManager)) + val mainIntent = Intent(Intent.ACTION_MAIN) + val mainActivities = packageManager.queryIntentActivities(mainIntent, 0) - Timber.i("Found ${appInfos.size} matching apps") + val appsInfo = mainActivities.map { + it.activityInfo.packageName + }.toSet().map { + packageManager.getApplicationInfo(it, PackageManager.GET_META_DATA) + }.sortedWith(DisplayNameComparator(packageManager)) - val appsList: List = appInfos.map { + return appsInfo.map { val shortName = packageManager.getApplicationLabel(it).toString() val packageName = it.packageName - val fullActivityName = packageManager.getApplicationInfo(packageName, 0).className - val icon = it.icon - Timber.i("Short name: $shortName, package: $packageName, full activity name: $fullActivityName") - - val launchIntent = packageManager.getLaunchIntentForPackage(packageName) - if (fullActivityName == null) return@map null - if (launchIntent == null) return@map null - return@map DeviceApp(shortName, fullActivityName, packageName, launchIntent) - + val fullName = packageManager.getApplicationInfo(packageName, 0).className ?: return@map null + val launchIntent = packageManager.getLaunchIntentForPackage(packageName) ?: return@map null + return@map DeviceApp(shortName, fullName, packageName, launchIntent) }.filterNotNull() - - Timber.i("Found ${appsList.size} matching Activities") - return appsList } } \ 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 index f8ad727a4f80..434d3cb87710 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -19,11 +19,15 @@ package com.duckduckgo.app.systemsearch import android.content.Intent import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.duckduckgo.app.autocomplete.api.AutoCompleteApi import com.duckduckgo.app.global.SingleLiveEvent import com.jakewharton.rxrelay2.PublishRelay import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.util.concurrent.TimeUnit @@ -76,8 +80,12 @@ class SystemSearchViewModel( val trimmedQuery = query.trim() if (trimmedQuery != currentViewState().queryText) { viewState.value = currentViewState().copy(queryText = trimmedQuery) - updateAppResults(deviceAppsLookup.query(query)) autoCompletePublishSubject.accept(trimmedQuery) + viewModelScope.launch { + withContext(Dispatchers.IO) { + updateAppResults(deviceAppsLookup.query(query)) + } + } } } @@ -95,10 +103,7 @@ class SystemSearchViewModel( val hasAllResults = autocompleteResults.isNotEmpty() && appResults.isNotEmpty() val newSuggestions = if (hasAllResults) autocompleteResults.take(4) else autocompleteResults val newApps = if (hasAllResults) appResults.take(4) else appResults - viewState.value = currentViewState().copy( - autocompleteResults = newSuggestions, - appResults = newApps - ) + viewState.postValue(currentViewState().copy(autocompleteResults = newSuggestions, appResults = newApps)) } fun userClearedQuery() { From ea4578c7b9d11044c2209d0eb48999e32b6071b1 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Sun, 9 Feb 2020 19:22:19 +0000 Subject: [PATCH 08/25] Use mini logo for better sizing --- app/src/main/res/layout/activity_system_search.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/activity_system_search.xml b/app/src/main/res/layout/activity_system_search.xml index de04e345654e..e14c8ff98de3 100644 --- a/app/src/main/res/layout/activity_system_search.xml +++ b/app/src/main/res/layout/activity_system_search.xml @@ -64,7 +64,7 @@ android:layout_width="30dp" android:layout_height="30dp" android:importantForAccessibility="no" - android:src="@drawable/logo_medium" + android:src="@drawable/logo_mini" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> From 36b1d4be1eb0d0d800a18b5b4461bbe215a642db Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Sun, 9 Feb 2020 20:56:18 +0000 Subject: [PATCH 09/25] Add scrolling --- .../res/layout/activity_system_search.xml | 116 ++++++++++-------- 1 file changed, 65 insertions(+), 51 deletions(-) diff --git a/app/src/main/res/layout/activity_system_search.xml b/app/src/main/res/layout/activity_system_search.xml index e14c8ff98de3..109dd02319db 100644 --- a/app/src/main/res/layout/activity_system_search.xml +++ b/app/src/main/res/layout/activity_system_search.xml @@ -14,28 +14,28 @@ ~ limitations under the License. --> - + android:layout_height="match_parent" + app:layout_scrollFlags="scroll|enterAlways"> + app:layout_scrollFlags="scroll|enterAlways"> + android:theme="@style/OmnibarToolbarTheme" + app:layout_scrollFlags="scroll|enterAlways"> - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + \ No newline at end of file From 026a68e6e2d15f9f72c484cbe09f1d02ed64ee93 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Sun, 9 Feb 2020 22:00:33 +0000 Subject: [PATCH 10/25] Add view state reset and tweak app label text size --- .../app/systemsearch/SystemSearchActivity.kt | 2 +- .../app/systemsearch/SystemSearchViewModel.kt | 43 +++++++++++++------ .../res/layout/activity_system_search.xml | 10 +++-- 3 files changed, 36 insertions(+), 19 deletions(-) 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 84309b39aa60..a45cf6b86502 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -68,7 +68,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { super.onNewIntent(newIntent) Timber.i("onNewIntent: $newIntent") - viewModel.resetViewState() + viewModel.resetState() val intent = newIntent ?: return when { launchedFromAssist(intent) -> pixel.fire(Pixel.PixelName.ASSIST_LAUNCH) 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 434d3cb87710..591f1b3712d5 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -55,19 +55,21 @@ class SystemSearchViewModel( private var autocompleteResults: List = emptyList() init { - resetViewState() + resetState() configureAutoComplete() } private fun currentViewState(): SystemSearchViewState = viewState.value!! - fun resetViewState() { + fun resetState() { + autocompleteResults = emptyList() + appResults = emptyList() viewState.value = SystemSearchViewState() } private fun configureAutoComplete() { autoCompletePublishSubject - .debounce(300, TimeUnit.MILLISECONDS) + .debounce(DEBOUNCE_TIME_MS, TimeUnit.MILLISECONDS) .switchMap { autoCompleteApi.autoComplete(it) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -78,13 +80,21 @@ class SystemSearchViewModel( fun userUpdatedQuery(query: String) { val trimmedQuery = query.trim() - if (trimmedQuery != currentViewState().queryText) { - viewState.value = currentViewState().copy(queryText = trimmedQuery) - autoCompletePublishSubject.accept(trimmedQuery) - viewModelScope.launch { - withContext(Dispatchers.IO) { - updateAppResults(deviceAppsLookup.query(query)) - } + + if (trimmedQuery == currentViewState().queryText) { + return + } + + if (trimmedQuery.isBlank()) { + userClearedQuery() + return + } + + viewState.value = currentViewState().copy(queryText = trimmedQuery) + autoCompletePublishSubject.accept(trimmedQuery) + viewModelScope.launch { + withContext(Dispatchers.IO) { + updateAppResults(deviceAppsLookup.query(query)) } } } @@ -100,15 +110,15 @@ class SystemSearchViewModel( } private fun refreshViewStateResults() { - val hasAllResults = autocompleteResults.isNotEmpty() && appResults.isNotEmpty() - val newSuggestions = if (hasAllResults) autocompleteResults.take(4) else autocompleteResults - val newApps = if (hasAllResults) appResults.take(4) else appResults + val hasMultiResults = autocompleteResults.isNotEmpty() && appResults.isNotEmpty() + val newSuggestions = if (hasMultiResults) autocompleteResults.take(MULTI_RESULTS_MAX_PER_GROUP) else autocompleteResults + val newApps = if (hasMultiResults) appResults.take(MULTI_RESULTS_MAX_PER_GROUP) else appResults viewState.postValue(currentViewState().copy(autocompleteResults = newSuggestions, appResults = newApps)) } fun userClearedQuery() { - viewState.value = currentViewState().copy(queryText = "", appResults = emptyList()) autoCompletePublishSubject.accept("") + resetState() } fun userSubmittedQuery(query: String) { @@ -118,4 +128,9 @@ class SystemSearchViewModel( fun userSelectedApp(app: DeviceApp) { command.value = Command.LaunchApplication(app.launchIntent) } + + companion object { + private const val DEBOUNCE_TIME_MS = 300L + private const val MULTI_RESULTS_MAX_PER_GROUP = 4 + } } diff --git a/app/src/main/res/layout/activity_system_search.xml b/app/src/main/res/layout/activity_system_search.xml index 109dd02319db..68e00bf5d6cb 100644 --- a/app/src/main/res/layout/activity_system_search.xml +++ b/app/src/main/res/layout/activity_system_search.xml @@ -148,12 +148,14 @@ android:layout_height="wrap_content" android:background="?attr/colorPrimary" android:backgroundTint="?attr/colorPrimary" + android:textSize="13dp" android:elevation="4dp" - android:paddingStart="8dp" - android:paddingTop="8dp" - android:paddingEnd="8dp" + android:paddingStart="16dp" + android:paddingTop="6dp" + android:paddingBottom="2dp" + android:paddingEnd="16dp" android:text="@string/system_search_app_label" - android:textColor="@color/warGreyTwo" + android:textColor="@color/grayish" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/autocompleteSuggestions" From 12b22bccbbcf5eb4712b0a8abb193e2bf5393483 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 17 Feb 2020 14:24:26 +0000 Subject: [PATCH 11/25] Remove colon from system search text as per design feedback --- app/src/main/res/values/string-untranslated.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 4ef933029037..331b68a18f87 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -19,7 +19,7 @@ "Reload" - From this device: + From this device Scanning From 3be73e3fbda990b5591b01c62429e46b876333d0 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 17 Feb 2020 14:48:23 +0000 Subject: [PATCH 12/25] Update layout to show shadow at the bottom of autocomplete --- .../res/layout/activity_system_search.xml | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/layout/activity_system_search.xml b/app/src/main/res/layout/activity_system_search.xml index 68e00bf5d6cb..1665ab29109c 100644 --- a/app/src/main/res/layout/activity_system_search.xml +++ b/app/src/main/res/layout/activity_system_search.xml @@ -123,19 +123,20 @@ + android:layout_height="wrap_content" + android:background="?attr/colorPrimary" + android:backgroundTint="?attr/colorPrimary" + android:elevation="4dp"> Date: Mon, 17 Feb 2020 18:09:30 +0000 Subject: [PATCH 13/25] Enable "no suggestions" and ensure it doesn't flash up on the screen --- .../app/browser/BrowserTabFragment.kt | 3 +-- .../BrowserAutoCompleteSuggestionsAdapter.kt | 24 +++++++++---------- .../app/systemsearch/SystemSearchActivity.kt | 5 ++-- .../app/systemsearch/SystemSearchViewModel.kt | 16 ++++++------- .../main/res/layout/fragment_browser_tab.xml | 1 - .../item_autocomplete_no_suggestions.xml | 2 +- 6 files changed, 24 insertions(+), 27 deletions(-) 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 bf2c102402f8..46b33a7e234c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1157,8 +1157,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/autocomplete/BrowserAutoCompleteSuggestionsAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/autocomplete/BrowserAutoCompleteSuggestionsAdapter.kt index 34931c1cdd5e..02841379c080 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 @@ -28,16 +28,17 @@ import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAda class BrowserAutoCompleteSuggestionsAdapter( private val immediateSearchClickListener: (AutoCompleteSuggestion) -> Unit, - private val editableSearchClickListener: (AutoCompleteSuggestion) -> Unit, - private val showsMessageOnNoSuggestions: Boolean = true - ) : RecyclerView.Adapter() { + private val editableSearchClickListener: (AutoCompleteSuggestion) -> Unit +) : RecyclerView.Adapter() { private val viewHolderFactoryMap: Map = mapOf( EMPTY_TYPE to EmptySuggestionViewHolderFactory(), 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) @@ -64,18 +65,17 @@ class BrowserAutoCompleteSuggestionsAdapter( } override fun getItemCount(): Int { - if (suggestions.isNotEmpty()) { - return suggestions.size + if (suggestions.isEmpty() && phrase.isNotBlank()) { + return 1 // No suggestions message } - return if (showsMessageOnNoSuggestions) 1 else 0 + return suggestions.size } @UiThread - fun updateData(newSuggestions: List) { - if (suggestions == newSuggestions) return - - suggestions.clear() - suggestions.addAll(newSuggestions) + fun updateData(newPhrase: String, newSuggestions: List) { + if (phrase == newPhrase && suggestions == newSuggestions) return + phrase = newPhrase + suggestions = newSuggestions notifyDataSetChanged() } 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 a45cf6b86502..e2b28aa69460 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -94,8 +94,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { }, editableSearchClickListener = { viewModel.userUpdatedQuery(it.phrase) - }, - showsMessageOnNoSuggestions = false + } ) autocompleteSuggestions.adapter = autocompleteSuggestionsAdapter } @@ -127,8 +126,8 @@ class SystemSearchActivity : DuckDuckGoActivity() { omnibarTextInput.setText(viewState.queryText) omnibarTextInput.setSelection(viewState.queryText.length) } - autocompleteSuggestionsAdapter.updateData(viewState.autocompleteResults) deviceLabel.isVisible = viewState.appResults.isNotEmpty() + autocompleteSuggestionsAdapter.updateData(viewState.autocompleteResults.query, viewState.autocompleteResults.suggestions) deviceAppSuggestionsAdapter.updateData(viewState.appResults) } 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 591f1b3712d5..5f4b6110bdd9 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -38,7 +38,7 @@ class SystemSearchViewModel( data class SystemSearchViewState( val queryText: String = "", - val autocompleteResults: List = emptyList(), + val autocompleteResults: AutoCompleteApi.AutoCompleteResult = AutoCompleteApi.AutoCompleteResult("", emptyList()), val appResults: List = emptyList() ) @@ -52,7 +52,7 @@ class SystemSearchViewModel( private val autoCompletePublishSubject = PublishRelay.create() private var appResults: List = emptyList() - private var autocompleteResults: List = emptyList() + private var autocompleteResults: AutoCompleteApi.AutoCompleteResult = AutoCompleteApi.AutoCompleteResult("", emptyList()) init { resetState() @@ -62,9 +62,9 @@ class SystemSearchViewModel( private fun currentViewState(): SystemSearchViewState = viewState.value!! fun resetState() { - autocompleteResults = emptyList() - appResults = emptyList() viewState.value = SystemSearchViewState() + autocompleteResults = AutoCompleteApi.AutoCompleteResult("", emptyList()) + appResults = emptyList() } private fun configureAutoComplete() { @@ -105,15 +105,15 @@ class SystemSearchViewModel( } private fun updateAutocompleteResult(results: AutoCompleteApi.AutoCompleteResult) { - autocompleteResults = results.suggestions + autocompleteResults = results refreshViewStateResults() } private fun refreshViewStateResults() { - val hasMultiResults = autocompleteResults.isNotEmpty() && appResults.isNotEmpty() - val newSuggestions = if (hasMultiResults) autocompleteResults.take(MULTI_RESULTS_MAX_PER_GROUP) else autocompleteResults + val hasMultiResults = autocompleteResults.suggestions.isNotEmpty() && appResults.isNotEmpty() + val newSuggestions = if (hasMultiResults) autocompleteResults.suggestions.take(MULTI_RESULTS_MAX_PER_GROUP) else autocompleteResults.suggestions val newApps = if (hasMultiResults) appResults.take(MULTI_RESULTS_MAX_PER_GROUP) else appResults - viewState.postValue(currentViewState().copy(autocompleteResults = newSuggestions, appResults = newApps)) + viewState.postValue(currentViewState().copy(autocompleteResults = AutoCompleteApi.AutoCompleteResult(autocompleteResults.query, newSuggestions), appResults = newApps)) } fun userClearedQuery() { 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_no_suggestions.xml b/app/src/main/res/layout/item_autocomplete_no_suggestions.xml index d1a9c4d97aa2..a62f8d6c26d0 100644 --- a/app/src/main/res/layout/item_autocomplete_no_suggestions.xml +++ b/app/src/main/res/layout/item_autocomplete_no_suggestions.xml @@ -18,7 +18,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="8dp" + android:layout_marginBottom="22dp" android:layout_marginTop="8dp"> Date: Thu, 20 Feb 2020 20:00:32 +0000 Subject: [PATCH 14/25] Fix scrolling --- .../app/systemsearch/SystemSearchActivity.kt | 25 ++- .../app/systemsearch/SystemSearchViewModel.kt | 26 ++- .../res/layout/activity_system_search.xml | 164 ++++++++---------- 3 files changed, 117 insertions(+), 98 deletions(-) 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 e2b28aa69460..5fda399d6eb6 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -29,6 +29,7 @@ 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 @@ -44,13 +45,16 @@ class SystemSearchActivity : DuckDuckGoActivity() { @Inject lateinit var pixel: Pixel - private val viewModel: SystemSearchViewModel by bindViewModel() + @Inject + lateinit var omnibardScrolling: 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()) } } @@ -61,6 +65,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { configureObservers() configureAutoComplete() configureDeviceAppSuggestions() + configureOmnibar() configureTextInput() } @@ -107,6 +112,18 @@ class SystemSearchActivity : DuckDuckGoActivity() { deviceAppSuggestions.adapter = deviceAppSuggestionsAdapter } + private fun configureOmnibar() { + results.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + val scrollable = results.maxScrollAmount > 0 + if (scrollable) { + omnibardScrolling.enableOmnibarScrolling(toolbar) + } else { + appBarLayout.setExpanded(true, true) + omnibardScrolling.disableOmnibarScrolling(toolbar) + } + } + } + private fun configureTextInput() { omnibarTextInput.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, keyEvent -> if (actionId == EditorInfo.IME_ACTION_GO || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER) { @@ -126,6 +143,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { 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) @@ -145,6 +163,11 @@ class SystemSearchActivity : DuckDuckGoActivity() { } } + private fun showOmnibar() { + results.scrollTo(0, 0) + appBarLayout.setExpanded(true) + } + private fun launchedFromAppBar(intent: Intent): Boolean { return intent.action == NEW_SEARCH_ACTION } 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 5f4b6110bdd9..8ddaae6a37e9 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.app.autocomplete.api.AutoCompleteApi +import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteResult import com.duckduckgo.app.global.SingleLiveEvent import com.jakewharton.rxrelay2.PublishRelay import io.reactivex.android.schedulers.AndroidSchedulers @@ -38,7 +39,7 @@ class SystemSearchViewModel( data class SystemSearchViewState( val queryText: String = "", - val autocompleteResults: AutoCompleteApi.AutoCompleteResult = AutoCompleteApi.AutoCompleteResult("", emptyList()), + val autocompleteResults: AutoCompleteResult = AutoCompleteResult("", emptyList()), val appResults: List = emptyList() ) @@ -52,7 +53,7 @@ class SystemSearchViewModel( private val autoCompletePublishSubject = PublishRelay.create() private var appResults: List = emptyList() - private var autocompleteResults: AutoCompleteApi.AutoCompleteResult = AutoCompleteApi.AutoCompleteResult("", emptyList()) + private var autocompleteResults: AutoCompleteResult = AutoCompleteResult("", emptyList()) init { resetState() @@ -63,7 +64,7 @@ class SystemSearchViewModel( fun resetState() { viewState.value = SystemSearchViewState() - autocompleteResults = AutoCompleteApi.AutoCompleteResult("", emptyList()) + autocompleteResults = AutoCompleteResult("", emptyList()) appResults = emptyList() } @@ -104,16 +105,23 @@ class SystemSearchViewModel( refreshViewStateResults() } - private fun updateAutocompleteResult(results: AutoCompleteApi.AutoCompleteResult) { + private fun updateAutocompleteResult(results: AutoCompleteResult) { autocompleteResults = results refreshViewStateResults() } private fun refreshViewStateResults() { val hasMultiResults = autocompleteResults.suggestions.isNotEmpty() && appResults.isNotEmpty() - val newSuggestions = if (hasMultiResults) autocompleteResults.suggestions.take(MULTI_RESULTS_MAX_PER_GROUP) else autocompleteResults.suggestions - val newApps = if (hasMultiResults) appResults.take(MULTI_RESULTS_MAX_PER_GROUP) else appResults - viewState.postValue(currentViewState().copy(autocompleteResults = AutoCompleteApi.AutoCompleteResult(autocompleteResults.query, newSuggestions), appResults = newApps)) + 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 userClearedQuery() { @@ -130,7 +138,7 @@ class SystemSearchViewModel( } companion object { - private const val DEBOUNCE_TIME_MS = 300L - private const val MULTI_RESULTS_MAX_PER_GROUP = 4 + private const val DEBOUNCE_TIME_MS = 200L + private const val RESULTS_MAX_RESULTS_PER_GROUP = 4 } } diff --git a/app/src/main/res/layout/activity_system_search.xml b/app/src/main/res/layout/activity_system_search.xml index 1665ab29109c..9441a17c5b5c 100644 --- a/app/src/main/res/layout/activity_system_search.xml +++ b/app/src/main/res/layout/activity_system_search.xml @@ -19,111 +19,99 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/rootView" android:layout_width="match_parent" - android:layout_height="match_parent" - app:layout_scrollFlags="scroll|enterAlways"> + android:layout_height="match_parent"> + android:theme="@style/AppTheme.Dark.AppBarOverlay"> - - - - - + + + + + + - - - - - - - - - - - - - - - - + android:background="@android:color/transparent" + android:fontFamily="sans-serif-medium" + android:hint="@string/omnibarInputHint" + android:imeOptions="flagNoExtractUi|actionGo|flagNoPersonalizedLearning" + android:inputType="textUri|textNoSuggestions" + android:maxLines="1" + android:selectAllOnFocus="true" + android:textColor="?attr/omnibarTextColor" + android:textColorHint="?attr/omnibarHintColor" + android:textCursorDrawable="@drawable/text_cursor" + android:textSize="16sp" + android:textStyle="normal" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/clearTextButton" + app:layout_constraintStart_toEndOf="@id/logo" + app:layout_constraintTop_toTopOf="parent" + tools:text="https://duckduckgo.com/?q=areallylongexampleexample"> + + + + + + + + + + Date: Thu, 20 Feb 2020 23:23:06 +0000 Subject: [PATCH 15/25] Improve app lookup speed by reducing package manager lookups --- .../app/systemsearch/DeviceInstalledApps.kt | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt index 666df22e9cb2..445e4a645bf4 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt @@ -17,9 +17,11 @@ package com.duckduckgo.app.systemsearch import android.content.Intent -import android.content.pm.ApplicationInfo.DisplayNameComparator import android.content.pm.PackageManager +import android.content.pm.PackageManager.GET_META_DATA import androidx.annotation.WorkerThread +import com.duckduckgo.app.global.performance.PerformanceConstants +import timber.log.Timber data class DeviceApp( val shortName: String, @@ -45,21 +47,22 @@ class DeviceAppsLookup(private val packageManager: PackageManager) { @WorkerThread private fun all(): List { - val mainIntent = Intent(Intent.ACTION_MAIN) - val mainActivities = packageManager.queryIntentActivities(mainIntent, 0) + val startTime = System.nanoTime() + val appsInfo = packageManager.getInstalledApplications(GET_META_DATA) + var sortTime = System.nanoTime() - startTime + Timber.d("Get took ${(sortTime / PerformanceConstants.NANO_TO_MILLIS_DIVISOR)}ms") - val appsInfo = mainActivities.map { - it.activityInfo.packageName - }.toSet().map { - packageManager.getApplicationInfo(it, PackageManager.GET_META_DATA) - }.sortedWith(DisplayNameComparator(packageManager)) - - return appsInfo.map { - val shortName = packageManager.getApplicationLabel(it).toString() + val results = appsInfo.map { val packageName = it.packageName - val fullName = packageManager.getApplicationInfo(packageName, 0).className ?: return@map null + val fullName = it.className ?: return@map null val launchIntent = packageManager.getLaunchIntentForPackage(packageName) ?: return@map null + val shortName = it.loadLabel(packageManager).toString() return@map DeviceApp(shortName, fullName, packageName, launchIntent) }.filterNotNull() + + var listTime = System.nanoTime() - startTime - sortTime + Timber.d("final list took ${(listTime / PerformanceConstants.NANO_TO_MILLIS_DIVISOR)}ms") + Timber.d("total ${((System.nanoTime() - startTime) / PerformanceConstants.NANO_TO_MILLIS_DIVISOR)}ms") + return results } } \ No newline at end of file From bda60f638f3c5e4652bf3d89762f12d9d6cabaec Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Thu, 20 Feb 2020 23:25:29 +0000 Subject: [PATCH 16/25] Remove additional dev logging --- .../duckduckgo/app/systemsearch/DeviceInstalledApps.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt index 445e4a645bf4..1e89e7f0528e 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt @@ -47,12 +47,9 @@ class DeviceAppsLookup(private val packageManager: PackageManager) { @WorkerThread private fun all(): List { - val startTime = System.nanoTime() val appsInfo = packageManager.getInstalledApplications(GET_META_DATA) - var sortTime = System.nanoTime() - startTime - Timber.d("Get took ${(sortTime / PerformanceConstants.NANO_TO_MILLIS_DIVISOR)}ms") - val results = appsInfo.map { + return appsInfo.map { val packageName = it.packageName val fullName = it.className ?: return@map null val launchIntent = packageManager.getLaunchIntentForPackage(packageName) ?: return@map null @@ -60,9 +57,5 @@ class DeviceAppsLookup(private val packageManager: PackageManager) { return@map DeviceApp(shortName, fullName, packageName, launchIntent) }.filterNotNull() - var listTime = System.nanoTime() - startTime - sortTime - Timber.d("final list took ${(listTime / PerformanceConstants.NANO_TO_MILLIS_DIVISOR)}ms") - Timber.d("total ${((System.nanoTime() - startTime) / PerformanceConstants.NANO_TO_MILLIS_DIVISOR)}ms") - return results } } \ No newline at end of file From b0ddd903ec72eb6fcec0cead8a3ae6f9966e99c9 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Fri, 21 Feb 2020 01:38:55 +0000 Subject: [PATCH 17/25] Tweak scrolling further --- .../com/duckduckgo/app/systemsearch/SystemSearchActivity.kt | 6 ++++-- app/src/main/res/layout/activity_system_search.xml | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) 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 5fda399d6eb6..bb42001d783d 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -114,11 +114,12 @@ class SystemSearchActivity : DuckDuckGoActivity() { private fun configureOmnibar() { results.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - val scrollable = results.maxScrollAmount > 0 + val scrollable = results.maxScrollAmount > MINIMUM_SCROLL_HEIGHT if (scrollable) { omnibardScrolling.enableOmnibarScrolling(toolbar) } else { - appBarLayout.setExpanded(true, true) + Timber.d("SCROLLABLE: false") + showOmnibar() omnibardScrolling.disableOmnibarScrolling(toolbar) } } @@ -184,6 +185,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { 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 fun intent(context: Context, widgetSearch: Boolean = false): Intent { val intent = Intent(context, SystemSearchActivity::class.java) diff --git a/app/src/main/res/layout/activity_system_search.xml b/app/src/main/res/layout/activity_system_search.xml index 9441a17c5b5c..8db71790ea0b 100644 --- a/app/src/main/res/layout/activity_system_search.xml +++ b/app/src/main/res/layout/activity_system_search.xml @@ -111,6 +111,7 @@ android:layout_height="wrap_content" android:clipToPadding="false" android:elevation="4dp" + android:paddingBottom="4dp" android:id="@+id/results" app:layout_behavior="@string/appbar_scrolling_view_behavior"> From b235b09d26b28bed4bb6aeb02b630516a3a57e86 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Fri, 21 Feb 2020 08:18:50 +0000 Subject: [PATCH 18/25] Switch to word prefix matching --- .../com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt index 1e89e7f0528e..8c8dc84bd448 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt @@ -20,8 +20,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageManager.GET_META_DATA import androidx.annotation.WorkerThread -import com.duckduckgo.app.global.performance.PerformanceConstants -import timber.log.Timber +import kotlin.text.RegexOption.IGNORE_CASE data class DeviceApp( val shortName: String, @@ -36,11 +35,11 @@ class DeviceAppsLookup(private val packageManager: PackageManager) { @WorkerThread fun query(query: String): List { - if (query.isBlank()) return emptyList() + val regex = ".*\\b${query}.*".toRegex(IGNORE_CASE) return allApps.filter { - it.shortName.contains(query, ignoreCase = true) + it.shortName.matches(regex) } } From 61fe705bab217f88187795a9ac8a8db860c70d41 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Fri, 21 Feb 2020 09:11:57 +0000 Subject: [PATCH 19/25] Small tidy ups, remove dev logging, add explanatory comment and cancel apps coroutine when new query arrives --- .../app/systemsearch/SystemSearchActivity.kt | 5 +---- .../app/systemsearch/SystemSearchViewModel.kt | 14 +++++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) 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 bb42001d783d..bd3f2ea2405a 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -37,7 +37,6 @@ import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.LaunchAppli import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.LaunchBrowser import com.duckduckgo.app.systemsearch.SystemSearchViewModel.SystemSearchViewState import kotlinx.android.synthetic.main.activity_system_search.* -import timber.log.Timber import javax.inject.Inject class SystemSearchActivity : DuckDuckGoActivity() { @@ -71,7 +70,6 @@ class SystemSearchActivity : DuckDuckGoActivity() { override fun onNewIntent(newIntent: Intent?) { super.onNewIntent(newIntent) - Timber.i("onNewIntent: $newIntent") viewModel.resetState() val intent = newIntent ?: return @@ -118,7 +116,6 @@ class SystemSearchActivity : DuckDuckGoActivity() { if (scrollable) { omnibardScrolling.enableOmnibarScrolling(toolbar) } else { - Timber.d("SCROLLABLE: false") showOmnibar() omnibardScrolling.disableOmnibarScrolling(toolbar) } @@ -185,7 +182,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { 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 + 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) 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 8ddaae6a37e9..498db3d095eb 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.jakewharton.rxrelay2.PublishRelay import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -52,9 +53,11 @@ class SystemSearchViewModel( val command: SingleLiveEvent = SingleLiveEvent() private val autoCompletePublishSubject = PublishRelay.create() - private var appResults: List = emptyList() private var autocompleteResults: AutoCompleteResult = AutoCompleteResult("", emptyList()) + private var appsJob: Job? = null + private var appResults: List = emptyList() + init { resetState() configureAutoComplete() @@ -63,9 +66,10 @@ class SystemSearchViewModel( private fun currentViewState(): SystemSearchViewState = viewState.value!! fun resetState() { - viewState.value = SystemSearchViewState() autocompleteResults = AutoCompleteResult("", emptyList()) + appsJob?.cancel() appResults = emptyList() + viewState.value = SystemSearchViewState() } private fun configureAutoComplete() { @@ -80,6 +84,9 @@ class SystemSearchViewModel( } fun userUpdatedQuery(query: String) { + + appsJob?.cancel() + val trimmedQuery = query.trim() if (trimmedQuery == currentViewState().queryText) { @@ -93,7 +100,8 @@ class SystemSearchViewModel( viewState.value = currentViewState().copy(queryText = trimmedQuery) autoCompletePublishSubject.accept(trimmedQuery) - viewModelScope.launch { + + appsJob = viewModelScope.launch { withContext(Dispatchers.IO) { updateAppResults(deviceAppsLookup.query(query)) } From fa43e942cda267b35ee1439a83c112efc84a2700 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Fri, 21 Feb 2020 09:27:46 +0000 Subject: [PATCH 20/25] Make dax launch DuckDuckGo --- .../app/systemsearch/SystemSearchActivity.kt | 17 +++++++++++++---- .../app/systemsearch/SystemSearchViewModel.kt | 9 +++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) 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 bd3f2ea2405a..3c591380dd78 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -33,8 +33,7 @@ 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.systemsearch.SystemSearchViewModel.Command.LaunchApplication -import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.LaunchBrowser +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 @@ -64,6 +63,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { configureObservers() configureAutoComplete() configureDeviceAppSuggestions() + configureDaxButton() configureOmnibar() configureTextInput() } @@ -110,6 +110,12 @@ class SystemSearchActivity : DuckDuckGoActivity() { deviceAppSuggestions.adapter = deviceAppSuggestionsAdapter } + private fun configureDaxButton() { + logo.setOnClickListener { + viewModel.userTappedDax() + } + } + private fun configureOmnibar() { results.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> val scrollable = results.maxScrollAmount > MINIMUM_SCROLL_HEIGHT @@ -149,12 +155,15 @@ class SystemSearchActivity : DuckDuckGoActivity() { private fun processCommand(command: SystemSearchViewModel.Command) { when (command) { + is LaunchDuckDuckGo -> { + startActivity(BrowserActivity.intent(this)) + finish() + } is LaunchBrowser -> { startActivity(BrowserActivity.intent(this, command.query)) finish() - } - is LaunchApplication -> { + is LaunchDeviceApplication -> { startActivity(command.intent) 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 498db3d095eb..4a50de5effa4 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -45,8 +45,9 @@ class SystemSearchViewModel( ) sealed class Command { + class LaunchDuckDuckGo : Command() data class LaunchBrowser(val query: String) : Command() - data class LaunchApplication(val intent: Intent) : Command() + data class LaunchDeviceApplication(val intent: Intent) : Command() } val viewState: MutableLiveData = MutableLiveData() @@ -132,6 +133,10 @@ class SystemSearchViewModel( ) } + fun userTappedDax() { + command.value = Command.LaunchDuckDuckGo() + } + fun userClearedQuery() { autoCompletePublishSubject.accept("") resetState() @@ -142,7 +147,7 @@ class SystemSearchViewModel( } fun userSelectedApp(app: DeviceApp) { - command.value = Command.LaunchApplication(app.launchIntent) + command.value = Command.LaunchDeviceApplication(app.launchIntent) } companion object { From 95284bb2828d58416ba9b4f6dae6a6b5bea3b276 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Fri, 21 Feb 2020 13:25:15 +0000 Subject: [PATCH 21/25] Add pixels --- .../duckduckgo/app/statistics/pixels/Pixel.kt | 9 ++++++--- .../app/systemsearch/SystemSearchActivity.kt | 19 +++++++++++++------ .../app/systemsearch/SystemSearchViewModel.kt | 4 ++++ .../main/res/values/string-untranslated.xml | 2 +- 4 files changed, 24 insertions(+), 10 deletions(-) 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 77b82e45eb32..2ecf9b860158 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 @@ -93,9 +93,12 @@ interface Pixel { WIDGETS_ADDED(pixelName = "m_w_a"), WIDGETS_DELETED(pixelName = "m_w_d"), - WIDGET_LAUNCH(pixelName = "m_w_l"), - ASSIST_LAUNCH(pixelName = "m_a_l"), - GOOGLE_BAR_LAUNCH(pixelName = "m_gb_l"), + APP_WIDGET_LAUNCH(pixelName = "m_w_l"), + APP_ASSIST_LAUNCH(pixelName = "m_a_l"), + APP_GOOGLE_BAR_LAUNCH(pixelName = "m_g_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/SystemSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt index 3c591380dd78..636b57fb460a 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -33,6 +33,7 @@ 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.* @@ -66,17 +67,20 @@ class SystemSearchActivity : DuckDuckGoActivity() { configureDaxButton() configureOmnibar() configureTextInput() + intent?.let { sendLaunchPixels(it) } } override fun onNewIntent(newIntent: Intent?) { super.onNewIntent(newIntent) - viewModel.resetState() - val intent = newIntent ?: return + newIntent?.let { sendLaunchPixels(it) } + } + + private fun sendLaunchPixels(intent: Intent) { when { - launchedFromAssist(intent) -> pixel.fire(Pixel.PixelName.ASSIST_LAUNCH) - launchedFromWidget(intent) -> pixel.fire(Pixel.PixelName.WIDGET_LAUNCH) - launchedFromAppBar(intent) -> pixel.fire(Pixel.PixelName.GOOGLE_BAR_LAUNCH) + launchedFromAssist(intent) -> pixel.fire(PixelName.APP_ASSIST_LAUNCH) + launchedFromWidget(intent) -> pixel.fire(PixelName.APP_WIDGET_LAUNCH) + launchedFromAppBar(intent) -> pixel.fire(PixelName.APP_GOOGLE_BAR_LAUNCH) } } @@ -93,7 +97,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { autocompleteSuggestions.layoutManager = LinearLayoutManager(this) autocompleteSuggestionsAdapter = BrowserAutoCompleteSuggestionsAdapter( immediateSearchClickListener = { - viewModel.userSubmittedQuery(it.phrase) + viewModel.userSubmittedAutocompleteResult(it.phrase) }, editableSearchClickListener = { viewModel.userUpdatedQuery(it.phrase) @@ -156,14 +160,17 @@ class SystemSearchActivity : DuckDuckGoActivity() { private fun processCommand(command: SystemSearchViewModel.Command) { when (command) { is LaunchDuckDuckGo -> { + pixel.fire(PixelName.INTERSTITIAL_LAUNCH_DAX) startActivity(BrowserActivity.intent(this)) finish() } is LaunchBrowser -> { + pixel.fire(PixelName.INTERSTITIAL_LAUNCH_BROWSER_QUERY) startActivity(BrowserActivity.intent(this, command.query)) finish() } is LaunchDeviceApplication -> { + pixel.fire(PixelName.INTERSTITIAL_LAUNCH_DEVICE_APP) startActivity(command.intent) 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 4a50de5effa4..39e2ad598136 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -146,6 +146,10 @@ class SystemSearchViewModel( command.value = Command.LaunchBrowser(query) } + fun userSubmittedAutocompleteResult(query: String) { + command.value = Command.LaunchBrowser(query) + } + fun userSelectedApp(app: DeviceApp) { command.value = Command.LaunchDeviceApplication(app.launchIntent) } diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 6e280ddb0f7a..8742cad9938d 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -20,7 +20,7 @@ From this device - + "Welcome to DuckDuckGo!"
Not to worry! Searching and browsing privately is easier than you think.]]>
From 005e1888d87fca9864b84c597f9267ea3f564b9c Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 24 Feb 2020 13:09:19 +0000 Subject: [PATCH 22/25] Unit tests --- .../autocomplete/api/AutoCompleteApiTest.kt | 5 +- .../app/browser/BrowserTabViewModelTest.kt | 13 +- .../app/systemsearch/DeviceAppLookupTest.kt | 120 ++++++++++++++ .../systemsearch/SystemSearchViewModelTest.kt | 153 ++++++++++++++++++ .../{AutoCompleteApi.kt => AutoComplete.kt} | 41 ++--- .../app/browser/BrowserTabFragment.kt | 10 +- .../app/browser/BrowserTabViewModel.kt | 21 +-- .../BrowserAutoCompleteSuggestionsAdapter.kt | 4 +- .../SuggestionViewHolderFactory.kt | 10 +- .../app/di/AppConfigurationDownloadModule.kt | 1 - .../app/di/SystemComponentsModule.kt | 11 +- .../duckduckgo/app/global/ViewModelFactory.kt | 8 +- ...iceInstalledApps.kt => DeviceAppLookup.kt} | 28 ++-- .../app/systemsearch/SystemSearchActivity.kt | 6 +- .../app/systemsearch/SystemSearchViewModel.kt | 25 ++- 15 files changed, 374 insertions(+), 82 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/systemsearch/DeviceAppLookupTest.kt create mode 100644 app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt rename app/src/main/java/com/duckduckgo/app/autocomplete/api/{AutoCompleteApi.kt => AutoComplete.kt} (85%) rename app/src/main/java/com/duckduckgo/app/systemsearch/{DeviceInstalledApps.kt => DeviceAppLookup.kt} (70%) 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 d74767c364ab..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, @@ -1264,7 +1265,7 @@ class BrowserTabViewModelTest { fun whenBookmarkSuggestionSubmittedThenAutoCompleteBookmarkSelectionPixelSent() = runBlocking { whenever(mockBookmarksDao.hasBookmarks()).thenReturn(true) val suggestion = AutoCompleteBookmarkSuggestion("example", "Example", "https://example.com") - testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteApi.AutoCompleteResult("", listOf(suggestion))) + 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)) } @@ -1273,7 +1274,7 @@ class BrowserTabViewModelTest { fun whenSearchSuggestionSubmittedWithBookmarksThenAutoCompleteSearchSelectionPixelSent() = runBlocking { whenever(mockBookmarksDao.hasBookmarks()).thenReturn(true) val suggestions = listOf(AutoCompleteSearchSuggestion("", false), AutoCompleteBookmarkSuggestion("", "", "")) - testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteApi.AutoCompleteResult("", suggestions)) + 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)) @@ -1282,7 +1283,7 @@ class BrowserTabViewModelTest { @Test fun whenSearchSuggestionSubmittedWithoutBookmarksThenAutoCompleteSearchSelectionPixelSent() = runBlocking { whenever(mockBookmarksDao.hasBookmarks()).thenReturn(false) - testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteApi.AutoCompleteResult("", emptyList())) + 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/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..b2251d750f9f --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -0,0 +1,153 @@ +/* + * 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.launchIntent), commandCaptor.lastValue) + } + + @Test + fun whenUserTapsDaxThenAppLaunched() { + testee.userTappedDax() + verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + assertTrue(commandCaptor.lastValue is LaunchDuckDuckGo) + } + + 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/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApi.kt b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt similarity index 85% rename from app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApi.kt rename to app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt index 58552c96af93..1103bd8e8854 100644 --- a/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoCompleteApi.kt +++ b/app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt @@ -16,20 +16,38 @@ package com.duckduckgo.app.autocomplete.api -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.AutoCompleteResult +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion +import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.global.UriString import io.reactivex.Observable import io.reactivex.functions.BiFunction import javax.inject.Inject -open class AutoCompleteApi @Inject constructor( +interface AutoComplete { + fun autoComplete(query: String): Observable + + 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())) @@ -65,17 +83,4 @@ open class AutoCompleteApi @Inject constructor( .toList() .onErrorReturn { emptyList() } .toObservable() - - 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) - } } \ No newline at end of file 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 5c771219d547..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.* 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 5b7a2c22e13c..6251e4e174a4 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 @@ -89,7 +92,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, @@ -267,7 +270,7 @@ class BrowserTabViewModel( private fun configureAutoComplete() { autoCompletePublishSubject .debounce(300, TimeUnit.MILLISECONDS) - .switchMap { autoCompleteApi.autoComplete(it) } + .switchMap { autoComplete.autoComplete(it) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ result -> 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 02841379c080..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,8 @@ 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.AutoCompleteApi.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion +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 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/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/SystemComponentsModule.kt b/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt index 9770840aba1d..ca85e4c0ecbb 100644 --- a/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt @@ -18,7 +18,10 @@ package com.duckduckgo.app.di import android.content.Context import android.content.pm.PackageManager -import com.duckduckgo.app.systemsearch.DeviceAppsLookup +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 @@ -30,7 +33,11 @@ open class SystemComponentsModule { @Provides fun packageManager(context: Context) = context.packageManager + @Singleton + @Provides + fun deviceAppsListProvider(packageManager: PackageManager): DeviceAppListProvider = InstalledDeviceAppListProvider(packageManager) + @Provides @Singleton - fun deviceAppsLookup(packageManager: PackageManager): DeviceAppsLookup = DeviceAppsLookup(packageManager) + 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 ce440014560c..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,7 +63,7 @@ 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.DeviceAppsLookup +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 @@ -87,7 +87,7 @@ class ViewModelFactory @Inject constructor( private val bookmarksDao: BookmarksDao, private val surveyDao: SurveyDao, private val autoCompleteApi: AutoCompleteApi, - private val deviceAppsLookup: DeviceAppsLookup, + private val deviceAppLookup: DeviceAppLookup, private val appSettingsPreferencesStore: SettingsDataStore, private val webViewLongPressHandler: LongPressHandler, private val defaultBrowserDetector: DefaultBrowserDetector, @@ -113,7 +113,7 @@ class ViewModelFactory @Inject constructor( with(modelClass) { when { isAssignableFrom(LaunchViewModel::class.java) -> LaunchViewModel(onboardingStore, appInstallationReferrerStateListener) - isAssignableFrom(SystemSearchViewModel::class.java) -> SystemSearchViewModel(autoCompleteApi, deviceAppsLookup) + isAssignableFrom(SystemSearchViewModel::class.java) -> SystemSearchViewModel(autoCompleteApi, deviceAppLookup) isAssignableFrom(OnboardingViewModel::class.java) -> onboardingViewModel() isAssignableFrom(BrowserViewModel::class.java) -> browserViewModel() isAssignableFrom(BrowserTabViewModel::class.java) -> browserTabViewModel() @@ -178,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/systemsearch/DeviceInstalledApps.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt similarity index 70% rename from app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt rename to app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt index 8c8dc84bd448..66328f6208a6 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceInstalledApps.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt @@ -24,37 +24,47 @@ import kotlin.text.RegexOption.IGNORE_CASE data class DeviceApp( val shortName: String, - val longName: String, val packageName: String, val launchIntent: Intent ) -class DeviceAppsLookup(private val packageManager: PackageManager) { +interface DeviceAppLookup { + @WorkerThread + fun query(query: String): List +} + +class InstalledDeviceAppLookup(private val appListProvider: DeviceAppListProvider) : DeviceAppLookup { - private val allApps by lazy { all() } + private val apps by lazy { appListProvider.get() } @WorkerThread - fun query(query: String): List { + override fun query(query: String): List { if (query.isBlank()) return emptyList() val regex = ".*\\b${query}.*".toRegex(IGNORE_CASE) - return allApps.filter { + return apps.filter { it.shortName.matches(regex) } } +} + +interface DeviceAppListProvider { + @WorkerThread + fun get(): List +} + +class InstalledDeviceAppListProvider(private val packageManager: PackageManager) : DeviceAppListProvider { @WorkerThread - private fun all(): List { + override fun get(): List { val appsInfo = packageManager.getInstalledApplications(GET_META_DATA) return appsInfo.map { val packageName = it.packageName - val fullName = it.className ?: return@map null val launchIntent = packageManager.getLaunchIntentForPackage(packageName) ?: return@map null val shortName = it.loadLabel(packageManager).toString() - return@map DeviceApp(shortName, fullName, packageName, launchIntent) + return@map DeviceApp(shortName, packageName, launchIntent) }.filterNotNull() - } } \ 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 index 636b57fb460a..d8b735a64885 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -45,7 +45,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { lateinit var pixel: Pixel @Inject - lateinit var omnibardScrolling: OmnibarScrolling + lateinit var omnibarScrolling: OmnibarScrolling private val viewModel: SystemSearchViewModel by bindViewModel() private lateinit var autocompleteSuggestionsAdapter: BrowserAutoCompleteSuggestionsAdapter @@ -124,10 +124,10 @@ class SystemSearchActivity : DuckDuckGoActivity() { results.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> val scrollable = results.maxScrollAmount > MINIMUM_SCROLL_HEIGHT if (scrollable) { - omnibardScrolling.enableOmnibarScrolling(toolbar) + omnibarScrolling.enableOmnibarScrolling(toolbar) } else { showOmnibar() - omnibardScrolling.disableOmnibarScrolling(toolbar) + omnibarScrolling.disableOmnibarScrolling(toolbar) } } } 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 39e2ad598136..6a718ee13058 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -20,22 +20,23 @@ import android.content.Intent import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi -import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteResult +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.schedulers.Schedulers -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import java.util.concurrent.TimeUnit class SystemSearchViewModel( - private val autoCompleteApi: AutoCompleteApi, - private val deviceAppsLookup: DeviceAppsLookup + private val autoComplete: AutoComplete, + private val deviceAppLookup: DeviceAppLookup, + private val dispatchers: DispatcherProvider = DefaultDispatcherProvider() ) : ViewModel() { data class SystemSearchViewState( @@ -45,7 +46,7 @@ class SystemSearchViewModel( ) sealed class Command { - class LaunchDuckDuckGo : Command() + object LaunchDuckDuckGo : Command() data class LaunchBrowser(val query: String) : Command() data class LaunchDeviceApplication(val intent: Intent) : Command() } @@ -76,7 +77,7 @@ class SystemSearchViewModel( private fun configureAutoComplete() { autoCompletePublishSubject .debounce(DEBOUNCE_TIME_MS, TimeUnit.MILLISECONDS) - .switchMap { autoCompleteApi.autoComplete(it) } + .switchMap { autoComplete.autoComplete(it) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ result -> @@ -102,10 +103,8 @@ class SystemSearchViewModel( viewState.value = currentViewState().copy(queryText = trimmedQuery) autoCompletePublishSubject.accept(trimmedQuery) - appsJob = viewModelScope.launch { - withContext(Dispatchers.IO) { - updateAppResults(deviceAppsLookup.query(query)) - } + appsJob = viewModelScope.launch(dispatchers.io()) { + updateAppResults(deviceAppLookup.query(query)) } } @@ -134,7 +133,7 @@ class SystemSearchViewModel( } fun userTappedDax() { - command.value = Command.LaunchDuckDuckGo() + command.value = Command.LaunchDuckDuckGo } fun userClearedQuery() { From 12451affd66ede3ca3d4e8bdc100cb35a4395df2 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Wed, 26 Feb 2020 13:47:17 +0000 Subject: [PATCH 23/25] Rename variables and methods for clarity --- .../main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt | 2 +- .../java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt | 4 ++-- .../com/duckduckgo/app/systemsearch/SystemSearchActivity.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) 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 2ecf9b860158..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 @@ -95,7 +95,7 @@ interface Pixel { APP_WIDGET_LAUNCH(pixelName = "m_w_l"), APP_ASSIST_LAUNCH(pixelName = "m_a_l"), - APP_GOOGLE_BAR_LAUNCH(pixelName = "m_g_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"), diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt index 66328f6208a6..b348a90a2977 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt @@ -40,10 +40,10 @@ class InstalledDeviceAppLookup(private val appListProvider: DeviceAppListProvide @WorkerThread override fun query(query: String): List { if (query.isBlank()) return emptyList() - val regex = ".*\\b${query}.*".toRegex(IGNORE_CASE) + val wordPrefixMatchingRegex = ".*\\b${query}.*".toRegex(IGNORE_CASE) return apps.filter { - it.shortName.matches(regex) + it.shortName.matches(wordPrefixMatchingRegex) } } } 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 d8b735a64885..d30bdd208c14 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -80,7 +80,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { when { launchedFromAssist(intent) -> pixel.fire(PixelName.APP_ASSIST_LAUNCH) launchedFromWidget(intent) -> pixel.fire(PixelName.APP_WIDGET_LAUNCH) - launchedFromAppBar(intent) -> pixel.fire(PixelName.APP_GOOGLE_BAR_LAUNCH) + launchedFromSystemSearchBox(intent) -> pixel.fire(PixelName.APP_SYSTEM_SEARCH_BOX_LAUNCH) } } @@ -182,7 +182,7 @@ class SystemSearchActivity : DuckDuckGoActivity() { appBarLayout.setExpanded(true) } - private fun launchedFromAppBar(intent: Intent): Boolean { + private fun launchedFromSystemSearchBox(intent: Intent): Boolean { return intent.action == NEW_SEARCH_ACTION } From c1343bd70b4ac99ca8b8fc81fd06bf34199b104d Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Wed, 26 Feb 2020 22:48:23 +0000 Subject: [PATCH 24/25] Store app icon to avoid duplicate lookups and show message when app not found --- .../systemsearch/SystemSearchViewModelTest.kt | 10 ++++- .../app/systemsearch/DeviceAppLookup.kt | 30 +++++++++++--- .../DeviceAppSuggestionsAdapter.kt | 2 +- .../app/systemsearch/SystemSearchActivity.kt | 40 ++++++++++++++----- .../app/systemsearch/SystemSearchViewModel.kt | 11 +++-- .../res/layout/activity_system_search.xml | 2 +- .../main/res/values/string-untranslated.xml | 3 +- 7 files changed, 77 insertions(+), 21 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt index b2251d750f9f..1aa7a0d4da24 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt @@ -127,7 +127,7 @@ class SystemSearchViewModelTest { fun whenUserSelectsAppResultThenAppLaunched() { testee.userSelectedApp(deviceApp) verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) - assertEquals(Command.LaunchDeviceApplication(deviceApp.launchIntent), commandCaptor.lastValue) + assertEquals(Command.LaunchDeviceApplication(deviceApp), commandCaptor.lastValue) } @Test @@ -137,6 +137,14 @@ class SystemSearchViewModelTest { 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) diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt index b348a90a2977..832eb0422d0a 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppLookup.kt @@ -19,33 +19,53 @@ 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 -) + 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 val apps by lazy { appListProvider.get() } + private var apps: List? = null @WorkerThread override fun query(query: String): List { if (query.isBlank()) return emptyList() - val wordPrefixMatchingRegex = ".*\\b${query}.*".toRegex(IGNORE_CASE) - return apps.filter { + 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 { diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt index 55f9f45874c1..45b1cabe484d 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/DeviceAppSuggestionsAdapter.kt @@ -43,7 +43,7 @@ class DeviceAppSuggestionsAdapter( holder.root.setOnClickListener { clickListener(app) } - val drawable = holder.icon.context.packageManager.getApplicationIcon(app.packageName) + val drawable = app.retrieveIcon(holder.icon.context.packageManager) holder.icon.setImageDrawable(drawable) } 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 d30bdd208c14..2c56d8c7025c 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchActivity.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.systemsearch +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.os.Bundle @@ -23,6 +24,8 @@ 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 @@ -160,20 +163,39 @@ class SystemSearchActivity : DuckDuckGoActivity() { private fun processCommand(command: SystemSearchViewModel.Command) { when (command) { is LaunchDuckDuckGo -> { - pixel.fire(PixelName.INTERSTITIAL_LAUNCH_DAX) - startActivity(BrowserActivity.intent(this)) - finish() + launchDuckDuckGo() } is LaunchBrowser -> { - pixel.fire(PixelName.INTERSTITIAL_LAUNCH_BROWSER_QUERY) - startActivity(BrowserActivity.intent(this, command.query)) - finish() + launchBrowser(command) } is LaunchDeviceApplication -> { - pixel.fire(PixelName.INTERSTITIAL_LAUNCH_DEVICE_APP) - startActivity(command.intent) - finish() + 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) } } 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 6a718ee13058..6ebaa17a121c 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -16,7 +16,6 @@ package com.duckduckgo.app.systemsearch -import android.content.Intent import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -48,7 +47,8 @@ class SystemSearchViewModel( sealed class Command { object LaunchDuckDuckGo : Command() data class LaunchBrowser(val query: String) : Command() - data class LaunchDeviceApplication(val intent: Intent) : Command() + data class LaunchDeviceApplication(val deviceApp: DeviceApp) : Command() + data class ShowAppNotFoundMessage(val appName: String) : Command() } val viewState: MutableLiveData = MutableLiveData() @@ -150,7 +150,12 @@ class SystemSearchViewModel( } fun userSelectedApp(app: DeviceApp) { - command.value = Command.LaunchDeviceApplication(app.launchIntent) + command.value = Command.LaunchDeviceApplication(app) + } + + fun appNotFound(app: DeviceApp) { + command.value = Command.ShowAppNotFoundMessage(app.shortName) + deviceAppLookup.refreshAppList() } companion object { diff --git a/app/src/main/res/layout/activity_system_search.xml b/app/src/main/res/layout/activity_system_search.xml index 8db71790ea0b..e198aa6f7f1a 100644 --- a/app/src/main/res/layout/activity_system_search.xml +++ b/app/src/main/res/layout/activity_system_search.xml @@ -140,7 +140,7 @@ android:paddingTop="6dp" android:paddingEnd="16dp" android:paddingBottom="2dp" - android:text="@string/system_search_app_label" + android:text="@string/systemSearchDeviceAppLabel" android:textColor="@color/grayish" android:textSize="13dp" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 8742cad9938d..0b2b65db23fa 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -19,7 +19,8 @@ "Reload" - From this device + From this device + Application could not be found "Welcome to DuckDuckGo!" From 43538815f171afca258a353b45ab93ef5828ded5 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Wed, 26 Feb 2020 23:35:25 +0000 Subject: [PATCH 25/25] Lint --- .../com/duckduckgo/app/browser/BrowserTabViewModel.kt | 8 ++++++-- .../com/duckduckgo/app/di/SystemComponentsModule.kt | 2 +- .../app/systemsearch/SystemSearchViewModel.kt | 10 +++++++++- 3 files changed, 16 insertions(+), 4 deletions(-) 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 6251e4e174a4..003c0e672cdd 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -77,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 @@ -219,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 @@ -268,7 +270,7 @@ class BrowserTabViewModel( @SuppressLint("CheckResult") private fun configureAutoComplete() { - autoCompletePublishSubject + autoCompleteDisposable = autoCompletePublishSubject .debounce(300, TimeUnit.MILLISECONDS) .switchMap { autoComplete.autoComplete(it) } .subscribeOn(Schedulers.io()) @@ -286,8 +288,10 @@ class BrowserTabViewModel( @VisibleForTesting public override fun onCleared() { - super.onCleared() buildingSiteFactoryJob?.cancel() + autoCompleteDisposable?.dispose() + autoCompleteDisposable = null + super.onCleared() } fun registerWebViewListener(browserWebViewClient: BrowserWebViewClient, browserChromeClient: BrowserChromeClient) { diff --git a/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt b/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt index ca85e4c0ecbb..57eb757ffd17 100644 --- a/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt @@ -31,7 +31,7 @@ open class SystemComponentsModule { @Singleton @Provides - fun packageManager(context: Context) = context.packageManager + fun packageManager(context: Context): PackageManager = context.packageManager @Singleton @Provides 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 6ebaa17a121c..3cc6b48b392b 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -26,6 +26,7 @@ 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 @@ -56,6 +57,7 @@ class SystemSearchViewModel( 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() @@ -75,7 +77,7 @@ class SystemSearchViewModel( } private fun configureAutoComplete() { - autoCompletePublishSubject + autoCompleteDisposable = autoCompletePublishSubject .debounce(DEBOUNCE_TIME_MS, TimeUnit.MILLISECONDS) .switchMap { autoComplete.autoComplete(it) } .subscribeOn(Schedulers.io()) @@ -158,6 +160,12 @@ class SystemSearchViewModel( 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