diff --git a/src/main/kotlin/com/looker/droidify/database/DAOs.kt b/src/main/kotlin/com/looker/droidify/database/DAOs.kt index da2ab7b3..cd4fb593 100644 --- a/src/main/kotlin/com/looker/droidify/database/DAOs.kt +++ b/src/main/kotlin/com/looker/droidify/database/DAOs.kt @@ -171,6 +171,97 @@ interface ProductDao : BaseDao { return query(SimpleSQLiteQuery(builder.build())) } + + @RawQuery + fun queryList( + query: SupportSQLiteQuery + ): List + + // TODO optimize and simplify + @Transaction + fun queryList( + installed: Boolean, updates: Boolean, searchQuery: String, + section: ProductItem.Section, order: ProductItem.Order + ): List { + val builder = QueryBuilder() + + val signatureMatches = """installed.${ROW_SIGNATURE} IS NOT NULL AND + product.${ROW_SIGNATURES} LIKE ('%.' || installed.${ROW_SIGNATURE} || '.%') AND + product.${ROW_SIGNATURES} != ''""" + + builder += """SELECT product.rowid AS _id, product.${ROW_REPOSITORY_ID}, + product.${ROW_PACKAGE_NAME}, product.${ROW_NAME}, + product.${ROW_SUMMARY}, installed.${ROW_VERSION}, + (COALESCE(lock.${ROW_VERSION_CODE}, -1) NOT IN (0, product.${ROW_VERSION_CODE}) AND + product.${ROW_COMPATIBLE} != 0 AND product.${ROW_VERSION_CODE} > + COALESCE(installed.${ROW_VERSION_CODE}, 0xffffffff) AND $signatureMatches) + AS ${ROW_CAN_UPDATE}, product.${ROW_COMPATIBLE}, + product.${ROW_DATA_ITEM},""" + + if (searchQuery.isNotEmpty()) { + builder += """(((product.${ROW_NAME} LIKE ? OR + product.${ROW_SUMMARY} LIKE ?) * 7) | + ((product.${ROW_PACKAGE_NAME} LIKE ?) * 3) | + (product.${ROW_DESCRIPTION} LIKE ?)) AS ${ROW_MATCH_RANK},""" + builder %= List(4) { "%$searchQuery%" } + } else { + builder += "0 AS ${ROW_MATCH_RANK}," + } + + builder += """MAX((product.${ROW_COMPATIBLE} AND + (installed.${ROW_SIGNATURE} IS NULL OR $signatureMatches)) || + PRINTF('%016X', product.${ROW_VERSION_CODE})) FROM $ROW_PRODUCT_NAME AS product""" + builder += """JOIN $ROW_REPOSITORY_NAME AS repository + ON product.${ROW_REPOSITORY_ID} = repository.${ROW_ID}""" + builder += """LEFT JOIN $ROW_LOCK_NAME AS lock + ON product.${ROW_PACKAGE_NAME} = lock.${ROW_PACKAGE_NAME}""" + + if (!installed && !updates) { + builder += "LEFT" + } + builder += """JOIN $ROW_INSTALLED_NAME AS installed + ON product.${ROW_PACKAGE_NAME} = installed.${ROW_PACKAGE_NAME}""" + + if (section is ProductItem.Section.Category) { + builder += """JOIN $ROW_CATEGORY_NAME AS category + ON product.${ROW_PACKAGE_NAME} = category.${ROW_PACKAGE_NAME}""" + } + + builder += """WHERE repository.${ROW_ENABLED} != 0 AND + repository.${ROW_DELETED} == 0""" + + if (section is ProductItem.Section.Category) { + builder += "AND category.${ROW_NAME} = ?" + builder %= section.name + } else if (section is ProductItem.Section.Repository) { + builder += "AND product.${ROW_REPOSITORY_ID} = ?" + builder %= section.id.toString() + } + + if (searchQuery.isNotEmpty()) { + builder += """AND $ROW_MATCH_RANK > 0""" + } + + builder += "GROUP BY product.${ROW_PACKAGE_NAME} HAVING 1" + + if (updates) { + builder += "AND $ROW_CAN_UPDATE" + } + builder += "ORDER BY" + + if (searchQuery.isNotEmpty()) { + builder += """$ROW_MATCH_RANK DESC,""" + } + + when (order) { + ProductItem.Order.NAME -> Unit + ProductItem.Order.DATE_ADDED -> builder += "product.${ROW_ADDED} DESC," + ProductItem.Order.LAST_UPDATE -> builder += "product.${ROW_UPDATED} DESC," + }::class + builder += "product.${ROW_NAME} COLLATE LOCALIZED ASC" + + return queryList(SimpleSQLiteQuery(builder.build())) + } } @Dao diff --git a/src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt b/src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt index 4cd78275..09503f4b 100644 --- a/src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt +++ b/src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt @@ -234,35 +234,6 @@ class MainActivityX : AppCompatActivity(), LoaderManager.LoaderCallbacks syncConnection.binder?.setUpdateNotificationBlocker(blockerFragment) } - fun attachCursorOwner(callback: CursorOwner.Callback, request: CursorOwner.Request) { - val oldActiveRequest = viewModel.activeRequests[request.id] - if (oldActiveRequest?.callback != null && - oldActiveRequest.callback != callback && oldActiveRequest.cursor != null - ) { - oldActiveRequest.callback.onCursorData(oldActiveRequest.request, null) - } - val cursor = if (oldActiveRequest?.request == request && oldActiveRequest.cursor != null) { - callback.onCursorData(request, oldActiveRequest.cursor) - oldActiveRequest.cursor - } else { - null - } - viewModel.activeRequests[request.id] = CursorOwner.ActiveRequest(request, callback, cursor) - if (cursor == null) { - LoaderManager.getInstance(this).restartLoader(request.id, null, this) - } - } - - - fun detachCursorOwner(callback: CursorOwner.Callback) { - for (id in viewModel.activeRequests.keys) { - val activeRequest = viewModel.activeRequests[id]!! - if (activeRequest.callback == callback) { - viewModel.activeRequests[id] = activeRequest.copy(callback = null) - } - } - } - override fun onCreateLoader(id: Int, args: Bundle?): Loader { val request = viewModel.activeRequests[id]!!.request return QueryLoader(this) { diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/ExploreFragment.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/ExploreFragment.kt index 215aea47..dc07584a 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/ExploreFragment.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/ExploreFragment.kt @@ -59,7 +59,7 @@ class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mainActivityX.attachCursorOwner(this, viewModel.request(source)) + viewModel.fillList(source) viewModel.db.repositoryDao.allFlowable .observeOn(Schedulers.io()) .flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } } @@ -68,12 +68,6 @@ class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback { .subscribe { repositories = it } } - override fun onDestroyView() { - super.onDestroyView() - - mainActivityX.detachCursorOwner(this) - } - override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { (binding.recyclerView.adapter as? AppListAdapter)?.apply { this.cursor = cursor diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/InstalledFragment.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/InstalledFragment.kt index bb51af66..f09a3440 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/InstalledFragment.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/InstalledFragment.kt @@ -72,7 +72,7 @@ class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mainActivityX.attachCursorOwner(this, viewModel.request(source)) + viewModel.fillList(source) viewModel.db.repositoryDao.allFlowable .observeOn(Schedulers.io()) .flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } } @@ -81,12 +81,6 @@ class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback { .subscribe { repositories = it } } - override fun onDestroyView() { - super.onDestroyView() - - mainActivityX.detachCursorOwner(this) - } - override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { // TODO get a list instead of the cursor // TODO use LiveData and observers instead of listeners diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/LatestFragment.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/LatestFragment.kt index f0cd127b..df7cc7b3 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/LatestFragment.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/LatestFragment.kt @@ -72,7 +72,7 @@ class LatestFragment : MainNavFragmentX(), CursorOwner.Callback { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mainActivityX.attachCursorOwner(this, viewModel.request(source)) + viewModel.fillList(source) viewModel.db.repositoryDao.allFlowable .observeOn(Schedulers.io()) .flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } } @@ -81,12 +81,6 @@ class LatestFragment : MainNavFragmentX(), CursorOwner.Callback { .subscribe { repositories = it } } - override fun onDestroyView() { - super.onDestroyView() - - mainActivityX.detachCursorOwner(this) - } - override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { // TODO get a list instead of the cursor // TODO use LiveData and observers instead of listeners diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/MainNavFragmentX.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/MainNavFragmentX.kt index 56686390..0b25f808 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/MainNavFragmentX.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/MainNavFragmentX.kt @@ -18,7 +18,7 @@ abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback { internal fun setSearchQuery(searchQuery: String) { viewModel.setSearchQuery(searchQuery) { if (view != null) { - mainActivityX.attachCursorOwner(this, viewModel.request(source)) + viewModel.fillList(source) } } } @@ -26,7 +26,7 @@ abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback { internal fun setSection(section: ProductItem.Section) { viewModel.setSection(section) { if (view != null) { - mainActivityX.attachCursorOwner(this, viewModel.request(source)) + viewModel.fillList(source) } } } @@ -34,7 +34,7 @@ abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback { internal fun setOrder(order: ProductItem.Order) { viewModel.setOrder(order) { if (view != null) { - mainActivityX.attachCursorOwner(this, viewModel.request(source)) + viewModel.fillList(source) } } } @@ -44,4 +44,37 @@ enum class Source(val titleResId: Int, val sections: Boolean, val order: Boolean AVAILABLE(R.string.available, true, true), INSTALLED(R.string.installed, false, true), UPDATES(R.string.updates, false, false) +} + +sealed class Request { + internal abstract val id: Int + + data class ProductsAvailable( + val searchQuery: String, val section: ProductItem.Section, + val order: ProductItem.Order, + ) : Request() { + override val id: Int + get() = 1 + } + + data class ProductsInstalled( + val searchQuery: String, val section: ProductItem.Section, + val order: ProductItem.Order, + ) : Request() { + override val id: Int + get() = 2 + } + + data class ProductsUpdates( + val searchQuery: String, val section: ProductItem.Section, + val order: ProductItem.Order, + ) : Request() { + override val id: Int + get() = 3 + } + + object Repositories : Request() { + override val id: Int + get() = 4 + } } \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainNavFragmentViewModelX.kt b/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainNavFragmentViewModelX.kt index ad6e8457..6d463a91 100644 --- a/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainNavFragmentViewModelX.kt +++ b/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainNavFragmentViewModelX.kt @@ -1,11 +1,13 @@ package com.looker.droidify.ui.viewmodels +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.looker.droidify.database.CursorOwner import com.looker.droidify.database.DatabaseX +import com.looker.droidify.database.Product import com.looker.droidify.entity.ProductItem +import com.looker.droidify.ui.fragments.Request import com.looker.droidify.ui.fragments.Source import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -13,6 +15,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class MainNavFragmentViewModelX(val db: DatabaseX) : ViewModel() { @@ -37,7 +40,7 @@ class MainNavFragmentViewModelX(val db: DatabaseX) : ViewModel() { started = SharingStarted.WhileSubscribed(5000) ) - fun request(source: Source): CursorOwner.Request { + fun request(source: Source): Request { var mSearchQuery = "" var mSections: ProductItem.Section = ProductItem.Section.All var mOrder: ProductItem.Order = ProductItem.Order.NAME @@ -47,17 +50,17 @@ class MainNavFragmentViewModelX(val db: DatabaseX) : ViewModel() { launch { order.collect { if (source.order) mOrder = it } } } return when (source) { - Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable( + Source.AVAILABLE -> Request.ProductsAvailable( mSearchQuery, mSections, mOrder ) - Source.INSTALLED -> CursorOwner.Request.ProductsInstalled( + Source.INSTALLED -> Request.ProductsInstalled( mSearchQuery, mSections, mOrder ) - Source.UPDATES -> CursorOwner.Request.ProductsUpdates( + Source.UPDATES -> Request.ProductsUpdates( mSearchQuery, mSections, mOrder @@ -65,6 +68,46 @@ class MainNavFragmentViewModelX(val db: DatabaseX) : ViewModel() { } } + var productsList = MediatorLiveData>() + + fun fillList(source: Source) { + viewModelScope.launch { + productsList.value = query(request(source))?.toMutableList() + } + } + + private suspend fun query(request: Request): List? { + return withContext(Dispatchers.IO) { + when (request) { + is Request.ProductsAvailable -> db.productDao + .queryList( + installed = false, + updates = false, + searchQuery = request.searchQuery, + section = request.section, + order = request.order + ) + is Request.ProductsInstalled -> db.productDao + .queryList( + installed = true, + updates = false, + searchQuery = request.searchQuery, + section = request.section, + order = request.order + ) + is Request.ProductsUpdates -> db.productDao + .queryList( + installed = true, + updates = true, + searchQuery = request.searchQuery, + section = request.section, + order = request.order + ) + else -> listOf() + } + } + } + fun setSection(newSection: ProductItem.Section, perform: () -> Unit) { viewModelScope.launch { if (newSection != sections.value) {