diff --git a/lawnchair/src/app/lawnchair/search/algorithms/LawnchairSearchAlgorithm.kt b/lawnchair/src/app/lawnchair/search/algorithms/LawnchairSearchAlgorithm.kt index 9ce375e561..a80245f2ce 100644 --- a/lawnchair/src/app/lawnchair/search/algorithms/LawnchairSearchAlgorithm.kt +++ b/lawnchair/src/app/lawnchair/search/algorithms/LawnchairSearchAlgorithm.kt @@ -25,9 +25,10 @@ import com.android.launcher3.BuildConfig import com.android.launcher3.Utilities import com.android.launcher3.allapps.BaseAllAppsAdapter import com.android.launcher3.search.SearchAlgorithm +import com.android.launcher3.search.SearchCallback import com.patrykmichalik.opto.core.firstBlocking -sealed class LawnchairSearchAlgorithm( +abstract class LawnchairSearchAlgorithm( protected val context: Context, ) : SearchAlgorithm { @@ -181,6 +182,11 @@ sealed class LawnchairSearchAlgorithm( else -> centerBackground } + open fun doZeroStateSearch(callback: SearchCallback) { + // Default implementation is to clear results. + callback.clearSearchResult() + } + companion object { const val APP_SEARCH = "appSearch" diff --git a/lawnchair/src/app/lawnchair/search/engine/NewSearchAlgorithmAdapter.kt b/lawnchair/src/app/lawnchair/search/engine/NewSearchAlgorithmAdapter.kt new file mode 100644 index 0000000000..30bacd92de --- /dev/null +++ b/lawnchair/src/app/lawnchair/search/engine/NewSearchAlgorithmAdapter.kt @@ -0,0 +1,179 @@ +package app.lawnchair.search.engine + +import app.lawnchair.search.engine.provider.AppSearchProvider +import app.lawnchair.search.engine.provider.ContactsSearchProvider +import app.lawnchair.search.engine.provider.FileSearchProvider +import app.lawnchair.search.engine.provider.HistorySearchProvider +import app.lawnchair.search.engine.provider.SettingsSearchProvider +import app.lawnchair.search.engine.provider.web.WebSuggestionProvider +import android.content.Context +import app.lawnchair.preferences2.PreferenceManager2 +import app.lawnchair.search.adapter.HEADER_JUSTIFY +import app.lawnchair.search.adapter.SPACE +import app.lawnchair.search.adapter.SearchTargetCompat +import app.lawnchair.search.adapter.SearchTargetFactory +import app.lawnchair.search.algorithms.LawnchairSearchAlgorithm +import app.lawnchair.search.algorithms.data.Calculation +import app.lawnchair.search.algorithms.data.ContactInfo +import app.lawnchair.search.algorithms.data.IFileInfo +import app.lawnchair.search.algorithms.data.RecentKeyword +import app.lawnchair.search.algorithms.data.SettingInfo +import app.lawnchair.search.engine.provider.CalculatorSearchProvider +import app.lawnchair.search.engine.provider.ShortcutSearchProvider +import com.android.launcher3.LauncherAppState +import com.android.launcher3.R +import com.android.launcher3.allapps.BaseAllAppsAdapter +import com.android.launcher3.search.SearchCallback +import com.patrykmichalik.opto.core.firstBlocking +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.firstOrNull + +class NewSearchAlgorithmAdapter(context: Context) : LawnchairSearchAlgorithm(context) { + + private val appState = LauncherAppState.getInstance(context) + + private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var currentJob: Job? = null + + private val appSearchProvider = AppSearchProvider + private val shortcutSearchProvider = ShortcutSearchProvider + private val historySearchProvider = HistorySearchProvider + + private val searchProviders: List = listOf( + SettingsSearchProvider, + FileSearchProvider, + ContactsSearchProvider, + WebSuggestionProvider + ) + + override fun doSearch(query: String, callback: SearchCallback) { + appState.model.enqueueModelUpdateTask { _, _, apps -> + val appResults = appSearchProvider.search(context, query, apps) + val shortcutResults = shortcutSearchProvider.search(context, appResults) + + currentJob?.cancel() + currentJob = coroutineScope.launch { + val nonAppProvidersFlow = combine( + searchProviders.map { it.search(context, query) } + ) { resultsArray -> + resultsArray.toList().flatten() + } + + nonAppProvidersFlow.collect { nonAppResults -> + val calcResult = CalculatorSearchProvider.search(context, query) + .firstOrNull() + + val allResults = appResults + shortcutResults + (calcResult ?: emptyList()) + nonAppResults + + val searchTargets = translateToSearchTargets(allResults, appResults.size) + val adapterItems = transformSearchResults(searchTargets) + withContext(Dispatchers.Main) { + callback.onSearchResult(query, ArrayList(adapterItems)) + } + } + } + } + } + + override fun doZeroStateSearch(callback: SearchCallback) { + currentJob?.cancel() + currentJob = coroutineScope.launch { + val prefs2 = PreferenceManager2.getInstance(context) + val maxHistory = prefs2.maxRecentResultCount.firstBlocking() + + val historyResults = historySearchProvider.getRecentKeywords(context, maxHistory) + + val searchTargets = translateToSearchTargets(historyResults, 0) + val adapterItems = transformSearchResults(searchTargets) + withContext(Dispatchers.Main) { + callback.onSearchResult("", ArrayList(adapterItems)) + } + } + } + + override fun cancel(interruptActiveRequests: Boolean) { + currentJob?.cancel() + } + + private fun translateToSearchTargets( + results: List, + appResultCount: Int, + ): List { + val factory = SearchTargetFactory(context) + val targets = mutableListOf() + + val apps = results.filterIsInstance() + val shortcuts = results.filterIsInstance() + val otherResults = results.filter { it !is SearchResult.App && it !is SearchResult.Shortcut } + + if (appResultCount == 1 && shortcuts.isNotEmpty()) { + val singleApp = apps.first() + targets.add(factory.createAppSearchTarget(singleApp.data, asRow = true)) + targets.addAll(shortcuts.map { factory.createShortcutTarget(it.data) }) + } else { + targets.addAll(apps.map { factory.createAppSearchTarget(it.data, asRow = false) }) + } + + if (targets.isNotEmpty() && otherResults.isNotEmpty()) { + targets.add(factory.createHeaderTarget(SPACE)) + } + + val webSuggestions = otherResults.filterIsInstance() + if (webSuggestions.isNotEmpty()) { + val suggestionsHeader = + factory.createHeaderTarget(context.getString(R.string.all_apps_search_result_suggestions)) + targets.add(suggestionsHeader) + targets.addAll( + webSuggestions.map { + factory.createWebSuggestionsTarget( + it.suggestion, + it.provider, + ) + }, + ) + } + + val calculator = otherResults.filterIsInstance() + if (calculator.isNotEmpty()) { + val calculatorHeader = + factory.createHeaderTarget(context.getString(R.string.all_apps_search_result_calculator)) + targets.add(calculatorHeader) + targets.add( + factory.createCalculatorTarget(calculator.first()), + ) + } + + val contacts = otherResults.filterIsInstance() + if (contacts.isNotEmpty()) { + val contactsHeader = + factory.createHeaderTarget(context.getString(R.string.all_apps_search_result_contacts_from_device)) + targets.add(contactsHeader) + targets.addAll(contacts.map { factory.createContactsTarget(it.data) }) + } + + val settings = otherResults.filterIsInstance() + if (settings.isNotEmpty()) { + val settingsHeader = + factory.createHeaderTarget(context.getString(R.string.all_apps_search_result_settings_entry_from_device)) + targets.add(settingsHeader) + targets.addAll(settings.mapNotNull { factory.createSettingsTarget(it.data) }) + } + + val files = otherResults.filterIsInstance() + if (files.isNotEmpty()) { + val filesHeader = + factory.createHeaderTarget(context.getString(R.string.all_apps_search_result_files)) + targets.add(filesHeader) + targets.addAll(files.map { factory.createFilesTarget(it.data) }) + } + + val history = otherResults.filterIsInstance() + if (history.isNotEmpty()) { + val historyHeader = factory.createHeaderTarget(context.getString(R.string.search_pref_result_history_title)) + targets.add(historyHeader) + } + + return targets + } +} diff --git a/lawnchair/src/app/lawnchair/search/engine/provider/AppSearchProvider.kt b/lawnchair/src/app/lawnchair/search/engine/provider/AppSearchProvider.kt index cc1c73d205..fd4135f0ca 100644 --- a/lawnchair/src/app/lawnchair/search/engine/provider/AppSearchProvider.kt +++ b/lawnchair/src/app/lawnchair/search/engine/provider/AppSearchProvider.kt @@ -18,7 +18,7 @@ import me.xdrop.fuzzywuzzy.algorithms.WeightedRatio object AppSearchProvider { - fun search(context: Context, query: String, allApps: AllAppsList): List { + fun search(context: Context, query: String, allApps: AllAppsList): List { val prefs = PreferenceManager2.getInstance(context) val hiddenApps = prefs.hiddenApps.firstBlocking() val hiddenAppsInSearch = prefs.hiddenAppsInSearch.firstBlocking() @@ -31,16 +31,7 @@ object AppSearchProvider { normalSearch(allApps.data, query, maxAppResults, hiddenApps, hiddenAppsInSearch) } - val finalResults = mutableListOf() - appResults.mapTo(finalResults) { SearchResult.App(data = it) } - - if (appResults.size == 1) { - val singleApp = appResults.first() - val shortcuts = getShortcuts(singleApp, context) - shortcuts.mapTo(finalResults) { SearchResult.Shortcut(data = it) } - } - - return finalResults + return appResults.map { SearchResult.App(data = it) } } private fun normalSearch(apps: List, query: String, maxResultsCount: Int, hiddenApps: Set, hiddenAppsInSearch: String): List { @@ -71,11 +62,4 @@ object AppSearchProvider { return matches.take(maxResultsCount) .map { it.referent } } - - private fun getShortcuts(app: AppInfo, context: Context): List { - val shortcuts = ShortcutRequest(context.launcher, app.user) - .withContainer(app.targetComponent) - .query(ShortcutRequest.PUBLISHED) - return PopupPopulator.sortAndFilterShortcuts(shortcuts) - } } diff --git a/lawnchair/src/app/lawnchair/search/engine/provider/ShortcutSearchProvider.kt b/lawnchair/src/app/lawnchair/search/engine/provider/ShortcutSearchProvider.kt new file mode 100644 index 0000000000..c8d4bee4ae --- /dev/null +++ b/lawnchair/src/app/lawnchair/search/engine/provider/ShortcutSearchProvider.kt @@ -0,0 +1,28 @@ +package app.lawnchair.search.engine.provider + +import android.content.Context +import android.content.pm.ShortcutInfo +import app.lawnchair.launcher +import app.lawnchair.search.engine.SearchResult +import app.lawnchair.util.isDefaultLauncher +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.popup.PopupPopulator +import com.android.launcher3.shortcuts.ShortcutRequest + +object ShortcutSearchProvider { + fun search(context: Context, appResults: List): List { + if (appResults.size == 1 && context.isDefaultLauncher()) { + val singleApp = appResults.first().data + val shortcuts = getShortcuts(singleApp, context) + return shortcuts.map { SearchResult.Shortcut(data = it) } + } + return emptyList() + } + + private fun getShortcuts(app: AppInfo, context: Context): List { + val shortcuts = ShortcutRequest(context.launcher, app.user) + .withContainer(app.targetComponent) + .query(ShortcutRequest.PUBLISHED) + return PopupPopulator.sortAndFilterShortcuts(shortcuts) + } +}