diff --git a/app/build.gradle b/app/build.gradle index e24eb6f..585e4a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -59,6 +59,9 @@ android { } kotlinOptions { + freeCompilerArgs = [ + '-Xopt-in=kotlin.RequiresOptIn' + ] jvmTarget = "1.8" } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index a77e45f..fb164d6 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,4 +1 @@ # Add project specific ProGuard rules here. - -# Action Views --keep class androidx.appcompat.widget.SearchView { (...); } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f998c2a..0d08ba6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -78,10 +78,6 @@ - - - { + 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) { diff --git a/app/src/main/java/be/digitalia/fosdem/activities/SearchResultActivity.kt b/app/src/main/java/be/digitalia/fosdem/activities/SearchResultActivity.kt index fb0f9eb..33d61aa 100644 --- a/app/src/main/java/be/digitalia/fosdem/activities/SearchResultActivity.kt +++ b/app/src/main/java/be/digitalia/fosdem/activities/SearchResultActivity.kt @@ -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(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(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 + 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 } } \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/db/ScheduleDao.kt b/app/src/main/java/be/digitalia/fosdem/db/ScheduleDao.kt index fdedc14..aca38a1 100644 --- a/app/src/main/java/be/digitalia/fosdem/db/ScheduleDao.kt +++ b/app/src/main/java/be/digitalia/fosdem/db/ScheduleDao.kt @@ -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 - /** - * 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. */ diff --git a/app/src/main/java/be/digitalia/fosdem/providers/SearchSuggestionProvider.kt b/app/src/main/java/be/digitalia/fosdem/providers/SearchSuggestionProvider.kt deleted file mode 100644 index 7dbc8ab..0000000 --- a/app/src/main/java/be/digitalia/fosdem/providers/SearchSuggestionProvider.kt +++ /dev/null @@ -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?) = throw UnsupportedOperationException() - - override fun delete(uri: Uri, selection: String?, selectionArgs: Array?) = throw UnsupportedOperationException() - - override fun getType(uri: Uri) = SearchManager.SUGGEST_MIME_TYPE - - override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, 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 - } -} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/utils/MenuItemExt.kt b/app/src/main/java/be/digitalia/fosdem/utils/MenuItemExt.kt deleted file mode 100644 index a1c4887..0000000 --- a/app/src/main/java/be/digitalia/fosdem/utils/MenuItemExt.kt +++ /dev/null @@ -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 - } - }) -} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/utils/StringUtils.kt b/app/src/main/java/be/digitalia/fosdem/utils/StringUtils.kt index 05d0da0..81db608 100644 --- a/app/src/main/java/be/digitalia/fosdem/utils/StringUtils.kt +++ b/app/src/main/java/be/digitalia/fosdem/utils/StringUtils.kt @@ -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 } } diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml new file mode 100644 index 0000000..03312a2 --- /dev/null +++ b/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml new file mode 100644 index 0000000..c5f93a3 --- /dev/null +++ b/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_clear_white_24dp.xml b/app/src/main/res/drawable/ic_clear_white_24dp.xml new file mode 100644 index 0000000..fd8599c --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/dialog_room_image.xml b/app/src/main/res/layout/dialog_room_image.xml index 0e1e42a..beadfbd 100644 --- a/app/src/main/res/layout/dialog_room_image.xml +++ b/app/src/main/res/layout/dialog_room_image.xml @@ -4,7 +4,7 @@ android:layout_height="wrap_content" android:orientation="vertical"> - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/single_event.xml b/app/src/main/res/layout/single_event.xml index 0f84f66..48d652f 100644 --- a/app/src/main/res/layout/single_event.xml +++ b/app/src/main/res/layout/single_event.xml @@ -11,7 +11,7 @@ android:layout_height="wrap_content" android:theme="?actionBarTheme"> - - + app:iconTint="?colorControlNormal" /> - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dd0c3e5..1d9632d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,8 +59,8 @@ Search events - Events Event, track, person + Clear text No result. @@ -73,7 +73,6 @@ Unable to load the session details.\nMake sure the database has been updated to the latest version. An error occurred during schedule update. Check your device connectivity. Retry - The minimum search text length is 3 chars. Settings diff --git a/app/src/main/res/xml/main_searchable.xml b/app/src/main/res/xml/main_searchable.xml deleted file mode 100644 index 7fe228a..0000000 --- a/app/src/main/res/xml/main_searchable.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - \ No newline at end of file