Single-provider search

This commit is contained in:
Koitharu
2020-02-29 11:52:53 +02:00
parent 9373bae091
commit 5f49030926
21 changed files with 243 additions and 102 deletions

View File

@@ -14,10 +14,10 @@
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity android:name=".ui.main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -29,14 +29,9 @@
</activity>
<activity android:name=".ui.details.MangaDetailsActivity" />
<activity android:name=".ui.reader.ReaderActivity" />
<activity android:name=".ui.search.SearchActivity">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/search" />
</activity>
<activity
android:name=".ui.search.SearchActivity"
android:label="@string/search" />
<activity
android:name=".ui.settings.SettingsActivity"
android:label="@string/settings" />
@@ -44,7 +39,7 @@
<service android:name=".ui.download.DownloadService" />
<provider
android:name=".domain.search.MangaSuggestionsProvider"
android:name=".ui.search.MangaSuggestionsProvider"
android:authorities="${applicationId}.MangaSuggestionsProvider" />
<provider
android:name="androidx.core.content.FileProvider"

View File

@@ -1,29 +0,0 @@
package org.koitharu.kotatsu.domain.search
import android.content.Context
import android.content.SearchRecentSuggestionsProvider
import android.provider.SearchRecentSuggestions
import org.koitharu.kotatsu.BuildConfig
class MangaSuggestionsProvider : SearchRecentSuggestionsProvider() {
init {
setupSuggestions(AUTHORITY, MODE)
}
companion object {
fun saveQuery(context: Context, query: String) {
SearchRecentSuggestions(context, AUTHORITY, MODE)
.saveRecentQuery(query, null)
}
fun clearHistory(context: Context) {
SearchRecentSuggestions(context, AUTHORITY, MODE)
.clearHistory()
}
private const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.MangaSuggestionsProvider"
private const val MODE = DATABASE_MODE_QUERIES
}
}

View File

@@ -20,7 +20,6 @@ import org.koitharu.kotatsu.ui.main.list.history.HistoryListFragment
import org.koitharu.kotatsu.ui.main.list.local.LocalListFragment
import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment
import org.koitharu.kotatsu.ui.settings.SettingsActivity
import org.koitharu.kotatsu.utils.SearchHelper
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener,
SharedPreferences.OnSharedPreferenceChangeListener {
@@ -65,7 +64,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.opt_main, menu)
menu?.findItem(R.id.action_search)?.let(SearchHelper::setupSearchView)
return super.onCreateOptionsMenu(menu)
}

View File

@@ -1,9 +1,13 @@
package org.koitharu.kotatsu.ui.main.list.remote
import android.view.Menu
import android.view.MenuInflater
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaFilter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.main.list.MangaListFragment
import org.koitharu.kotatsu.ui.search.SearchHelper
import org.koitharu.kotatsu.utils.ext.withArgs
class RemoteListFragment : MangaListFragment<Unit>() {
@@ -25,6 +29,14 @@ class RemoteListFragment : MangaListFragment<Unit>() {
super.onFilterChanged(filter)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.opt_remote, menu)
menu.findItem(R.id.action_search)?.let { menuItem ->
SearchHelper.setupSearchView(menuItem, source)
}
super.onCreateOptionsMenu(menu, inflater)
}
companion object {
private const val ARG_SOURCE = "provider"

View File

@@ -0,0 +1,91 @@
package org.koitharu.kotatsu.ui.search
import android.app.SearchManager
import android.content.ContentResolver
import android.content.Context
import android.content.SearchRecentSuggestionsProvider
import android.database.Cursor
import android.net.Uri
import android.provider.SearchRecentSuggestions
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.cursoradapter.widget.CursorAdapter
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
class MangaSuggestionsProvider : SearchRecentSuggestionsProvider() {
init {
setupSuggestions(
AUTHORITY,
MODE
)
}
private class SearchSuggestionAdapter(context: Context, cursor: Cursor) : CursorAdapter(
context, cursor,
FLAG_REGISTER_CONTENT_OBSERVER
) {
override fun newView(context: Context, cursor: Cursor?, parent: ViewGroup?): View {
return LayoutInflater.from(context)
.inflate(R.layout.item_search_complete, parent, false)
}
override fun bindView(view: View, context: Context, cursor: Cursor) {
if (view !is TextView) return
view.text = cursor.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY))
}
override fun convertToString(cursor: Cursor?): CharSequence {
return cursor?.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY))
.orEmpty()
}
}
companion object {
private const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.MangaSuggestionsProvider"
private const val MODE = DATABASE_MODE_QUERIES
private val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(AUTHORITY)
.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY)
.build()
private val projection = arrayOf("_id", SearchManager.SUGGEST_COLUMN_QUERY)
fun saveQuery(context: Context, query: String) {
SearchRecentSuggestions(
context,
AUTHORITY,
MODE
).saveRecentQuery(query, null)
}
fun clearHistory(context: Context) {
SearchRecentSuggestions(
context,
AUTHORITY,
MODE
).clearHistory()
}
private fun getCursor(context: Context): Cursor? {
return context.contentResolver?.query(uri, projection, null, arrayOf(""), null)
}
fun getSuggestionAdapter(context: Context): CursorAdapter? = getCursor(
context
)?.let { cursor ->
SearchSuggestionAdapter(context, cursor).also {
it.setFilterQueryProvider { q ->
context.contentResolver?.query(uri, projection, " ?", arrayOf(q.toString()), null)
}
}
}
}
}

