Skip to content

Commit d7e8e7b

Browse files
fix: CursorWindow crash on large libraries (SQLiteBlobTooBigException) (#961)
getAllOwnedApps loaded all rows in one cursor, which can overflow the CursorWindow when rows contain large blobs. Try loading all at once; on SQLiteBlobTooBigException, retry with progressively smaller pages (50 → 25 → ... → 1). Rethrows if a single row exceeds the window. Flow reactivity preserved via COUNT(*) observer that triggers a reload on every table change.
1 parent d1e620b commit d7e8e7b

1 file changed

Lines changed: 65 additions & 11 deletions

File tree

app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,27 @@ import androidx.room.Dao
44
import androidx.room.Insert
55
import androidx.room.OnConflictStrategy
66
import androidx.room.Query
7+
import androidx.room.Transaction
78
import androidx.room.Update
89
import app.gamenative.data.SteamApp
910
import app.gamenative.service.SteamService.Companion.INVALID_PKG_ID
11+
import kotlinx.coroutines.ExperimentalCoroutinesApi
1012
import 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
1330
interface 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

Comments
 (0)