diff --git a/app/src/main/baseline-prof.txt b/app/src/main/baseline-prof.txt index c8b3f9c1..795055b3 100644 --- a/app/src/main/baseline-prof.txt +++ b/app/src/main/baseline-prof.txt @@ -11784,10 +11784,7 @@ PLcom/anod/appwatcher/database/AppListTable$DefaultImpls;->loadRowIds(Lcom/anod/ PLcom/anod/appwatcher/database/AppListTable$Queries;->()V PLcom/anod/appwatcher/database/AppListTable$Queries;->()V PLcom/anod/appwatcher/database/AppListTable$Queries;->changes(Lcom/anod/appwatcher/database/AppListTable;)Lkotlinx/coroutines/flow/Flow; -PLcom/anod/appwatcher/database/AppListTable$Queries;->createAppsListQuery(IZLjava/lang/Integer;Ljava/lang/String;Lcom/anod/appwatcher/database/SqlOffset;)Lkotlin/Pair; -PLcom/anod/appwatcher/database/AppListTable$Queries;->createSelection(Ljava/lang/Integer;Ljava/lang/String;Lcom/anod/appwatcher/database/SqlOffset;)Lkotlin/Pair; PLcom/anod/appwatcher/database/AppListTable$Queries;->createSortOrder(IZ)Ljava/lang/String; -PLcom/anod/appwatcher/database/AppListTable$Queries;->loadAppList(IZLjava/lang/Integer;Ljava/lang/String;Lcom/anod/appwatcher/database/SqlOffset;Lcom/anod/appwatcher/database/AppListTable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; PLcom/anod/appwatcher/database/AppListTable$loadRowIds$2;->(Lcom/anod/appwatcher/database/AppListTable;Lkotlin/coroutines/Continuation;)V PLcom/anod/appwatcher/database/AppListTable$loadRowIds$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; PLcom/anod/appwatcher/database/AppListTable$loadRowIds$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; @@ -11868,10 +11865,6 @@ PLcom/anod/appwatcher/database/DbContentProvider$Companion;->()V PLcom/anod/appwatcher/database/DbContentProvider$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V PLcom/anod/appwatcher/database/DbContentProvider$dbSchemaManager$2;->(Lcom/anod/appwatcher/database/DbContentProvider;)V PLcom/anod/appwatcher/database/SchedulesTable_Impl;->getRequiredConverters()Ljava/util/List; -PLcom/anod/appwatcher/database/SqlOffset;->()V -PLcom/anod/appwatcher/database/SqlOffset;->(II)V -PLcom/anod/appwatcher/database/SqlOffset;->getLimit()I -PLcom/anod/appwatcher/database/SqlOffset;->getOffset()I PLcom/anod/appwatcher/database/TagsTable_Impl;->-$$Nest$fget__db(Lcom/anod/appwatcher/database/TagsTable_Impl;)Landroidx/room/RoomDatabase; PLcom/anod/appwatcher/database/TagsTable_Impl;->(Landroidx/room/RoomDatabase;)V PLcom/anod/appwatcher/database/TagsTable_Impl;->getRequiredConverters()Ljava/util/List; diff --git a/app/src/main/java/com/anod/appwatcher/database/AppListTable.kt b/app/src/main/java/com/anod/appwatcher/database/AppListTable.kt index 17a464ea..d6ea03b4 100644 --- a/app/src/main/java/com/anod/appwatcher/database/AppListTable.kt +++ b/app/src/main/java/com/anod/appwatcher/database/AppListTable.kt @@ -25,8 +25,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -class SqlOffset(val offset: Int, val limit: Int) - @Dao interface AppListTable { @@ -40,7 +38,7 @@ interface AppListTable { suspend fun load(query: SupportSQLiteQuery): List @RawQuery - suspend fun count(query: SupportSQLiteQuery): Int + suspend fun loadRows(query: SupportSQLiteQuery): List @Query("SELECT * FROM $TABLE WHERE ${Columns.APP_ID} == :appId") fun observeApp(appId: String): Flow @@ -185,45 +183,36 @@ interface AppListTable { titleFilter: String, table: AppListTable ): Flow> { - val query = createAppsListQuery(sortId, orderByRecentlyUpdated, tagId, titleFilter, null) + val query = createAppsListQuery(sortId, orderByRecentlyUpdated, tagId, titleFilter) return table.observe(SimpleSQLiteQuery(query.first, query.second)) } - suspend fun loadAppList( + suspend fun loadAppListRows( sortId: Int, orderByRecentlyDiscovered: Boolean, tagId: Int?, titleFilter: String, - offset: SqlOffset?, table: AppListTable - ): List { - val query = createAppsListQuery(sortId, orderByRecentlyDiscovered, tagId, titleFilter, offset) - return table.load(SimpleSQLiteQuery(query.first, query.second)) - } - - suspend fun countAppList(tagId: Int?, titleFilter: String, table: AppListTable): Int { - val query = createAppsListCountQuery(tagId, titleFilter) - return table.count(SimpleSQLiteQuery(query.first, query.second)) + ): List { + val query = createAppsListRowsQuery(sortId, orderByRecentlyDiscovered, tagId, titleFilter) + return table.loadRows(SimpleSQLiteQuery(query.first, query.second)) } - private fun createAppsListCountQuery(tagId: Int?, titleFilter: String): Pair> { - val selection = createSelection(tagId, titleFilter, null) - val sql = - "SELECT COUNT(DISTINCT $TABLE.${BaseColumns._ID}) " + - "FROM $TABLE " + - "WHERE ${selection.first} " - return Pair(sql, selection.second) + suspend fun loadAppList(rowIds: List, table: AppListTable): List { + if (rowIds.isEmpty()) { + return emptyList() + } + val query = createAppsListRowsQuery(rowIds) + return table.load(SimpleSQLiteQuery(query)) } internal fun createAppsListQuery( sortId: Int, orderByRecentlyDiscovered: Boolean, tagId: Int?, - titleFilter: String, - offset: SqlOffset? + titleFilter: String ): Pair> { - val rangeSql = if (offset == null) "" else " LIMIT ? OFFSET ? " - val selection = createSelection(tagId, titleFilter, offset) + val selection = createSelection(tagId, titleFilter) val sql = "SELECT $TABLE.*, ${ChangelogTable.TableColumns.DETAILS}, ${ChangelogTable.TableColumns.NO_NEW_DETAILS}, " + @@ -232,11 +221,38 @@ interface AppListTable { "LEFT JOIN ${ChangelogTable.TABLE} ON ${TableColumns.APP_ID} == ${ChangelogTable.TableColumns.APP_ID} " + "AND ${TableColumns.VERSION_NUMBER} == ${ChangelogTable.TableColumns.VERSION_CODE} " + "WHERE ${selection.first} " + - "ORDER BY ${createSortOrder(sortId, orderByRecentlyDiscovered)} " + - rangeSql + "ORDER BY ${createSortOrder(sortId, orderByRecentlyDiscovered)} " + return Pair(sql, selection.second) + } + + internal fun createAppsListRowsQuery( + sortId: Int, + orderByRecentlyDiscovered: Boolean, + tagId: Int?, + titleFilter: String + ): Pair> { + val selection = createSelection(tagId, titleFilter) + val sql = + "SELECT $TABLE.${BaseColumns._ID} " + + "FROM $TABLE " + + "WHERE ${selection.first} " + + "ORDER BY ${createSortOrder(sortId, orderByRecentlyDiscovered)} " return Pair(sql, selection.second) } + private fun createAppsListRowsQuery(rowIds: List): String { + val rowIdsSql = rowIds.joinToString(",") + val orderSql = rowIds.mapIndexed { index, rowId -> "WHEN $rowId THEN $index" }.joinToString(" ") + return "SELECT $TABLE.*, ${ChangelogTable.TableColumns.DETAILS}, ${ChangelogTable.TableColumns.NO_NEW_DETAILS}, " + + "CASE WHEN ${Columns.SYNC_TIMESTAMP} > $recentTime THEN 1 ELSE 0 END ${Columns.RECENT_FLAG} " + + "FROM $TABLE " + + "LEFT JOIN ${ChangelogTable.TABLE} ON ${TableColumns.APP_ID} == ${ChangelogTable.TableColumns.APP_ID} " + + "AND ${TableColumns.VERSION_NUMBER} == ${ChangelogTable.TableColumns.VERSION_CODE} " + + "WHERE $TABLE.${BaseColumns._ID} IN ($rowIdsSql) " + + "AND ${Columns.STATUS} != ${App.STATUS_DELETED} " + + "ORDER BY CASE $TABLE.${BaseColumns._ID} $orderSql END" + } + suspend fun insert(app: App, db: AppsDatabase): Long = withContext(Dispatchers.IO) { // Skip id to apply autoincrement return@withContext db.runInTransaction(Callable { @@ -263,7 +279,7 @@ interface AppListTable { Columns.STATUS + " DESC" ) if (orderByRecentlyUpdated) { - filter.add(Columns.RECENT_FLAG + " DESC") + filter.add("CASE WHEN ${Columns.SYNC_TIMESTAMP} > $recentTime THEN 1 ELSE 0 END DESC") } when (sortId) { Preferences.SORT_NAME_DESC -> filter.add(Columns.TITLE + " COLLATE NOCASE DESC") @@ -275,9 +291,9 @@ interface AppListTable { return filter.joinToString(", ") } - private fun createSelection(tagId: Int?, titleFilter: String, offset: SqlOffset?): Pair> { + private fun createSelection(tagId: Int?, titleFilter: String): Pair> { val selc = ArrayList(3) - val args = ArrayList(5) + val args = ArrayList(3) selc.add(Columns.STATUS + " != ?") args.add(App.STATUS_DELETED.toString()) @@ -307,11 +323,6 @@ interface AppListTable { args.add("%$titleFilter%") } - if (offset != null) { - args.add(offset.limit.toString()) - args.add(offset.offset.toString()) - } - return Pair(selc.joinToString(" AND "), args.toTypedArray()) } diff --git a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListPagingSource.kt b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListPagingSource.kt index d330c434..e045388c 100644 --- a/app/src/main/java/com/anod/appwatcher/watchlist/WatchListPagingSource.kt +++ b/app/src/main/java/com/anod/appwatcher/watchlist/WatchListPagingSource.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.Immutable import androidx.paging.PagingState import com.anod.appwatcher.database.AppListTable import com.anod.appwatcher.database.AppsDatabase -import com.anod.appwatcher.database.SqlOffset import com.anod.appwatcher.database.entities.AppListItem import com.anod.appwatcher.database.entities.packageToApp import com.anod.appwatcher.installed.InstalledTaskWorker @@ -16,6 +15,8 @@ import com.anod.appwatcher.preferences.Preferences import info.anodsplace.applog.AppLog import info.anodsplace.framework.content.InstalledApps import kotlin.math.max +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock class WatchListPagingSource( private val config: Config, @@ -25,7 +26,15 @@ class WatchListPagingSource( private val installedApps: InstalledApps, ) : FilterablePagingSource() { override var filterQuery: String = "" + set(value) { + if (field != value) { + rowIdSnapshot = null + } + field = value + } private val itemFilter: AppListFilter = createFilter(config.filterId) + private val rowIdSnapshotMutex = Mutex() + private var rowIdSnapshot: List? = null @Immutable data class Config(val filterId: Int, val tagId: Int?, val showRecentlyDiscovered: Boolean, val showOnDevice: Boolean, val showRecentlyInstalled: Boolean,) @@ -53,11 +62,15 @@ class WatchListPagingSource( limit = max(0, limit) } - val data = AppListTable.Queries.loadAppList( - sortId, config.showRecentlyDiscovered, config.tagId, filterQuery, SqlOffset(offset, limit), database.apps() - ) + val rowIds = loadRowIdSnapshot(sortId) + val pageRowIds = if (offset >= rowIds.size) { + emptyList() + } else { + rowIds.subList(offset, minOf(rowIds.size, offset + limit)) + } + val data = AppListTable.Queries.loadAppList(pageRowIds, database.apps()) val filtered = data.filter { !itemFilter.filterRecord(it) } - var totalItems = countTotalItems() + var totalItems = countTotalItems(hasMissingSnapshotRows = data.size < pageRowIds.size) items.addAll(filtered.map { SectionItem.App( @@ -67,7 +80,7 @@ class WatchListPagingSource( ) }) - if (config.showOnDevice && data.size < limit) { + if (config.showOnDevice && pageRowIds.size < limit) { items.addAll(loadOnDeviceItems(sortId)) } @@ -80,7 +93,7 @@ class WatchListPagingSource( key = params.key, offset = offset, loadSize = params.loadSize, - loadedDataSize = data.size, + loadedDataSize = pageRowIds.size, limit = limit ) val itemsBefore = if (totalItems == LoadResult.Page.COUNT_UNDEFINED) { @@ -104,12 +117,34 @@ class WatchListPagingSource( return page } - private suspend fun countTotalItems(): Int { - if (config.filterId != Filters.ALL || config.showOnDevice) { + private fun countTotalItems(hasMissingSnapshotRows: Boolean): Int { + if (config.filterId != Filters.ALL || config.showOnDevice || hasMissingSnapshotRows) { return LoadResult.Page.COUNT_UNDEFINED } - val appsCount = AppListTable.Queries.countAppList(config.tagId, filterQuery, database.apps()) - return appsCount + if (config.showRecentlyInstalled) 1 else 0 + return (rowIdSnapshot?.size ?: 0) + if (config.showRecentlyInstalled) 1 else 0 + } + + private suspend fun loadRowIdSnapshot(sortId: Int): List { + val existingSnapshot = rowIdSnapshot + if (existingSnapshot != null) { + return existingSnapshot + } + return rowIdSnapshotMutex.withLock { + val lockedSnapshot = rowIdSnapshot + if (lockedSnapshot != null) { + lockedSnapshot + } else { + AppListTable.Queries.loadAppListRows( + sortId, + config.showRecentlyDiscovered, + config.tagId, + filterQuery, + database.apps() + ).also { + rowIdSnapshot = it + } + } + } } private suspend fun loadOnDeviceItems(sortId: Int): List { diff --git a/app/src/release/generated/baselineProfiles/baseline-prof.txt b/app/src/release/generated/baselineProfiles/baseline-prof.txt index 2efeff05..29287107 100644 --- a/app/src/release/generated/baselineProfiles/baseline-prof.txt +++ b/app/src/release/generated/baselineProfiles/baseline-prof.txt @@ -25309,12 +25309,7 @@ Lcom/anod/appwatcher/database/AppListTable$Queries; SPLcom/anod/appwatcher/database/AppListTable$Queries;->()V SPLcom/anod/appwatcher/database/AppListTable$Queries;->()V SPLcom/anod/appwatcher/database/AppListTable$Queries;->changes(Lcom/anod/appwatcher/database/AppListTable;)Lkotlinx/coroutines/flow/Flow; -SPLcom/anod/appwatcher/database/AppListTable$Queries;->countAppList(Ljava/lang/Integer;Ljava/lang/String;Lcom/anod/appwatcher/database/AppListTable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -SPLcom/anod/appwatcher/database/AppListTable$Queries;->createAppsListCountQuery(Ljava/lang/Integer;Ljava/lang/String;)Lkotlin/Pair; -SPLcom/anod/appwatcher/database/AppListTable$Queries;->createAppsListQuery$app(IZLjava/lang/Integer;Ljava/lang/String;Lcom/anod/appwatcher/database/SqlOffset;)Lkotlin/Pair; -SPLcom/anod/appwatcher/database/AppListTable$Queries;->createSelection(Ljava/lang/Integer;Ljava/lang/String;Lcom/anod/appwatcher/database/SqlOffset;)Lkotlin/Pair; SPLcom/anod/appwatcher/database/AppListTable$Queries;->createSortOrder(IZ)Ljava/lang/String; -SPLcom/anod/appwatcher/database/AppListTable$Queries;->loadAppList(IZLjava/lang/Integer;Ljava/lang/String;Lcom/anod/appwatcher/database/SqlOffset;Lcom/anod/appwatcher/database/AppListTable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lcom/anod/appwatcher/database/AppListTable$loadRowIds$2; SPLcom/anod/appwatcher/database/AppListTable$loadRowIds$2;->(Lcom/anod/appwatcher/database/AppListTable;Lkotlin/coroutines/Continuation;)V SPLcom/anod/appwatcher/database/AppListTable$loadRowIds$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -25326,8 +25321,6 @@ SPLcom/anod/appwatcher/database/AppListTable_Impl;->()V SPLcom/anod/appwatcher/database/AppListTable_Impl;->(Landroidx/room/RoomDatabase;)V SPLcom/anod/appwatcher/database/AppListTable_Impl;->_loadRowIds$lambda$0(Ljava/lang/String;Ljava/util/List;Landroidx/sqlite/SQLiteConnection;)Ljava/util/List; SPLcom/anod/appwatcher/database/AppListTable_Impl;->_loadRowIds(Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -SPLcom/anod/appwatcher/database/AppListTable_Impl;->count$lambda$1(Ljava/lang/String;Landroidx/room/RoomRawQuery;Landroidx/sqlite/SQLiteConnection;)I -SPLcom/anod/appwatcher/database/AppListTable_Impl;->count(Landroidx/sqlite/db/SupportSQLiteQuery;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; SPLcom/anod/appwatcher/database/AppListTable_Impl;->load$lambda$0(Ljava/lang/String;Landroidx/room/RoomRawQuery;Landroidx/sqlite/SQLiteConnection;)Ljava/util/List; SPLcom/anod/appwatcher/database/AppListTable_Impl;->load(Landroidx/sqlite/db/SupportSQLiteQuery;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; SPLcom/anod/appwatcher/database/AppListTable_Impl;->loadRowIds(Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -25468,11 +25461,6 @@ Lcom/anod/appwatcher/database/SchedulesTable_Impl$Companion; SPLcom/anod/appwatcher/database/SchedulesTable_Impl$Companion;->()V SPLcom/anod/appwatcher/database/SchedulesTable_Impl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V SPLcom/anod/appwatcher/database/SchedulesTable_Impl$Companion;->getRequiredConverters()Ljava/util/List; -Lcom/anod/appwatcher/database/SqlOffset; -SPLcom/anod/appwatcher/database/SqlOffset;->()V -SPLcom/anod/appwatcher/database/SqlOffset;->(II)V -SPLcom/anod/appwatcher/database/SqlOffset;->getLimit()I -SPLcom/anod/appwatcher/database/SqlOffset;->getOffset()I Lcom/anod/appwatcher/database/TagsTable; Lcom/anod/appwatcher/database/TagsTable_Impl; SPLcom/anod/appwatcher/database/TagsTable_Impl;->()V diff --git a/app/src/release/generated/baselineProfiles/startup-prof.txt b/app/src/release/generated/baselineProfiles/startup-prof.txt index 2efeff05..29287107 100644 --- a/app/src/release/generated/baselineProfiles/startup-prof.txt +++ b/app/src/release/generated/baselineProfiles/startup-prof.txt @@ -25309,12 +25309,7 @@ Lcom/anod/appwatcher/database/AppListTable$Queries; SPLcom/anod/appwatcher/database/AppListTable$Queries;->()V SPLcom/anod/appwatcher/database/AppListTable$Queries;->()V SPLcom/anod/appwatcher/database/AppListTable$Queries;->changes(Lcom/anod/appwatcher/database/AppListTable;)Lkotlinx/coroutines/flow/Flow; -SPLcom/anod/appwatcher/database/AppListTable$Queries;->countAppList(Ljava/lang/Integer;Ljava/lang/String;Lcom/anod/appwatcher/database/AppListTable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -SPLcom/anod/appwatcher/database/AppListTable$Queries;->createAppsListCountQuery(Ljava/lang/Integer;Ljava/lang/String;)Lkotlin/Pair; -SPLcom/anod/appwatcher/database/AppListTable$Queries;->createAppsListQuery$app(IZLjava/lang/Integer;Ljava/lang/String;Lcom/anod/appwatcher/database/SqlOffset;)Lkotlin/Pair; -SPLcom/anod/appwatcher/database/AppListTable$Queries;->createSelection(Ljava/lang/Integer;Ljava/lang/String;Lcom/anod/appwatcher/database/SqlOffset;)Lkotlin/Pair; SPLcom/anod/appwatcher/database/AppListTable$Queries;->createSortOrder(IZ)Ljava/lang/String; -SPLcom/anod/appwatcher/database/AppListTable$Queries;->loadAppList(IZLjava/lang/Integer;Ljava/lang/String;Lcom/anod/appwatcher/database/SqlOffset;Lcom/anod/appwatcher/database/AppListTable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lcom/anod/appwatcher/database/AppListTable$loadRowIds$2; SPLcom/anod/appwatcher/database/AppListTable$loadRowIds$2;->(Lcom/anod/appwatcher/database/AppListTable;Lkotlin/coroutines/Continuation;)V SPLcom/anod/appwatcher/database/AppListTable$loadRowIds$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -25326,8 +25321,6 @@ SPLcom/anod/appwatcher/database/AppListTable_Impl;->()V SPLcom/anod/appwatcher/database/AppListTable_Impl;->(Landroidx/room/RoomDatabase;)V SPLcom/anod/appwatcher/database/AppListTable_Impl;->_loadRowIds$lambda$0(Ljava/lang/String;Ljava/util/List;Landroidx/sqlite/SQLiteConnection;)Ljava/util/List; SPLcom/anod/appwatcher/database/AppListTable_Impl;->_loadRowIds(Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -SPLcom/anod/appwatcher/database/AppListTable_Impl;->count$lambda$1(Ljava/lang/String;Landroidx/room/RoomRawQuery;Landroidx/sqlite/SQLiteConnection;)I -SPLcom/anod/appwatcher/database/AppListTable_Impl;->count(Landroidx/sqlite/db/SupportSQLiteQuery;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; SPLcom/anod/appwatcher/database/AppListTable_Impl;->load$lambda$0(Ljava/lang/String;Landroidx/room/RoomRawQuery;Landroidx/sqlite/SQLiteConnection;)Ljava/util/List; SPLcom/anod/appwatcher/database/AppListTable_Impl;->load(Landroidx/sqlite/db/SupportSQLiteQuery;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; SPLcom/anod/appwatcher/database/AppListTable_Impl;->loadRowIds(Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -25468,11 +25461,6 @@ Lcom/anod/appwatcher/database/SchedulesTable_Impl$Companion; SPLcom/anod/appwatcher/database/SchedulesTable_Impl$Companion;->()V SPLcom/anod/appwatcher/database/SchedulesTable_Impl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V SPLcom/anod/appwatcher/database/SchedulesTable_Impl$Companion;->getRequiredConverters()Ljava/util/List; -Lcom/anod/appwatcher/database/SqlOffset; -SPLcom/anod/appwatcher/database/SqlOffset;->()V -SPLcom/anod/appwatcher/database/SqlOffset;->(II)V -SPLcom/anod/appwatcher/database/SqlOffset;->getLimit()I -SPLcom/anod/appwatcher/database/SqlOffset;->getOffset()I Lcom/anod/appwatcher/database/TagsTable; Lcom/anod/appwatcher/database/TagsTable_Impl; SPLcom/anod/appwatcher/database/TagsTable_Impl;->()V diff --git a/app/src/test/java/com/anod/appwatcher/database/AppListTableQueriesTest.kt b/app/src/test/java/com/anod/appwatcher/database/AppListTableQueriesTest.kt index ec352273..1845096d 100644 --- a/app/src/test/java/com/anod/appwatcher/database/AppListTableQueriesTest.kt +++ b/app/src/test/java/com/anod/appwatcher/database/AppListTableQueriesTest.kt @@ -16,14 +16,15 @@ class AppListTableQueriesTest { sortId = Preferences.SORT_NAME_ASC, orderByRecentlyDiscovered = false, tagId = null, - titleFilter = "", - offset = SqlOffset(offset = 0, limit = 20) + titleFilter = "" ) assertTrue(sql.contains("LEFT JOIN changelog ON app_list.app_id == changelog.app_id")) assertTrue(sql.contains("AND app_list.ver_num == changelog.code")) assertFalse(sql.contains("SELECT MAX(")) - assertEquals(listOf(App.STATUS_DELETED.toString(), "20", "0"), args.toList()) + assertFalse(sql.contains("LIMIT")) + assertFalse(sql.contains("OFFSET")) + assertEquals(listOf(App.STATUS_DELETED.toString()), args.toList()) } @Test @@ -33,8 +34,7 @@ class AppListTableQueriesTest { sortId = Preferences.SORT_NAME_ASC, orderByRecentlyDiscovered = false, tagId = tagId, - titleFilter = "", - offset = null + titleFilter = "" ) assertFalse(sql.contains("INNER JOIN app_tags")) @@ -42,14 +42,28 @@ class AppListTableQueriesTest { assertEquals(listOf(App.STATUS_DELETED.toString(), tagId.toString()), args.toList()) } + @Test + fun `row snapshot query can order by recently discovered`() { + val (sql, args) = AppListTable.Queries.createAppsListRowsQuery( + sortId = Preferences.SORT_NAME_ASC, + orderByRecentlyDiscovered = true, + tagId = null, + titleFilter = "" + ) + + assertTrue(sql.contains("CASE WHEN sync_version >")) + assertTrue(sql.contains("THEN 1 ELSE 0 END DESC")) + assertFalse(sql.contains("recent_flag DESC")) + assertEquals(listOf(App.STATUS_DELETED.toString()), args.toList()) + } + @Test fun `untagged list query uses not exists`() { val (sql, args) = AppListTable.Queries.createAppsListQuery( sortId = Preferences.SORT_NAME_ASC, orderByRecentlyDiscovered = false, tagId = Tag.empty.id, - titleFilter = "", - offset = null + titleFilter = "" ) assertFalse(sql.contains("LEFT JOIN app_tags")) diff --git a/app/src/test/java/com/anod/appwatcher/database/AppListTableRoomTest.kt b/app/src/test/java/com/anod/appwatcher/database/AppListTableRoomTest.kt index 1eb663e0..e2bd0760 100644 --- a/app/src/test/java/com/anod/appwatcher/database/AppListTableRoomTest.kt +++ b/app/src/test/java/com/anod/appwatcher/database/AppListTableRoomTest.kt @@ -52,11 +52,6 @@ class AppListTableRoomTest { assertEquals(listOf("alpha"), loadIds(tagId = null, titleFilter = "Alp")) assertEquals(listOf("beta"), loadIds(tagId = 2, titleFilter = "Bet")) assertEquals(listOf("gamma"), loadIds(tagId = Tag.empty.id, titleFilter = "Gam")) - - assertEquals(3, count(tagId = null, titleFilter = "")) - assertEquals(1, count(tagId = 1, titleFilter = "")) - assertEquals(2, count(tagId = 2, titleFilter = "")) - assertEquals(1, count(tagId = Tag.empty.id, titleFilter = "")) } private suspend fun insertApp(appId: String, title: String, status: Int = App.STATUS_NORMAL) { @@ -96,20 +91,17 @@ class AppListTableRoomTest { ) } - private suspend fun loadIds(tagId: Int?, titleFilter: String): List = - AppListTable.Queries.loadAppList( + private suspend fun loadIds(tagId: Int?, titleFilter: String): List { + val rowIds = AppListTable.Queries.loadAppListRows( sortId = Preferences.SORT_NAME_ASC, orderByRecentlyDiscovered = false, - tagId = tagId, - titleFilter = titleFilter, - offset = null, - table = db.apps() - ).map { item -> item.app.appId } - - private suspend fun count(tagId: Int?, titleFilter: String): Int = - AppListTable.Queries.countAppList( tagId = tagId, titleFilter = titleFilter, table = db.apps() ) + return AppListTable.Queries.loadAppList( + rowIds = rowIds, + table = db.apps() + ).map { item -> item.app.appId } + } } \ No newline at end of file diff --git a/app/src/test/java/com/anod/appwatcher/watchlist/WatchListPagingSourceRoomTest.kt b/app/src/test/java/com/anod/appwatcher/watchlist/WatchListPagingSourceRoomTest.kt index 108a8fdf..e164037d 100644 --- a/app/src/test/java/com/anod/appwatcher/watchlist/WatchListPagingSourceRoomTest.kt +++ b/app/src/test/java/com/anod/appwatcher/watchlist/WatchListPagingSourceRoomTest.kt @@ -95,6 +95,71 @@ class WatchListPagingSourceRoomTest { assertEquals(null, secondPage.nextKey) } + @Test + fun pagingSourceKeepsStableRowsWhenStatusChangesMoveItemsAcrossOffsets() = runBlocking { + repeat(10) { index -> + insertApp( + appId = "updated-$index", + packageName = "updated.$index", + title = "Zzz Moved Later $index", + status = App.STATUS_UPDATED + ) + } + repeat(100) { index -> + insertApp(appId = "normal-$index", packageName = "normal.$index", title = "Normal $index") + } + val pagingSource = createPagingSource(showOnDevice = false) + + val firstResult = pagingSource.load(PagingSource.LoadParams.Refresh(key = null, loadSize = 60, placeholdersEnabled = false)) + val firstPage = firstResult as PagingSource.LoadResult.Page + firstPage.data + .filterIsInstance() + .filter { it.appListItem.app.status == App.STATUS_UPDATED } + .take(5) + .forEach { + db.apps().updateStatus(it.appListItem.app.rowId, App.STATUS_NORMAL) + } + + val pages = mutableListOf(firstPage) + var nextKey = firstPage.nextKey + while (nextKey != null) { + val result = pagingSource.load(PagingSource.LoadParams.Append(key = nextKey, loadSize = 20, placeholdersEnabled = false)) + val page = result as PagingSource.LoadResult.Page + pages.add(page) + nextKey = page.nextKey + } + val items = pages.flatMap { it.data }.filterIsInstance() + + assertEquals(110, items.size) + assertEquals(items.map { it.sectionKey }.toSet().size, items.size) + } + + @Test + fun pagingSourceDoesNotRenderRowsDeletedAfterSnapshot() = runBlocking { + repeat(40) { index -> + insertApp(appId = "app-$index", packageName = "app.$index", title = "App $index") + } + val pagingSource = createPagingSource(showOnDevice = false) + + val firstResult = pagingSource.load(PagingSource.LoadParams.Refresh(key = null, loadSize = 20, placeholdersEnabled = false)) + val firstPage = firstResult as PagingSource.LoadResult.Page + val secondPageRow = AppListTable.Queries.loadAppListRows( + sortId = Preferences.SORT_NAME_ASC, + orderByRecentlyDiscovered = false, + tagId = null, + titleFilter = "", + table = db.apps() + )[20] + db.apps().updateStatus(secondPageRow, App.STATUS_DELETED) + + val secondResult = pagingSource.load(PagingSource.LoadParams.Append(key = firstPage.nextKey!!, loadSize = 20, placeholdersEnabled = false)) + val secondPage = secondResult as PagingSource.LoadResult.Page + + assertTrue(secondPage.data.filterIsInstance().none { it.appListItem.app.rowId == secondPageRow }) + assertEquals(PagingSource.LoadResult.Page.COUNT_UNDEFINED, secondPage.itemsBefore) + assertEquals(PagingSource.LoadResult.Page.COUNT_UNDEFINED, secondPage.itemsAfter) + } + private fun createPagingSource(showOnDevice: Boolean) = WatchListPagingSource( config = WatchListPagingSource.Config( filterId = Filters.ALL, @@ -119,7 +184,12 @@ class WatchListPagingSourceRoomTest { ) ) - private suspend fun insertApp(appId: String, packageName: String, title: String) { + private suspend fun insertApp( + appId: String, + packageName: String, + title: String, + status: Int = App.STATUS_NORMAL + ) { AppListTable.Queries.insert( App( rowId = 0, @@ -130,7 +200,7 @@ class WatchListPagingSourceRoomTest { title = title, creator = "creator", iconUrl = "", - status = App.STATUS_NORMAL, + status = status, uploadDate = "", price = Price(text = "", cur = "", micros = 0), detailsUrl = null,