1
0
Fork 0
mirror of https://github.com/MatomoCamp/matomocamp-companion-android.git synced 2024-09-19 16:13:46 +02:00

remove SearchView and search suggestions provider, replace it with an EditText and sample its updates

This commit is contained in:
Christophe Beyls 2021-10-10 15:50:18 +02:00
parent f43a5241a0
commit 6fde0a2c56
21 changed files with 155 additions and 250 deletions

View file

@ -59,6 +59,9 @@ android {
}
kotlinOptions {
freeCompilerArgs = [
'-Xopt-in=kotlin.RequiresOptIn'
]
jvmTarget = "1.8"
}

View file

@ -1,4 +1 @@
# Add project specific ProGuard rules here.
# Action Views
-keep class androidx.appcompat.widget.SearchView { <init>(...); }

View file

@ -78,10 +78,6 @@
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/main_searchable" />
</activity>
<activity
android:name=".activities.ExternalBookmarksActivity"
@ -123,10 +119,6 @@
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
<provider
android:name=".providers.SearchSuggestionProvider"
android:authorities="${applicationId}.search"
android:exported="true" />
<provider
android:name=".providers.BookmarksExportProvider"
android:authorities="${applicationId}.bookmarks"

View file

@ -1,7 +1,6 @@
package be.digitalia.fosdem.activities
import android.annotation.SuppressLint
import android.app.SearchManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
@ -19,10 +18,8 @@ import android.widget.TextView
import androidx.annotation.IdRes
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.edit
import androidx.core.content.getSystemService
import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible
import androidx.drawerlayout.widget.DrawerLayout
@ -44,7 +41,6 @@ import be.digitalia.fosdem.model.LoadingState
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
import be.digitalia.fosdem.utils.awaitCloseDrawer
import be.digitalia.fosdem.utils.configureToolbarColors
import be.digitalia.fosdem.utils.fixCollapsibleActionView
import be.digitalia.fosdem.utils.setNfcAppDataPushMessageCallbackIfAvailable
import com.google.android.material.navigation.NavigationView
import com.google.android.material.progressindicator.BaseProgressIndicator
@ -89,7 +85,6 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
private lateinit var holder: ViewHolder
private lateinit var drawerToggle: ActionBarDrawerToggle
private var searchMenuItem: MenuItem? = null
private lateinit var currentSection: Section
@ -260,26 +255,8 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
}
}
override fun onStop() {
searchMenuItem?.run {
if (isActionViewExpanded) {
collapseActionView()
}
}
super.onStop()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main, menu)
searchMenuItem = menu.findItem(R.id.search)?.apply {
fixCollapsibleActionView()
// Associate searchable configuration with the SearchView
val searchManager: SearchManager? = getSystemService()
(actionView as SearchView).setSearchableInfo(searchManager?.getSearchableInfo(componentName))
}
return true
}
@ -290,6 +267,12 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
}
return when (item.itemId) {
R.id.search -> {
val intent = Intent(this, SearchResultActivity::class.java)
startActivity(intent)
overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
true
}
R.id.refresh -> {
val icon = item.icon
if (icon is Animatable) {

View file

@ -3,106 +3,103 @@ package be.digitalia.fosdem.activities
import android.app.SearchManager
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.View
import android.widget.EditText
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.core.content.getSystemService
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.add
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import be.digitalia.fosdem.R
import be.digitalia.fosdem.fragments.SearchResultListFragment
import be.digitalia.fosdem.utils.trimNonAlpha
import be.digitalia.fosdem.viewmodels.SearchViewModel
import be.digitalia.fosdem.viewmodels.SearchViewModel.Result.QueryTooShort
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
@AndroidEntryPoint
class SearchResultActivity : SimpleToolbarActivity() {
class SearchResultActivity : AppCompatActivity(R.layout.search_result) {
private val viewModel: SearchViewModel by viewModels()
private var searchView: SearchView? = null
private lateinit var searchEditText: EditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null) {
supportFragmentManager.commit { add<SearchResultListFragment>(R.id.content) }
handleIntent(intent, false)
searchEditText = findViewById(R.id.search_edittext)
val searchClearButton: View = findViewById(R.id.search_clear)
searchEditText.textChangeEvents
.onEach {
// immediately update the button state
searchClearButton.isGone = it.isNullOrEmpty()
}
.sample(SEARCH_INPUT_SAMPLE_MILLIS)
.onEach {
// only update the results every SEARCH_INPUT_SAMPLE_MILLIS
viewModel.query = it?.toString() ?: ""
}
.launchIn(lifecycleScope)
searchClearButton.setOnClickListener {
searchEditText.text = null
}
viewModel.results.observe(this) { result ->
if (result is QueryTooShort) {
with(theme.obtainStyledAttributes(R.styleable.ErrorColors)) {
val textColor = getColor(R.styleable.ErrorColors_colorOnError, 0)
val backgroundColor = getColor(R.styleable.ErrorColors_colorError, 0)
recycle()
Snackbar.make(findViewById(R.id.content), R.string.search_length_error, Snackbar.LENGTH_LONG)
.setTextColor(textColor)
.setBackgroundTint(backgroundColor)
.show()
}
}
if (savedInstanceState == null) {
supportFragmentManager.commit { add<SearchResultListFragment>(R.id.content) }
handleIntent(intent)
searchEditText.requestFocus()
} else {
searchEditText.setText(viewModel.query)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent, true)
handleIntent(intent)
}
private fun handleIntent(intent: Intent, isNewIntent: Boolean) {
when (intent.action) {
Intent.ACTION_SEARCH, GMS_ACTION_SEARCH -> {
// Normal search, results are displayed here
val query = intent.getStringExtra(SearchManager.QUERY)?.trim() ?: ""
searchView?.setQueryWithoutFocus(query)
viewModel.query = query
}
Intent.ACTION_VIEW -> {
// Search suggestion, dispatch to EventDetailsActivity
val dispatchIntent = Intent(this, EventDetailsActivity::class.java).setData(intent.data)
startActivity(dispatchIntent)
if (!isNewIntent) {
finish()
}
}
private fun handleIntent(intent: Intent) {
val query = when (intent.action) {
Intent.ACTION_SEARCH, GMS_ACTION_SEARCH -> intent.getStringExtra(SearchManager.QUERY)
?.trimNonAlpha() ?: ""
else -> ""
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.search, menu)
menu.findItem(R.id.search)?.apply {
// Associate searchable configuration with the SearchView
val searchManager: SearchManager? = getSystemService()
searchView = (actionView as SearchView).apply {
setSearchableInfo(searchManager?.getSearchableInfo(componentName))
setIconifiedByDefault(false) // Always show the search view
setQueryWithoutFocus(viewModel.query)
}
}
return true
}
private fun SearchView.setQueryWithoutFocus(query: String?) {
// Force losing the focus to prevent the suggestions from appearing
clearFocus()
isFocusable = false
isFocusableInTouchMode = false
setQuery(query, false)
viewModel.query = query
searchEditText.setText(query)
}
override fun onSupportNavigateUp(): Boolean {
finish()
onBackPressed()
return true
}
override fun finish() {
super.finish()
overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
}
@OptIn(ExperimentalCoroutinesApi::class)
private val EditText.textChangeEvents: Flow<CharSequence?>
get() = callbackFlow {
val textWatcher = doOnTextChanged { text, _, _, _ -> trySend(text) }
awaitClose { removeTextChangedListener(textWatcher) }
}
companion object {
// Search Intent sent by Google Now
private const val GMS_ACTION_SEARCH = "com.google.android.gms.actions.SEARCH_ACTION"
private const val SEARCH_INPUT_SAMPLE_MILLIS = 400L
}
}

View file

@ -1,8 +1,5 @@
package be.digitalia.fosdem.db
import android.app.SearchManager
import android.database.Cursor
import android.provider.BaseColumns
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.core.content.edit
@ -410,38 +407,6 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
ORDER BY e.start_time ASC""")
abstract fun getSearchResults(query: String): DataSource.Factory<Int, StatusEvent>
/**
* Method called by SearchSuggestionProvider to return search results in the format expected by the search framework.
*/
@Query("""SELECT e.id AS ${BaseColumns._ID},
et.title AS ${SearchManager.SUGGEST_COLUMN_TEXT_1},
IFNULL(GROUP_CONCAT(p.name, ', '), '') || ' - ' || t.name AS ${SearchManager.SUGGEST_COLUMN_TEXT_2},
e.id AS ${SearchManager.SUGGEST_COLUMN_INTENT_DATA}
FROM events e
JOIN events_titles et ON e.id = et.`rowid`
JOIN tracks t ON e.track_id = t.id
LEFT JOIN events_persons ep ON e.id = ep.event_id
LEFT JOIN persons p ON ep.person_id = p.`rowid`
WHERE e.id IN (
SELECT `rowid`
FROM events_titles
WHERE events_titles MATCH :query || '*'
UNION
SELECT e.id
FROM events e
JOIN tracks t ON e.track_id = t.id
WHERE t.name LIKE '%' || :query || '%'
UNION
SELECT ep.event_id
FROM events_persons ep
JOIN persons p ON ep.person_id = p.`rowid`
WHERE p.name MATCH :query || '*'
)
GROUP BY e.id
ORDER BY e.start_time ASC LIMIT :limit""")
@WorkerThread
abstract fun getSearchSuggestionResults(query: String, limit: Int): Cursor
/**
* Returns all persons in alphabetical order.
*/

View file

@ -1,63 +0,0 @@
package be.digitalia.fosdem.providers
import android.app.SearchManager
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import androidx.core.content.ContentProviderCompat
import be.digitalia.fosdem.db.ScheduleDao
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
/**
* Simple content provider responsible for search suggestions.
*
* @author Christophe Beyls
*/
class SearchSuggestionProvider : ContentProvider() {
private val scheduleDao: ScheduleDao by lazy {
EntryPointAccessors.fromApplication(
ContentProviderCompat.requireContext(this),
SearchSuggestionProviderEntryPoint::class.java
).scheduleDao
}
override fun onCreate() = true
override fun insert(uri: Uri, values: ContentValues?) = throw UnsupportedOperationException()
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?) = throw UnsupportedOperationException()
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = throw UnsupportedOperationException()
override fun getType(uri: Uri) = SearchManager.SUGGEST_MIME_TYPE
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
var query = uri.lastPathSegment ?: return null
// Ignore empty or too small queries
query = query.trim()
if (query.length < MIN_QUERY_LENGTH || query == "search_suggest_query") {
return null
}
val limitParam = uri.getQueryParameter("limit")
val limit = if (limitParam.isNullOrEmpty()) DEFAULT_MAX_RESULTS else limitParam.toInt()
return scheduleDao.getSearchSuggestionResults(query, limit)
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SearchSuggestionProviderEntryPoint {
val scheduleDao: ScheduleDao
}
companion object {
private const val MIN_QUERY_LENGTH = 3
private const val DEFAULT_MAX_RESULTS = 5
}
}

View file

@ -1,17 +0,0 @@
package be.digitalia.fosdem.utils
import android.view.MenuItem
// Workaround for disappearing menu items bug
fun MenuItem.fixCollapsibleActionView() {
setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
item.actionView?.findActivity()?.invalidateOptionsMenu()
return true
}
})
}

View file

@ -78,7 +78,7 @@ private fun String.replaceNonAlphaGroups(replacement: Char): String {
/**
* Removes all removable chars at the beginning and end of source.
*/
private fun String.trimNonAlpha(): String {
fun String.trimNonAlpha(): String {
return trim { it.isRemovable }
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:fromAlpha="0.0"
android:toAlpha="1.0" />

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:fillAfter="true"
android:fromAlpha="1.0"
android:toAlpha="0.0" />

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>

View file

@ -4,7 +4,7 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -30,7 +30,7 @@
android:layout_width="match_parent"
android:layout_height="128dp" />
<androidx.appcompat.widget.Toolbar
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activities.SearchResultActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
style="@style/Widget.App.Toolbar.PrimarySurface"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<EditText
android:id="@+id/search_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@null"
android:hint="@string/search_hint"
android:imeOptions="actionSearch"
android:inputType="text"
android:saveEnabled="false"
android:singleLine="true" />
<ImageView
android:id="@+id/search_clear"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_margin="10dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/search_clear"
android:scaleType="center"
android:visibility="gone"
app:srcCompat="@drawable/ic_clear_white_24dp"
tools:visibility="visible" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>

View file

@ -11,7 +11,7 @@
android:layout_height="wrap_content"
android:theme="?actionBarTheme">
<androidx.appcompat.widget.Toolbar
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -11,7 +11,7 @@
android:layout_height="wrap_content"
android:theme="?actionBarTheme">
<androidx.appcompat.widget.Toolbar
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -7,9 +7,7 @@
android:icon="@drawable/ic_search_white_24dp"
android:menuCategory="secondary"
android:title="@string/search_events"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:iconTint="?colorControlNormal"
app:showAsAction="ifRoom|collapseActionView" />
app:iconTint="?colorControlNormal" />
<item
android:id="@+id/refresh"
android:icon="@drawable/avd_sync_white_24dp"

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/search"
android:icon="@drawable/ic_search_white_24dp"
android:title="@string/search_events"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:iconTint="?colorControlNormal"
app:showAsAction="always" />
</menu>

View file

@ -59,8 +59,8 @@
<!-- Search -->
<string name="search_events">Search events</string>
<string name="search_settings_description">Events</string>
<string name="search_hint">Event, track, person</string>
<string name="search_clear">Clear text</string>
<string name="no_search_result">No result.</string>
<!-- External bookmarks -->
@ -73,7 +73,6 @@
<string name="event_not_found_error">Unable to load the session details.\nMake sure the database has been updated to the latest version.</string>
<string name="schedule_loading_error">An error occurred during schedule update. Check your device connectivity.</string>
<string name="schedule_loading_retry_action">Retry</string>
<string name="search_length_error">The minimum search text length is 3 chars.</string>
<!-- Settings -->
<string name="settings">Settings</string>

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:imeOptions="actionSearch"
android:includeInGlobalSearch="true"
android:label="@string/app_name"
android:searchSettingsDescription="@string/search_settings_description"
android:searchSuggestAuthority="be.digitalia.fosdem.search"
android:searchSuggestIntentAction="android.intent.action.VIEW"
android:searchSuggestThreshold="3"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" >
</searchable>