Single-provider search

pull/1/head
Koitharu 6 years ago
parent 9373bae091
commit 5f49030926

@ -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"

@ -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
}
}

@ -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)
}

@ -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"

@ -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)
}
}
}
}
}

@ -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)
}
}

@ -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)
}
}

@ -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
}
}
}

@ -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) {

@ -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)
}
}

@ -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"

@ -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>

@ -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

@ -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"

@ -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"

@ -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" />

@ -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>

@ -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>

@ -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>

@ -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>

@ -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" />
Loading…
Cancel
Save