diff --git a/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt b/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt index b8bef5a2..c81618dd 100644 --- a/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt +++ b/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt @@ -8,40 +8,43 @@ import com.looker.droidify.utility.extension.android.asSequence import com.looker.droidify.utility.extension.text.debug class QueryBuilder { - companion object { - fun trimQuery(query: String): String { - return query.lines().map { it.trim() }.filter { it.isNotEmpty() }.joinToString(separator = " ") + companion object { + fun trimQuery(query: String): String { + return query.lines().map { it.trim() }.filter { it.isNotEmpty() } + .joinToString(separator = " ") + } } - } - private val builder = StringBuilder() - private val arguments = mutableListOf() + private val builder = StringBuilder() + private val arguments = mutableListOf() - operator fun plusAssign(query: String) { - if (builder.isNotEmpty()) { - builder.append(" ") + operator fun plusAssign(query: String) { + if (builder.isNotEmpty()) { + builder.append(" ") + } + builder.append(trimQuery(query)) } - builder.append(trimQuery(query)) - } - operator fun remAssign(argument: String) { - this.arguments += argument - } - - operator fun remAssign(arguments: List) { - this.arguments += arguments - } - - fun query(db: SQLiteDatabase, signal: CancellationSignal?): Cursor { - val query = builder.toString() - val arguments = arguments.toTypedArray() - if (BuildConfig.DEBUG) { - synchronized(QueryBuilder::class.java) { - debug(query) - db.rawQuery("EXPLAIN QUERY PLAN $query", arguments).use { it.asSequence() - .forEach { debug(":: ${it.getString(it.getColumnIndex("detail"))}") } } - } + operator fun remAssign(argument: String) { + this.arguments += argument + } + + operator fun remAssign(arguments: List) { + this.arguments += arguments + } + + fun query(db: SQLiteDatabase, signal: CancellationSignal?): Cursor { + val query = builder.toString() + val arguments = arguments.toTypedArray() + if (BuildConfig.DEBUG) { + synchronized(QueryBuilder::class.java) { + debug(query) + db.rawQuery("EXPLAIN QUERY PLAN $query", arguments).use { + it.asSequence() + .forEach { debug(":: ${it.getString(it.getColumnIndex("detail"))}") } + } + } + } + return db.rawQuery(query, arguments, signal) } - return db.rawQuery(query, arguments, signal) - } } diff --git a/src/main/kotlin/com/looker/droidify/screen/ProductsFragment.kt b/src/main/kotlin/com/looker/droidify/screen/ProductsFragment.kt index 8b65f108..e74689dd 100644 --- a/src/main/kotlin/com/looker/droidify/screen/ProductsFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/ProductsFragment.kt @@ -8,172 +8,185 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.schedulers.Schedulers import com.looker.droidify.R import com.looker.droidify.database.CursorOwner import com.looker.droidify.database.Database import com.looker.droidify.entity.ProductItem import com.looker.droidify.utility.RxUtils import com.looker.droidify.widget.RecyclerFastScroller +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.schedulers.Schedulers -class ProductsFragment(): ScreenFragment(), CursorOwner.Callback { - companion object { - private const val EXTRA_SOURCE = "source" +class ProductsFragment() : ScreenFragment(), CursorOwner.Callback { + companion object { + private const val EXTRA_SOURCE = "source" - private const val STATE_CURRENT_SEARCH_QUERY = "currentSearchQuery" - private const val STATE_CURRENT_SECTION = "currentSection" - private const val STATE_CURRENT_ORDER = "currentOrder" - private const val STATE_LAYOUT_MANAGER = "layoutManager" - } - - enum class Source(val titleResId: Int, val sections: Boolean, val order: Boolean) { - AVAILABLE(R.string.available, true, true), - INSTALLED(R.string.installed, false, false), - UPDATES(R.string.updates, false, false) - } - - constructor(source: Source): this() { - arguments = Bundle().apply { - putString(EXTRA_SOURCE, source.name) - } - } - - val source: Source - get() = requireArguments().getString(EXTRA_SOURCE)!!.let(Source::valueOf) - - private var searchQuery = "" - private var section: ProductItem.Section = ProductItem.Section.All - private var order = ProductItem.Order.NAME - - private var currentSearchQuery = "" - private var currentSection: ProductItem.Section = ProductItem.Section.All - private var currentOrder = ProductItem.Order.NAME - private var layoutManagerState: Parcelable? = null - - private var recyclerView: RecyclerView? = null - - private var repositoriesDisposable: Disposable? = null - - private val request: CursorOwner.Request - get() { - val searchQuery = searchQuery - val section = if (source.sections) section else ProductItem.Section.All - val order = if (source.order) order else ProductItem.Order.NAME - return when (source) { - Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(searchQuery, section, order) - Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(searchQuery, section, order) - Source.UPDATES -> CursorOwner.Request.ProductsUpdates(searchQuery, section, order) - } + private const val STATE_CURRENT_SEARCH_QUERY = "currentSearchQuery" + private const val STATE_CURRENT_SECTION = "currentSection" + private const val STATE_CURRENT_ORDER = "currentOrder" + private const val STATE_LAYOUT_MANAGER = "layoutManager" } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return RecyclerView(requireContext()).apply { - id = android.R.id.list - layoutManager = LinearLayoutManager(context) - isMotionEventSplittingEnabled = false - isVerticalScrollBarEnabled = false - setHasFixedSize(true) - recycledViewPool.setMaxRecycledViews(ProductsAdapter.ViewType.PRODUCT.ordinal, 30) - val adapter = ProductsAdapter { screenActivity.navigateProduct(it.packageName) } - this.adapter = adapter - RecyclerFastScroller(this) - recyclerView = this + enum class Source(val titleResId: Int, val sections: Boolean, val order: Boolean) { + AVAILABLE(R.string.available, true, true), + INSTALLED(R.string.installed, false, false), + UPDATES(R.string.updates, false, false) } - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - currentSearchQuery = savedInstanceState?.getString(STATE_CURRENT_SEARCH_QUERY).orEmpty() - currentSection = savedInstanceState?.getParcelable(STATE_CURRENT_SECTION) ?: ProductItem.Section.All - currentOrder = savedInstanceState?.getString(STATE_CURRENT_ORDER) - ?.let(ProductItem.Order::valueOf) ?: ProductItem.Order.NAME - layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER) - - screenActivity.cursorOwner.attach(this, request) - repositoriesDisposable = Observable.just(Unit) - .concatWith(Database.observable(Database.Subject.Repositories)) - .observeOn(Schedulers.io()) - .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } - .map { it.asSequence().map { Pair(it.id, it) }.toMap() } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { (recyclerView?.adapter as? ProductsAdapter)?.repositories = it } - } - - override fun onDestroyView() { - super.onDestroyView() - - recyclerView = null - - screenActivity.cursorOwner.detach(this) - repositoriesDisposable?.dispose() - repositoriesDisposable = null - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - - outState.putString(STATE_CURRENT_SEARCH_QUERY, currentSearchQuery) - outState.putParcelable(STATE_CURRENT_SECTION, currentSection) - outState.putString(STATE_CURRENT_ORDER, currentOrder.name) - (layoutManagerState ?: recyclerView?.layoutManager?.onSaveInstanceState()) - ?.let { outState.putParcelable(STATE_LAYOUT_MANAGER, it) } - } - - override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { - (recyclerView?.adapter as? ProductsAdapter)?.apply { - this.cursor = cursor - emptyText = when { - cursor == null -> "" - searchQuery.isNotEmpty() -> getString(R.string.no_matching_applications_found) - else -> when (source) { - Source.AVAILABLE -> getString(R.string.no_applications_available) - Source.INSTALLED -> getString(R.string.no_applications_installed) - Source.UPDATES -> getString(R.string.all_applications_up_to_date) + constructor(source: Source) : this() { + arguments = Bundle().apply { + putString(EXTRA_SOURCE, source.name) } - } } - layoutManagerState?.let { - layoutManagerState = null - recyclerView?.layoutManager?.onRestoreInstanceState(it) + val source: Source + get() = requireArguments().getString(EXTRA_SOURCE)!!.let(Source::valueOf) + + private var searchQuery = "" + private var section: ProductItem.Section = ProductItem.Section.All + private var order = ProductItem.Order.NAME + + private var currentSearchQuery = "" + private var currentSection: ProductItem.Section = ProductItem.Section.All + private var currentOrder = ProductItem.Order.NAME + private var layoutManagerState: Parcelable? = null + + private var recyclerView: RecyclerView? = null + + private var repositoriesDisposable: Disposable? = null + + private val request: CursorOwner.Request + get() { + val searchQuery = searchQuery + val section = if (source.sections) section else ProductItem.Section.All + val order = if (source.order) order else ProductItem.Order.NAME + return when (source) { + Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable( + searchQuery, + section, + order + ) + Source.INSTALLED -> CursorOwner.Request.ProductsInstalled( + searchQuery, + section, + order + ) + Source.UPDATES -> CursorOwner.Request.ProductsUpdates(searchQuery, section, order) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return RecyclerView(requireContext()).apply { + id = android.R.id.list + layoutManager = LinearLayoutManager(context) + isMotionEventSplittingEnabled = false + isVerticalScrollBarEnabled = false + setHasFixedSize(true) + recycledViewPool.setMaxRecycledViews(ProductsAdapter.ViewType.PRODUCT.ordinal, 30) + val adapter = ProductsAdapter { screenActivity.navigateProduct(it.packageName) } + this.adapter = adapter + RecyclerFastScroller(this) + recyclerView = this + } } - if (currentSearchQuery != searchQuery || currentSection != section || currentOrder != order) { - currentSearchQuery = searchQuery - currentSection = section - currentOrder = order - recyclerView?.scrollToPosition(0) - } - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + currentSearchQuery = savedInstanceState?.getString(STATE_CURRENT_SEARCH_QUERY).orEmpty() + currentSection = + savedInstanceState?.getParcelable(STATE_CURRENT_SECTION) ?: ProductItem.Section.All + currentOrder = savedInstanceState?.getString(STATE_CURRENT_ORDER) + ?.let(ProductItem.Order::valueOf) ?: ProductItem.Order.NAME + layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER) - internal fun setSearchQuery(searchQuery: String) { - if (this.searchQuery != searchQuery) { - this.searchQuery = searchQuery - if (view != null) { screenActivity.cursorOwner.attach(this, request) - } + repositoriesDisposable = Observable.just(Unit) + .concatWith(Database.observable(Database.Subject.Repositories)) + .observeOn(Schedulers.io()) + .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } + .map { it.asSequence().map { Pair(it.id, it) }.toMap() } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { (recyclerView?.adapter as? ProductsAdapter)?.repositories = it } } - } - internal fun setSection(section: ProductItem.Section) { - if (this.section != section) { - this.section = section - if (view != null) { - screenActivity.cursorOwner.attach(this, request) - } - } - } + override fun onDestroyView() { + super.onDestroyView() - internal fun setOrder(order: ProductItem.Order) { - if (this.order != order) { - this.order = order - if (view != null) { - screenActivity.cursorOwner.attach(this, request) - } + recyclerView = null + + screenActivity.cursorOwner.detach(this) + repositoriesDisposable?.dispose() + repositoriesDisposable = null + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + outState.putString(STATE_CURRENT_SEARCH_QUERY, currentSearchQuery) + outState.putParcelable(STATE_CURRENT_SECTION, currentSection) + outState.putString(STATE_CURRENT_ORDER, currentOrder.name) + (layoutManagerState ?: recyclerView?.layoutManager?.onSaveInstanceState()) + ?.let { outState.putParcelable(STATE_LAYOUT_MANAGER, it) } + } + + override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { + (recyclerView?.adapter as? ProductsAdapter)?.apply { + this.cursor = cursor + emptyText = when { + cursor == null -> "" + searchQuery.isNotEmpty() -> getString(R.string.no_matching_applications_found) + else -> when (source) { + Source.AVAILABLE -> getString(R.string.no_applications_available) + Source.INSTALLED -> getString(R.string.no_applications_installed) + Source.UPDATES -> getString(R.string.all_applications_up_to_date) + } + } + } + + layoutManagerState?.let { + layoutManagerState = null + recyclerView?.layoutManager?.onRestoreInstanceState(it) + } + + if (currentSearchQuery != searchQuery || currentSection != section || currentOrder != order) { + currentSearchQuery = searchQuery + currentSection = section + currentOrder = order + recyclerView?.scrollToPosition(0) + } + } + + internal fun setSearchQuery(searchQuery: String) { + if (this.searchQuery != searchQuery) { + this.searchQuery = searchQuery + if (view != null) { + screenActivity.cursorOwner.attach(this, request) + } + } + } + + internal fun setSection(section: ProductItem.Section) { + if (this.section != section) { + this.section = section + if (view != null) { + screenActivity.cursorOwner.attach(this, request) + } + } + } + + internal fun setOrder(order: ProductItem.Order) { + if (this.order != order) { + this.order = order + if (view != null) { + screenActivity.cursorOwner.attach(this, request) + } + } } - } }