Skip to content

Commit 9fa7ee8

Browse files
authored
Merge pull request #6890 from seadowg/row-number-crash
Ensure Cursors don't cause crashes by being processed after a close() call
2 parents df953c6 + 453e032 commit 9fa7ee8

File tree

8 files changed

+123
-68
lines changed

8 files changed

+123
-68
lines changed

collect_app/src/main/java/org/odk/collect/android/database/entities/DatabaseEntitiesRepository.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import android.database.Cursor
66
import android.database.sqlite.SQLiteDatabase
77
import android.database.sqlite.SQLiteException
88
import android.provider.BaseColumns._ID
9-
import androidx.core.database.getLongOrNull
109
import org.odk.collect.db.sqlite.CursorExt.first
1110
import org.odk.collect.db.sqlite.CursorExt.foldAndClose
1211
import org.odk.collect.db.sqlite.CursorExt.getBoolean
@@ -235,17 +234,22 @@ class DatabaseEntitiesRepository(context: Context, dbPath: String, private val c
235234

236235
private fun queryWithAttachedRowNumber(list: String, query: Query?): List<Entity.Saved> {
237236
try {
237+
val cursorMapper = { cursor: Cursor ->
238+
cursor.foldAndClose {
239+
mapCursorRowToEntity(it, it.getInt(ROW_NUMBER))
240+
}
241+
}
242+
238243
return if (query == null) {
239-
databaseConnection.rawQueryWithRowNumber(list)
244+
databaseConnection.rawQueryWithRowNumber(list, cursorMapper = cursorMapper)
240245
} else {
241246
val sqlQuery = query.toSql()
242247
databaseConnection.rawQueryWithRowNumber(
243248
list,
244249
sqlQuery.selection,
245-
sqlQuery.selectionArgs
250+
sqlQuery.selectionArgs,
251+
cursorMapper = cursorMapper
246252
)
247-
}.foldAndClose {
248-
mapCursorRowToEntity(it, it.getInt(ROW_NUMBER))
249253
}
250254
} catch (e: SQLiteException) {
251255
throw QueryException(e.message)

collect_app/src/test/java/org/odk/collect/android/database/DatabaseConnectionTest.kt

Lines changed: 0 additions & 40 deletions
This file was deleted.

db/src/main/java/org/odk/collect/db/sqlite/DatabaseConnection.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.odk.collect.db.sqlite
22

33
import android.content.Context
4+
import android.database.Cursor
45
import android.database.sqlite.SQLiteDatabase
56
import android.database.sqlite.SQLiteDatabase.CursorFactory
67
import android.database.sqlite.SQLiteOpenHelper
@@ -91,7 +92,13 @@ open class DatabaseConnection @JvmOverloads constructor(
9192
*/
9293
fun <T> withSynchronizedConnection(block: DatabaseConnection.() -> T): T {
9394
return synchronized(dbHelper) {
94-
block(this)
95+
val result = block(this)
96+
97+
if (result !is Cursor) {
98+
result
99+
} else {
100+
throw IllegalStateException("Returning a Cursor removes synchronized guarantees!")
101+
}
95102
}
96103
}
97104

db/src/main/java/org/odk/collect/db/sqlite/RowNumbers.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import org.odk.collect.db.sqlite.SQLiteColumns.ROW_ID
55
import org.odk.collect.db.sqlite.SQLiteColumns.ROW_NUMBER
66

77
object RowNumbers {
8-
fun SynchronizedDatabaseConnection.rawQueryWithRowNumber(table: String, selection: String? = null, selectionArgs: Array<String>? = null): Cursor {
8+
fun <T> SynchronizedDatabaseConnection.rawQueryWithRowNumber(table: String, selection: String? = null, selectionArgs: Array<String>? = null, cursorMapper: (Cursor) -> T): T {
99
this.ensureRowIdTable(table)
1010

11-
val cursor = if (selection != null) {
11+
return if (selection != null) {
1212
this.withConnection {
13-
readableDatabase
13+
val cursor = readableDatabase
1414
.rawQuery(
1515
"""
1616
SELECT *, i.$ROW_ID as $ROW_NUMBER
@@ -20,10 +20,12 @@ object RowNumbers {
2020
""".trimIndent(),
2121
selectionArgs
2222
)
23+
24+
cursorMapper(cursor)
2325
}
2426
} else {
2527
this.withConnection {
26-
readableDatabase
28+
val cursor = readableDatabase
2729
.rawQuery(
2830
"""
2931
SELECT *, i.$ROW_ID as $ROW_NUMBER
@@ -33,10 +35,10 @@ object RowNumbers {
3335
""".trimIndent(),
3436
null
3537
)
38+
39+
cursorMapper(cursor)
3640
}
3741
}
38-
39-
return cursor
4042
}
4143

4244
fun SynchronizedDatabaseConnection.invalidateRowNumbers(table: String) {

db/src/main/java/org/odk/collect/db/sqlite/SynchronizedDatabaseConnection.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ class SynchronizedDatabaseConnection(
2424
return databaseConnection.withSynchronizedConnection(block)
2525
}
2626

27-
fun <T> transaction(
28-
body: SQLiteDatabase.() -> T
27+
fun transaction(
28+
body: SQLiteDatabase.() -> Unit
2929
) {
3030
return withConnection {
3131
writableDatabase.transaction {
@@ -38,8 +38,8 @@ class SynchronizedDatabaseConnection(
3838
* Runs a transaction and then calls [DatabaseConnection.reset]. Useful for transactions
3939
* that will mutate the DB schema.
4040
*/
41-
fun <T> resetTransaction(
42-
body: SQLiteDatabase.() -> T
41+
fun resetTransaction(
42+
body: SQLiteDatabase.() -> Unit
4343
) {
4444
transaction(body)
4545
databaseConnection.reset()
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package org.odk.collect.db.sqlite
2+
3+
import android.content.Context
4+
import androidx.test.core.app.ApplicationProvider
5+
import androidx.test.ext.junit.runners.AndroidJUnit4
6+
import org.junit.Assert.assertFalse
7+
import org.junit.Assert.assertTrue
8+
import org.junit.Assert.fail
9+
import org.junit.Test
10+
import org.junit.runner.RunWith
11+
import org.odk.collect.db.sqlite.support.NoopMigrator
12+
import org.odk.collect.shared.TempFiles
13+
import java.io.File
14+
15+
@RunWith(AndroidJUnit4::class)
16+
class DatabaseConnectionTest {
17+
18+
private val context = ApplicationProvider.getApplicationContext<Context>()
19+
20+
@Test
21+
fun `#withSynchronizedConnection cannot return a Cursor`() {
22+
val dbConnection = DatabaseConnection(
23+
context,
24+
TempFiles.createTempDir().absolutePath,
25+
"temp.db",
26+
NoopMigrator(),
27+
1
28+
)
29+
30+
dbConnection.writableDatabase.execSQL("CREATE TABLE blah (id integer);")
31+
32+
try {
33+
dbConnection.withSynchronizedConnection {
34+
readableDatabase.query(
35+
"blah",
36+
null,
37+
null,
38+
null,
39+
null,
40+
null,
41+
null
42+
)
43+
}
44+
45+
fail()
46+
} catch (_: IllegalStateException) {
47+
// Expected
48+
}
49+
}
50+
51+
// https://github.com/getodk/collect/issues/5042
52+
@Test
53+
fun `database file should be recreated if removed between operations`() {
54+
val dbDir = TempFiles.createTempDir()
55+
val dbFileName = "temp.db"
56+
val dbPath = dbDir.absolutePath + File.separator + dbFileName
57+
58+
DatabaseConnection(
59+
context,
60+
dbDir.absolutePath,
61+
dbFileName,
62+
NoopMigrator(),
63+
1
64+
).also {
65+
it.readableDatabase
66+
assertTrue(File(dbPath).exists())
67+
68+
File(dbPath).delete()
69+
assertFalse(File(dbPath).exists())
70+
71+
it.readableDatabase
72+
assertTrue(File(dbPath).exists())
73+
}
74+
}
75+
}

db/src/test/java/org/odk/collect/db/sqlite/RowNumbersTest.kt

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package org.odk.collect.db.sqlite
22

33
import android.content.ContentValues
44
import android.content.Context
5-
import android.database.sqlite.SQLiteDatabase
65
import android.provider.BaseColumns._ID
76
import androidx.test.core.app.ApplicationProvider
87
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -15,6 +14,7 @@ import org.odk.collect.db.sqlite.CursorExt.rowToMap
1514
import org.odk.collect.db.sqlite.RowNumbers.invalidateRowNumbers
1615
import org.odk.collect.db.sqlite.RowNumbers.rawQueryWithRowNumber
1716
import org.odk.collect.db.sqlite.SQLiteColumns.ROW_NUMBER
17+
import org.odk.collect.db.sqlite.support.NoopMigrator
1818
import org.odk.collect.shared.TempFiles
1919

2020
@RunWith(AndroidJUnit4::class)
@@ -41,8 +41,9 @@ class RowNumbersTest {
4141
insertOrThrow("test_table", null, ContentValues().also { it.put("position", "second") })
4242
}
4343

44-
val rows =
45-
dbConnection.rawQueryWithRowNumber("test_table").foldAndClose { it.rowToMap() }
44+
val rows = dbConnection.rawQueryWithRowNumber("test_table") { cursor ->
45+
cursor.foldAndClose { it.rowToMap() }
46+
}
4647
assertThat(rows.size, equalTo(2))
4748

4849
assertThat(rows[0]["position"], equalTo("first"))
@@ -72,8 +73,9 @@ class RowNumbersTest {
7273
insertOrThrow("test_table", null, ContentValues().also { it.put("position", "third") })
7374
}
7475

75-
val beforeRows =
76-
dbConnection.rawQueryWithRowNumber("test_table").foldAndClose { it.rowToMap() }
76+
val beforeRows = dbConnection.rawQueryWithRowNumber("test_table") { cursor ->
77+
cursor.foldAndClose { it.rowToMap() }
78+
}
7779
assertThat(beforeRows.size, equalTo(3))
7880

7981
dbConnection.transaction {
@@ -82,8 +84,9 @@ class RowNumbersTest {
8284

8385
dbConnection.invalidateRowNumbers("test_table")
8486

85-
val afterRows =
86-
dbConnection.rawQueryWithRowNumber("test_table").foldAndClose { it.rowToMap() }
87+
val afterRows = dbConnection.rawQueryWithRowNumber("test_table") { cursor ->
88+
cursor.foldAndClose { it.rowToMap() }
89+
}
8790
assertThat(afterRows.size, equalTo(2))
8891

8992
assertThat(afterRows[0]["position"], equalTo("first"))
@@ -93,8 +96,3 @@ class RowNumbersTest {
9396
assertThat(afterRows[1][ROW_NUMBER], equalTo("2"))
9497
}
9598
}
96-
97-
private class NoopMigrator : DatabaseMigrator {
98-
override fun onCreate(db: SQLiteDatabase?) {}
99-
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int) {}
100-
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.odk.collect.db.sqlite.support
2+
3+
import android.database.sqlite.SQLiteDatabase
4+
import org.odk.collect.db.sqlite.DatabaseMigrator
5+
6+
class NoopMigrator : DatabaseMigrator {
7+
override fun onCreate(db: SQLiteDatabase?) {}
8+
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int) {}
9+
}

0 commit comments

Comments
 (0)