diff --git a/lawnchair/res/layout/search_result_empty_state.xml b/lawnchair/res/layout/search_result_empty_state.xml new file mode 100644 index 0000000000..cfa14be56b --- /dev/null +++ b/lawnchair/res/layout/search_result_empty_state.xml @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/lawnchair/res/layout/search_result_search_settings.xml b/lawnchair/res/layout/search_result_search_settings.xml new file mode 100644 index 0000000000..77d8917109 --- /dev/null +++ b/lawnchair/res/layout/search_result_search_settings.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml index 6ca35d67cb..6f5fe61575 100644 --- a/lawnchair/res/values/strings.xml +++ b/lawnchair/res/values/strings.xml @@ -725,7 +725,7 @@ --> - Search + Search apps, web, and more Search apps No apps found matching \"%1$s\" @@ -808,4 +808,8 @@ To search your files, grant storage permissions to Lawnchair Grant permissions Custom search engine name + + Start searching + Find apps, contacts, and more. Your recent searches will appear here. + Find apps, contacts, and more. diff --git a/lawnchair/src/app/lawnchair/allapps/AllAppsSearchInput.kt b/lawnchair/src/app/lawnchair/allapps/AllAppsSearchInput.kt index c2e81c1a79..9e42123ad3 100644 --- a/lawnchair/src/app/lawnchair/allapps/AllAppsSearchInput.kt +++ b/lawnchair/src/app/lawnchair/allapps/AllAppsSearchInput.kt @@ -138,6 +138,10 @@ class AllAppsSearchInput(context: Context, attrs: AttributeSet?) : with(input) { addTextChangedListener { + if (input.text.toString().isEmpty()) { + searchAlgorithm?.doZeroStateSearch(this@AllAppsSearchInput) + } + actionButton.isVisible = !it.isNullOrEmpty() micIcon.isVisible = shouldShowIcons && voiceIntent != null && it.isNullOrEmpty() lensIcon.isVisible = shouldShowIcons && supportsLens && lensIntent != null && it.isNullOrEmpty() diff --git a/lawnchair/src/app/lawnchair/allapps/views/SearchResultEmptyState.kt b/lawnchair/src/app/lawnchair/allapps/views/SearchResultEmptyState.kt new file mode 100644 index 0000000000..688394846a --- /dev/null +++ b/lawnchair/src/app/lawnchair/allapps/views/SearchResultEmptyState.kt @@ -0,0 +1,48 @@ +package app.lawnchair.allapps.views + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.ViewCompat +import app.lawnchair.search.adapter.SearchTargetCompat +import app.lawnchair.search.model.SearchResultActionCallBack +import app.lawnchair.theme.color.tokens.ColorTokens +import com.android.launcher3.R +import com.android.launcher3.util.Themes + +class SearchResultEmptyState(context: Context, attrs: AttributeSet?) : + LinearLayout(context, attrs), + SearchResultView { + + private lateinit var icon: ImageView + private lateinit var title: TextView + private lateinit var subtitle: TextView + + override fun onFinishInflate() { + super.onFinishInflate() + title = ViewCompat.requireViewById(this, R.id.empty_state_title) + subtitle = ViewCompat.requireViewById(this, R.id.empty_state_subtitle) + icon = ViewCompat.requireViewById(this, R.id.empty_state_icon) + + subtitle.setTextColor(Themes.getAttrColor(context, android.R.attr.textColorTertiary)) + icon.setColorFilter(ColorTokens.ColorAccent.resolveColor(context)) + } + + override val isQuickLaunch: Boolean = false + override fun launch(): Boolean = false + + override fun bind( + target: SearchTargetCompat, + shortcuts: List, + callBack: SearchResultActionCallBack?, + ) { + val extras = target.extras + val titleRes = extras.getInt("titleRes") + val subtitleRes = extras.getInt("subtitleRes") + + if (titleRes != 0) title.setText(titleRes) + if (subtitleRes != 0) subtitle.setText(subtitleRes) + } +} diff --git a/lawnchair/src/app/lawnchair/allapps/views/SearchResultSearchSettings.kt b/lawnchair/src/app/lawnchair/allapps/views/SearchResultSearchSettings.kt new file mode 100644 index 0000000000..28080d39bb --- /dev/null +++ b/lawnchair/src/app/lawnchair/allapps/views/SearchResultSearchSettings.kt @@ -0,0 +1,39 @@ +package app.lawnchair.allapps.views + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageButton +import android.widget.LinearLayout +import androidx.core.view.ViewCompat +import app.lawnchair.search.adapter.SearchTargetCompat +import app.lawnchair.search.model.SearchResultActionCallBack +import app.lawnchair.ui.preferences.PreferenceActivity +import app.lawnchair.ui.preferences.destinations.SearchRoute +import app.lawnchair.ui.preferences.navigation.Search +import com.android.launcher3.R + +class SearchResultSearchSettings(context: Context, attrs: AttributeSet?) : + LinearLayout(context, attrs), + SearchResultView { + + private lateinit var iconButton: ImageButton + + override fun onFinishInflate() { + super.onFinishInflate() + iconButton = ViewCompat.requireViewById(this, R.id.search_settings) + iconButton.setOnClickListener { + context.startActivity(PreferenceActivity.createIntent(context, Search(SearchRoute.DRAWER_SEARCH))) + } + } + + override val isQuickLaunch: Boolean = false + override fun launch(): Boolean = false + + override fun bind( + target: SearchTargetCompat, + shortcuts: List, + callBack: SearchResultActionCallBack?, + ) { + // no-op + } +} diff --git a/lawnchair/src/app/lawnchair/search/LawnchairSearchAdapterProvider.kt b/lawnchair/src/app/lawnchair/search/LawnchairSearchAdapterProvider.kt index 855e9e9874..128b134cc9 100644 --- a/lawnchair/src/app/lawnchair/search/LawnchairSearchAdapterProvider.kt +++ b/lawnchair/src/app/lawnchair/search/LawnchairSearchAdapterProvider.kt @@ -36,6 +36,8 @@ class LawnchairSearchAdapterProvider( append(SEARCH_RESULT_SETTINGS_TILE, R.layout.search_result_small_icon_row) append(SEARCH_RESULT_RECENT_TILE, R.layout.search_result_small_icon_row) append(SEARCH_RESULT_CALCULATOR, R.layout.search_result_tall_icon_row_calculator) + append(SEARCH_RESULT_EMPTY_STATE, R.layout.search_result_empty_state) + append(SEARCH_RESULT_SEARCH_SETTINGS, R.layout.search_result_search_settings) } private var quickLaunchItem: SearchResultView? = null set(value) { @@ -108,6 +110,8 @@ class LawnchairSearchAdapterProvider( private const val SEARCH_RESULT_SETTINGS_TILE = 1 shl 18 private const val SEARCH_RESULT_RECENT_TILE = 1 shl 19 private const val SEARCH_RESULT_CALCULATOR = 1 shl 20 + private const val SEARCH_RESULT_EMPTY_STATE = 1 shl 21 + private const val SEARCH_RESULT_SEARCH_SETTINGS = 1 shl 22 val viewTypeMap = mapOf( LayoutType.ICON_SINGLE_VERTICAL_TEXT to SEARCH_RESULT_ICON, @@ -121,6 +125,8 @@ class LawnchairSearchAdapterProvider( LayoutType.ICON_SLICE to SEARCH_RESULT_SETTINGS_TILE, LayoutType.WIDGET_LIVE to SEARCH_RESULT_RECENT_TILE, LayoutType.CALCULATOR to SEARCH_RESULT_CALCULATOR, + LayoutType.EMPTY_STATE to SEARCH_RESULT_EMPTY_STATE, + LayoutType.SEARCH_SETTINGS to SEARCH_RESULT_SEARCH_SETTINGS, ) } } diff --git a/lawnchair/src/app/lawnchair/search/adapter/SearchTargetCompat.kt b/lawnchair/src/app/lawnchair/search/adapter/SearchTargetCompat.kt index a7489193b9..4609dec880 100644 --- a/lawnchair/src/app/lawnchair/search/adapter/SearchTargetCompat.kt +++ b/lawnchair/src/app/lawnchair/search/adapter/SearchTargetCompat.kt @@ -275,6 +275,8 @@ data class SearchTargetCompat( RESULT_TYPE_FILE_TILE, RESULT_TYPE_SETTING_TILE, RESULT_TYPE_CALCULATOR, + RESULT_TYPE_EMPTY_RESULT, + RESULT_TYPE_SEARCH_SETTINGS, ], ) @Retention(AnnotationRetention.SOURCE) @@ -292,6 +294,8 @@ data class SearchTargetCompat( const val RESULT_TYPE_FILE_TILE = 1 shl 8 const val RESULT_TYPE_SETTING_TILE = 1 shl 9 const val RESULT_TYPE_CALCULATOR = 1 shl 10 + const val RESULT_TYPE_EMPTY_RESULT = 1 shl 11 + const val RESULT_TYPE_SEARCH_SETTINGS = 1 shl 12 fun wrap(target: SearchTarget): SearchTargetCompat = SearchTargetCompat(target) } diff --git a/lawnchair/src/app/lawnchair/search/adapter/SearchTargetFactory.kt b/lawnchair/src/app/lawnchair/search/adapter/SearchTargetFactory.kt index 8d6ad8fe93..1abe6de13d 100644 --- a/lawnchair/src/app/lawnchair/search/adapter/SearchTargetFactory.kt +++ b/lawnchair/src/app/lawnchair/search/adapter/SearchTargetFactory.kt @@ -13,6 +13,7 @@ import android.provider.ContactsContract import android.provider.MediaStore import android.util.Log import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.core.net.toUri import androidx.core.os.bundleOf import app.lawnchair.allapps.views.SearchResultView @@ -313,6 +314,53 @@ class SearchTargetFactory( ) } + fun createEmptyStateTarget( + @StringRes titleRes: Int, + @StringRes subtitleRes: Int, + ): SearchTargetCompat { + val id = "empty_state:$titleRes" + // The action doesn't do anything, it's just for display. + val action = SearchActionCompat.Builder(id, "") + .setIntent(Intent()) + .build() + + return SearchTargetCompat.Builder( + SearchTargetCompat.RESULT_TYPE_EMPTY_RESULT, + LayoutType.EMPTY_STATE, + id, + ).apply { + setPackageName(EMPTY_STATE) + setUserHandle(Process.myUserHandle()) + setSearchAction(action) + setExtras( + bundleOf( + "titleRes" to titleRes, + "subtitleRes" to subtitleRes, + ), + ) + }.build() + } + + fun createSearchSettingsTarget(): SearchTargetCompat { + val id = "action:search_settings" + + // The action doesn't do anything, it's just for display. + val action = SearchActionCompat.Builder(id, "") + .setIntent(Intent()) + .build() + + return SearchTargetCompat.Builder( + SearchTargetCompat.RESULT_TYPE_SEARCH_SETTINGS, + LayoutType.SEARCH_SETTINGS, + id, + ).apply { + setPackageName(SEARCH_SETTINGS) + setUserHandle(Process.myUserHandle()) + setSearchAction(action) + setExtras(Bundle()) + }.build() + } + companion object { private const val HASH_ALGORITHM = "SHA-256" @@ -423,3 +471,5 @@ const val SHORTCUT = "shortcut" const val HISTORY = "recent_keyword" const val HEADER_JUSTIFY = "header_justify" const val CALCULATOR = "calculator" +const val EMPTY_STATE = "empty_state" +const val SEARCH_SETTINGS = "search_settings" diff --git a/lawnchair/src/app/lawnchair/search/algorithms/LawnchairNewLocalSearchAlgorithm.kt b/lawnchair/src/app/lawnchair/search/algorithms/LawnchairNewLocalSearchAlgorithm.kt index 2a3365fea1..d0972c1e4d 100644 --- a/lawnchair/src/app/lawnchair/search/algorithms/LawnchairNewLocalSearchAlgorithm.kt +++ b/lawnchair/src/app/lawnchair/search/algorithms/LawnchairNewLocalSearchAlgorithm.kt @@ -10,10 +10,12 @@ import app.lawnchair.search.algorithms.engine.ActionsSectionBuilder import app.lawnchair.search.algorithms.engine.AppsAndShortcutsSectionBuilder import app.lawnchair.search.algorithms.engine.CalculationSectionBuilder import app.lawnchair.search.algorithms.engine.ContactsSectionBuilder +import app.lawnchair.search.algorithms.engine.EmptyStateSectionBuilder import app.lawnchair.search.algorithms.engine.FilesSectionBuilder import app.lawnchair.search.algorithms.engine.HistorySectionBuilder import app.lawnchair.search.algorithms.engine.SearchProvider import app.lawnchair.search.algorithms.engine.SearchResult +import app.lawnchair.search.algorithms.engine.SearchSettingsSectionBuilder import app.lawnchair.search.algorithms.engine.SectionBuilder import app.lawnchair.search.algorithms.engine.SettingsSectionBuilder import app.lawnchair.search.algorithms.engine.WebSuggestionsSectionBuilder @@ -26,7 +28,9 @@ import app.lawnchair.search.algorithms.engine.provider.SettingsSearchProvider import app.lawnchair.search.algorithms.engine.provider.ShortcutSearchProvider import app.lawnchair.search.algorithms.engine.provider.web.CustomWebSearchProvider import app.lawnchair.search.algorithms.engine.provider.web.WebSuggestionProvider +import com.android.internal.R.id.actions 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 @@ -89,12 +93,40 @@ class LawnchairNewLocalSearchAlgorithm(context: Context) : LawnchairSearchAlgori override fun doZeroStateSearch(callback: SearchCallback) { currentJob?.cancel() currentJob = coroutineScope.launch { + val prefs = PreferenceManager.getInstance(context) val prefs2 = PreferenceManager2.getInstance(context) + + val historyEnabled = prefs.searchResulRecentSuggestion.get() val maxHistory = prefs2.maxRecentResultCount.firstBlocking() - val historyResults = historySearchProvider.getRecentKeywords(context, maxHistory) + val historyResults = if (historyEnabled) { + historySearchProvider.getRecentKeywords(context, maxHistory) + } else { + emptyList() + } - val searchTargets = translateToSearchTargets(historyResults) + val resultsToTranslate = if (historyResults.isNotEmpty()) { + historyResults + listOf(SearchResult.Action.SearchSettings) + } else { + listOf( + if (historyEnabled) { + // State A: No History + SearchResult.Action.EmptyState( + titleRes = R.string.search_empty_state_title, + subtitleRes = R.string.search_empty_state_no_history_subtitle, + ) + } else { + // State B: History Disabled + SearchResult.Action.EmptyState( + titleRes = R.string.search_empty_state_title, + subtitleRes = R.string.search_empty_state_history_disabled_subtitle, + ) + }, + SearchResult.Action.SearchSettings, + ) + } + + val searchTargets = translateToSearchTargets(resultsToTranslate) val adapterItems = transformSearchResults(searchTargets) withContext(Dispatchers.Main) { callback.onSearchResult("", ArrayList(adapterItems)) @@ -111,8 +143,6 @@ class LawnchairNewLocalSearchAlgorithm(context: Context) : LawnchairSearchAlgori val prefs = PreferenceManager.getInstance(context) val prefs2 = PreferenceManager2.getInstance(context) - actions.add(SearchResult.Action.Divider()) - if (prefs.searchResultStartPageSuggestion.get()) { val provider = prefs2.webSuggestionProvider.firstBlocking() val webProvider = provider.configure(context) @@ -137,6 +167,9 @@ class LawnchairNewLocalSearchAlgorithm(context: Context) : LawnchairSearchAlgori actions.add(SearchResult.Action.MarketSearch(query = query)) } + actions.add(SearchResult.Action.SearchSettings) + actions.add(SearchResult.Action.SearchSettings) + return actions } @@ -149,6 +182,8 @@ class LawnchairNewLocalSearchAlgorithm(context: Context) : LawnchairSearchAlgori SettingsSectionBuilder, HistorySectionBuilder, ActionsSectionBuilder, + EmptyStateSectionBuilder, + SearchSettingsSectionBuilder, ) private fun translateToSearchTargets( diff --git a/lawnchair/src/app/lawnchair/search/algorithms/LawnchairSearchAlgorithm.kt b/lawnchair/src/app/lawnchair/search/algorithms/LawnchairSearchAlgorithm.kt index ced4184fd2..049a794c7c 100644 --- a/lawnchair/src/app/lawnchair/search/algorithms/LawnchairSearchAlgorithm.kt +++ b/lawnchair/src/app/lawnchair/search/algorithms/LawnchairSearchAlgorithm.kt @@ -12,11 +12,13 @@ import app.lawnchair.search.adapter.SearchTargetCompat.Companion.RESULT_TYPE_APP import app.lawnchair.search.adapter.SearchTargetCompat.Companion.RESULT_TYPE_SHORTCUT import com.android.app.search.LayoutType.CALCULATOR import com.android.app.search.LayoutType.EMPTY_DIVIDER +import com.android.app.search.LayoutType.EMPTY_STATE import com.android.app.search.LayoutType.HORIZONTAL_MEDIUM_TEXT import com.android.app.search.LayoutType.ICON_HORIZONTAL_TEXT import com.android.app.search.LayoutType.ICON_SINGLE_VERTICAL_TEXT import com.android.app.search.LayoutType.ICON_SLICE import com.android.app.search.LayoutType.PEOPLE_TILE +import com.android.app.search.LayoutType.SEARCH_SETTINGS import com.android.app.search.LayoutType.SMALL_ICON_HORIZONTAL_TEXT import com.android.app.search.LayoutType.TEXT_HEADER import com.android.app.search.LayoutType.THUMBNAIL @@ -32,6 +34,12 @@ sealed class LawnchairSearchAlgorithm( protected val context: Context, ) : SearchAlgorithm { + private val transparentBackground = SearchItemBackground( + context, + showBackground = false, + roundBottom = false, + roundTop = false, + ) private val iconBackground = SearchItemBackground( context, showBackground = false, @@ -169,6 +177,8 @@ sealed class LawnchairSearchAlgorithm( layoutType == ICON_SLICE -> getGroupedBackground(index, settingIndices) layoutType == WIDGET_LIVE -> getGroupedBackground(index, recentIndices) layoutType == CALCULATOR && calculator.isNotEmpty() -> normalBackground + layoutType == EMPTY_STATE -> transparentBackground + layoutType == SEARCH_SETTINGS -> transparentBackground isFirst && isLast -> normalBackground isFirst -> topBackground isLast -> bottomBackground diff --git a/lawnchair/src/app/lawnchair/search/algorithms/engine/SearchResult.kt b/lawnchair/src/app/lawnchair/search/algorithms/engine/SearchResult.kt index 870cf80393..a38e45d8c6 100644 --- a/lawnchair/src/app/lawnchair/search/algorithms/engine/SearchResult.kt +++ b/lawnchair/src/app/lawnchair/search/algorithms/engine/SearchResult.kt @@ -1,9 +1,8 @@ package app.lawnchair.search.algorithms.engine -import android.annotation.DrawableRes import android.content.pm.ShortcutInfo -import app.lawnchair.search.adapter.HEADER -import app.lawnchair.search.adapter.SPACE +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import app.lawnchair.search.algorithms.data.ContactInfo import app.lawnchair.search.algorithms.data.IFileInfo import app.lawnchair.search.algorithms.data.RecentKeyword @@ -32,7 +31,10 @@ sealed class SearchResult { val searchUrl: String, @DrawableRes val providerIconRes: Int, ) : Action() - data class Header(val title: String, val pkg: String = HEADER) : Action() - data class Divider(val pkg: String = SPACE) : Action() + data class EmptyState( + @StringRes val titleRes: Int, + @StringRes val subtitleRes: Int, + ) : Action() + data object SearchSettings : Action() } } diff --git a/lawnchair/src/app/lawnchair/search/algorithms/engine/SectionBuilder.kt b/lawnchair/src/app/lawnchair/search/algorithms/engine/SectionBuilder.kt index 289e83a92c..448303eba2 100644 --- a/lawnchair/src/app/lawnchair/search/algorithms/engine/SectionBuilder.kt +++ b/lawnchair/src/app/lawnchair/search/algorithms/engine/SectionBuilder.kt @@ -209,3 +209,42 @@ object AppsAndShortcutsSectionBuilder : SectionBuilder { return targets } } + +object EmptyStateSectionBuilder : SectionBuilder { + override fun build( + context: Context, + factory: SearchTargetFactory, + results: List, + ): List { + val result = results.filterIsInstance() + val targets = mutableListOf() + + result.firstOrNull()?.let { + targets.add( + factory.createEmptyStateTarget( + it.titleRes, + it.subtitleRes, + ), + ) + } + return targets + } +} + +object SearchSettingsSectionBuilder : SectionBuilder { + override fun build( + context: Context, + factory: SearchTargetFactory, + results: List, + ): List { + val result = results.filterIsInstance() + val targets = mutableListOf() + + result.firstOrNull()?.let { + targets.add( + factory.createSearchSettingsTarget(), + ) + } + return targets + } +} diff --git a/platform_frameworks_libs_systemui b/platform_frameworks_libs_systemui index cc37c52766..87a0e7b617 160000 --- a/platform_frameworks_libs_systemui +++ b/platform_frameworks_libs_systemui @@ -1 +1 @@ -Subproject commit cc37c52766cd935a2440062492fe13017e7b3487 +Subproject commit 87a0e7b6176bd9d69cc68a70ffac511b0fb7ff20