@@ -4,10 +4,27 @@ import androidx.room.Dao
44import androidx.room.Insert
55import androidx.room.OnConflictStrategy
66import androidx.room.Query
7+ import androidx.room.Transaction
78import androidx.room.Update
89import app.gamenative.data.SteamApp
910import app.gamenative.service.SteamService.Companion.INVALID_PKG_ID
11+ import kotlinx.coroutines.ExperimentalCoroutinesApi
1012import kotlinx.coroutines.flow.Flow
13+ import kotlinx.coroutines.flow.distinctUntilChanged
14+ import kotlinx.coroutines.flow.flatMapLatest
15+ import kotlinx.coroutines.flow.flow
16+
17+ private const val OWNED_APPS_WHERE =
18+ " WHERE app.id != 480 " + // Actively filter out Spacewar
19+ " AND app.package_id != :invalidPkgId " +
20+ " AND app.type != 0 " +
21+ " AND EXISTS (" +
22+ " SELECT 1 FROM steam_license AS license " +
23+ " WHERE license.packageId = app.package_id " +
24+ " AND (license.license_flags & 8 = 0) " + // exclude expired licenses (e.g. free weekends)
25+ " ) "
26+
27+ private const val PAGE_SIZE = 50
1128
1229@Dao
1330interface SteamAppDao {
@@ -21,21 +38,58 @@ interface SteamAppDao {
2138 @Update
2239 suspend fun update (app : SteamApp )
2340
41+ // observe change count — triggers re-load without pulling all blobs into one CursorWindow
42+ @Query(
43+ " SELECT COUNT(*) FROM steam_app AS app " + OWNED_APPS_WHERE ,
44+ )
45+ fun _observeOwnedAppCount (
46+ invalidPkgId : Int = INVALID_PKG_ID ,
47+ ): Flow <Int >
48+
49+ // paged data load — each page fits comfortably in a CursorWindow
2450 @Query(
25- " SELECT * FROM steam_app AS app " +
26- " WHERE app.id != 480 " + // Actively filter out Spacewar
27- " AND app.package_id != :invalidPkgId " +
28- " AND app.type != 0 " +
29- " AND EXISTS (" +
30- " SELECT 1 FROM steam_license AS license " +
31- " WHERE license.packageId = app.package_id " +
32- " AND (license.license_flags & 8 = 0) " + // exclude expired licenses (e.g. free weekends)
33- " ) " +
34- " ORDER BY LOWER(app.name)" ,
51+ " SELECT * FROM steam_app AS app " + OWNED_APPS_WHERE +
52+ " ORDER BY LOWER(app.name), app.id LIMIT :limit OFFSET :offset" ,
3553 )
54+ suspend fun _getOwnedAppsPage (
55+ limit : Int ,
56+ offset : Int ,
57+ invalidPkgId : Int = INVALID_PKG_ID ,
58+ ): List <SteamApp >
59+
60+ @Transaction
61+ suspend fun _getAllOwnedAppsPaged (invalidPkgId : Int = INVALID_PKG_ID ): List <SteamApp > {
62+ val result = mutableListOf<SteamApp >()
63+ var offset = 0
64+ while (true ) {
65+ // reset per-offset: try full fetch on first page, PAGE_SIZE thereafter
66+ var pageSize = if (offset == 0 ) Int .MAX_VALUE else PAGE_SIZE
67+ while (true ) {
68+ try {
69+ val page = _getOwnedAppsPage (pageSize, offset, invalidPkgId)
70+ if (page.isEmpty()) return result
71+ result + = page
72+ if (pageSize == Int .MAX_VALUE ) return result // got everything in one shot
73+ offset + = page.size
74+ break
75+ } catch (e: android.database.sqlite.SQLiteBlobTooBigException ) {
76+ if (pageSize <= 1 ) throw e // single row exceeds window, can't recover
77+ pageSize = if (pageSize == Int .MAX_VALUE ) PAGE_SIZE else (pageSize / 2 ).coerceAtLeast(1 )
78+ }
79+ }
80+ }
81+ }
82+
83+ // emits full list on count changes, loaded in pages to avoid CursorWindow overflow.
84+ // property-only updates (name, icon) won't re-emit until the next count change.
85+ @OptIn(ExperimentalCoroutinesApi ::class )
3686 fun getAllOwnedApps (
3787 invalidPkgId : Int = INVALID_PKG_ID ,
38- ): Flow <List <SteamApp >>
88+ ): Flow <List <SteamApp >> = _observeOwnedAppCount (invalidPkgId)
89+ .distinctUntilChanged() // skip reload when count unchanged
90+ .flatMapLatest { // cancel stale reloads during rapid PICS inserts
91+ flow { emit(_getAllOwnedAppsPaged (invalidPkgId)) }
92+ }
3993
4094 @Query(" SELECT * FROM steam_app WHERE received_pics = 0 AND package_id != :invalidPkgId AND owner_account_id = :ownerId" )
4195 fun getAllOwnedAppsWithoutPICS (
0 commit comments