View File

@@ -1,10 +1,11 @@
package org.koitharu.kotatsu.ui.search
import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.domain.search.MangaSuggestionsProvider
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.common.BaseActivity
class SearchActivity : BaseActivity() {
@@ -12,22 +13,31 @@ class SearchActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
val query = if (Intent.ACTION_SEARCH == intent.action) {
intent.getStringExtra(SearchManager.QUERY)?.trim()
} else {
null
}
if (query == null) {
val source = intent.getParcelableExtra<MangaSource>(EXTRA_SOURCE)
val query = intent.getStringExtra(EXTRA_QUERY)
if (source == null || query == null) {
finish()
return
}
MangaSuggestionsProvider.saveQuery(this, query)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
title = query
supportActionBar?.setSubtitle(R.string.search_results)
supportFragmentManager
.beginTransaction()
.replace(R.id.container, SearchFragment.newInstance(query))
.replace(R.id.container, SearchFragment.newInstance(source, query))
.commit()
}
companion object {
private const val EXTRA_SOURCE = "source"
private const val EXTRA_QUERY = "query"
fun newIntent(context: Context, source: MangaSource, query: String) =
Intent(context, SearchActivity::class.java)
.putExtra(EXTRA_SOURCE, source as Parcelable)
.putExtra(EXTRA_QUERY, query)
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.ui.search
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.main.list.MangaListFragment
import org.koitharu.kotatsu.utils.ext.withArgs
@@ -9,9 +10,10 @@ class SearchFragment : MangaListFragment<Unit>() {
private val presenter by moxyPresenter(factory = ::SearchPresenter)
private val query by stringArg(ARG_QUERY)
private val source by arg<MangaSource>(ARG_SOURCE)
override fun onRequestMoreItems(offset: Int) {
presenter.loadList(query.orEmpty(), offset)
presenter.loadList(source, query.orEmpty(), offset)
}
override fun getTitle(): CharSequence? {
@@ -21,8 +23,10 @@ class SearchFragment : MangaListFragment<Unit>() {
companion object {
private const val ARG_QUERY = "query"
private const val ARG_SOURCE = "source"
fun newInstance(query: String) = SearchFragment().withArgs(1) {
fun newInstance(source: MangaSource, query: String) = SearchFragment().withArgs(2) {
putParcelable(ARG_SOURCE, source)
putString(ARG_QUERY, query)
}
}

View File

@@ -0,0 +1,55 @@
package org.koitharu.kotatsu.ui.search
import android.app.SearchManager
import android.content.Context
import android.database.Cursor
import android.view.MenuItem
import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.SearchView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.utils.ext.safe
object SearchHelper {
@JvmStatic
fun setupSearchView(menuItem: MenuItem, source: MangaSource) {
val view = menuItem.actionView as? SearchView ?: return
val context = view.context
view.queryHint = context.getString(R.string.search_manga)
view.imeOptions = EditorInfo.IME_ACTION_SEARCH
view.inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE
view.suggestionsAdapter = MangaSuggestionsProvider.getSuggestionAdapter(context)
view.setOnQueryTextListener(QueryListener(context, source))
view.setOnSuggestionListener(SuggestionListener(view))
}
private class QueryListener(private val context: Context, private val source: MangaSource) :
SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return if (!query.isNullOrBlank()) {
context.startActivity(SearchActivity.newIntent(context, source, query.trim()))
MangaSuggestionsProvider.saveQuery(context, query)
true
} else false
}
override fun onQueryTextChange(newText: String?) = false
}
private class SuggestionListener(private val view: SearchView) :
SearchView.OnSuggestionListener {
override fun onSuggestionSelect(position: Int) = false
override fun onSuggestionClick(position: Int): Boolean {
val query = safe {
val c = view.suggestionsAdapter.getItem(position) as? Cursor
c?.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY))
} ?: return false
view.setQuery(query, true)
return true
}
}
}

View File

@@ -21,13 +21,12 @@ class SearchPresenter : BasePresenter<MangaListView<Unit>>() {
super.onFirstViewAttach()
}
fun loadList(query: String, offset: Int) {
fun loadList(source: MangaSource, query: String, offset: Int) {
presenterScope.launch {
viewState.onLoadingChanged(true)
try {
//TODO select source
val list = withContext(Dispatchers.IO) {
MangaProviderFactory.create(MangaSource.READMANGA_RU)
MangaProviderFactory.create(source)
.getList(offset, query = query)
}
if (offset == 0) {

View File

@@ -1,20 +0,0 @@
package org.koitharu.kotatsu.utils
import android.app.SearchManager
import android.content.ComponentName
import android.content.Context
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import org.koitharu.kotatsu.ui.search.SearchActivity
object SearchHelper {
@JvmStatic
fun setupSearchView(menuItem: MenuItem) {
val view = menuItem.actionView as? SearchView ?: return
val context = view.context
val searchManager = context.applicationContext.getSystemService(Context.SEARCH_SERVICE) as SearchManager
val info = searchManager.getSearchableInfo(ComponentName(context, SearchActivity::class.java))
view.setSearchableInfo(info)
}
}

View File

@@ -10,7 +10,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
android:theme="@style/AppToolbarTheme">
<androidx.appcompat.widget.Toolbar
android:id="@id/toolbar"

View File

@@ -18,14 +18,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
android:theme="@style/AppToolbarTheme">
<androidx.appcompat.widget.Toolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/AppPopupTheme" />
app:layout_scrollFlags="scroll|enterAlways" />
</com.google.android.material.appbar.AppBarLayout>

View File

@@ -28,7 +28,7 @@
android:background="@color/dim"
android:elevation="0dp"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
android:theme="@style/AppToolbarTheme"
app:elevation="0dp">
<com.google.android.material.appbar.MaterialToolbar
@@ -47,7 +47,7 @@
android:background="@color/dim"
android:elevation="0dp"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
android:theme="@style/AppToolbarTheme"
app:elevation="0dp">
<com.google.android.material.appbar.MaterialToolbar

View File

@@ -11,7 +11,7 @@
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:background="?colorPrimary"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
android:theme="@style/AppToolbarTheme">
<androidx.appcompat.widget.Toolbar
android:id="@id/toolbar"

View File

@@ -10,7 +10,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
android:theme="@style/AppToolbarTheme">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?listPreferredItemHeightSmall"
android:background="?selectableItemBackground"
android:drawableStart="@drawable/ic_history"
android:drawablePadding="20dp"
android:gravity="center_vertical"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:textAppearance="?textAppearanceListItemSmall"
android:textColor="?android:textColorPrimary"
android:theme="@style/AppPopupTheme"
tools:text="@tools:sample/full_names" />

View File

@@ -3,12 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search"
android:orderInCategory="0"
android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" />
</menu>

View File

@@ -0,0 +1,14 @@
<?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/action_search"
android:icon="@drawable/ic_search"
android:orderInCategory="1"
android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always|collapseActionView" />
</menu>

View File

@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppPopupTheme" parent="ThemeOverlay.MaterialComponents.Dark" />
<style name="AppSuggestion" parent="Widget.AppCompat.AutoCompleteTextView">
<item name="android:popupBackground">@android:color/background_dark</item>
</style>
</resources>

View File

@@ -10,4 +10,13 @@
<style name="AppPopupTheme" parent="ThemeOverlay.MaterialComponents.Light" />
<style name="AppToolbarTheme" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" >
<item name="popupTheme">@style/AppPopupTheme</item>
<item name="autoCompleteTextViewStyle">@style/AppSuggestion</item>
</style>
<style name="AppSuggestion" parent="Widget.AppCompat.Light.AutoCompleteTextView">
<item name="android:popupBackground">@android:color/background_light</item>
</style>
</resources>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<searchable
xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_manga"
android:inputType="textPersonName"
android:label="@string/app_name"
android:searchSuggestAuthority="org.koitharu.kotatsu.MangaSuggestionsProvider"
android:searchSuggestSelection=" ?"
android:voiceLanguageModel="web_search"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" />