Search code examples
androidkotlinandroid-sqliteandroid-room

Android Room SQLiteReadOnlyDatabaseException


I have converted my app to use Android Room for SQLite DB. There are some crashes on different devices with my implementation.

Fatal Exception: android.database.sqlite.SQLiteReadOnlyDatabaseException: attempt to write a readonly database (code 1032 SQLITE_READONLY_DBMOVED)
   at android.database.sqlite.SQLiteConnection.nativeExecute(SQLiteConnection.java)
   at android.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:707)
   at android.database.sqlite.SQLiteConnection.setLocaleFromConfiguration(SQLiteConnection.java:473)
   at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:261)
   at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:205)
   at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:505)
   at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:206)
   at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:198)
   at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:918)
   at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:898)
   at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:762)
   at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:751)
   at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:373)
   at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:316)
   at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:145)
   at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:106)
   at androidx.room.SQLiteCopyOpenHelper.getWritableDatabase(SQLiteCopyOpenHelper.java:97)
   at androidx.room.RoomDatabase.internalBeginTransaction(RoomDatabase.java:482)
   at androidx.room.RoomDatabase.beginTransaction(RoomDatabase.java:471)
   at com.luzeon.MyApp.sqlite.ViewLogDao_Impl$5.call(ViewLogDao_Impl.java:94)
   at com.luzeon.MyApp.sqlite.ViewLogDao_Impl$5.call(ViewLogDao_Impl.java:91)
   at androidx.room.CoroutinesRoom$Companion$execute$2.invokeSuspend(CoroutinesRoom.kt:61)
   at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
   at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
   at androidx.room.TransactionExecutor$1.run(TransactionExecutor.java:47)
   at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
   at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
   at java.lang.Thread.run(Thread.java:923)

I have created the apps Room DB

@Database(entities = [ViewLogModel::class], version = 4)
abstract class MyAppDatabase : RoomDatabase() {
abstract fun viewLogDao(): ViewLogDao

companion object {
    // For Singleton Instance
    @Volatile
    private var INSTANCE: MyAppDatabase? = null

    fun getAppDataBase(context: Context): MyAppDatabase {
        return INSTANCE ?: synchronized(this) {
            INSTANCE ?: Room.databaseBuilder(context.applicationContext, MyAppDatabase::class.java, "MyAppDatabase")
                .createFromAsset(“myapp.db")
                .setJournalMode(RoomDatabase.JournalMode.TRUNCATE) // disable WAL
                .fallbackToDestructiveMigration()
                .build()
        }
    }

    fun destroyDataBase(){
        INSTANCE = null
    }
}

}

And have a DB Helper class

class MyAppDatabaseHelper(private val context: Context, private val coroutineScope: CoroutineScope) {

fun updateViewLog(viewLogModel: ViewLogModel) {

    try {
        // get the database
        val db = MyAppDatabase.getAppDataBase(context)

        coroutineScope.launch {
            // store in db
            db.viewLogDao().insertOrUpdateViewLog(viewLogModel)

        }
    } catch (e: Exception) {}
}

suspend fun getViewLog(memberId: Int): JSONArray {
    try {
        val jsonArray = JSONArray()

        // get the database
        val db = MyAppDatabase.getAppDataBase(context)

        val viewLog = db.viewLogDao().getViewLog(memberId)

        for (view in viewLog) {
            // create the object
            val jsonObject = JSONObject()
            try {
                jsonObject.put("mid", view.mid)
            } catch (e: JSONException) {
            }
            try {
                jsonObject.put("uts", view.uts)
            } catch (e: JSONException) {
            }
            jsonArray.put(jsonObject)
        }

        // clear log (current user records or records older than 24hrs)
        db.viewLogDao().deleteViewLog(memberId, Utilities.getUtsYesterday().toFloat())

        // return the array
        return jsonArray
    } catch (e: Exception) {

        return JSONArray()
    }

}

}

With ViewLogDao

@Dao
interface ViewLogDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrUpdateViewLog(viewLog: ViewLogModel)

@Update
suspend fun updateViewLog(viewLog: ViewLogModel)

@Delete
suspend fun deleteAllViewLog(viewLog: ViewLogModel)

@Query("DELETE FROM viewlog WHERE VID= :vid OR UTS < :uts")
suspend fun deleteViewLog(vid: Int, uts: Float)

@Query("SELECT * FROM viewLog")
suspend fun getAll(): List<ViewLogModel>

@Query("SELECT * FROM viewLog WHERE vid = :vid")
suspend fun getViewLog(vid: Int): List<ViewLogModel>

}

And ViewLogModel

@Entity(tableName = "viewLog")
data class ViewLogModel(
    @ColumnInfo(name = "VID") val vid: Int,
    @ColumnInfo(name = "UTS") val uts: Float,
    @ColumnInfo(name = "MID") @PrimaryKey val mid: Int)

I have not been able to find how to catch the SQLiteReadOnlyDatabaseException in the rare occurrences when the DB is read only. Or is there a way to ensure the ROOM Db is read/write?


Solution

  • I have not been able to find how to catch the SQLiteReadOnlyDatabaseException in the rare occurrences when the DB is read only. Or is there a way to ensure the ROOM Db is read/write?

    The message code 1032 SQLITE_READONLY_DBMOVED :-

    The SQLITE_READONLY_DBMOVED error code is an extended error code for SQLITE_READONLY. The SQLITE_READONLY_DBMOVED error code indicates that a database cannot be modified because the database file has been moved since it was opened, and so any attempt to modify the database might result in database corruption if the processes crashes because the rollback journal would not be correctly named.

    If the message is to be believed then the database has been moved/renamed. From the message it would appear that the (one of the two being handled) database is being renamed whilst it is open.

    In the log many of the entries are similar so it looks like two databases are being managed i.e. it is the stage of creating the database from the asset.

    This may well be an issue with the createFromAsset handling which I understand to not necessarily be rock solid. e.g. at present there are issues with the prePackagedDatabaseCallback.

    As such by using createFromAsset that you can do nothing other than raise an issue.

    I would suggest circumventing the issue and pre-copying the asset yourself before passing control to Room.

    • to undertake the copy you do not need to open the database as a database just as a file.

    The other alternative, could be to see if exclusively using WAL mode, overcomes the issue. As you are disabling WAL mode, then I guess that you have no wish to do so (hence why suggested as the last).

    • this would not only entail not disabling WAL mode but also having the asset set to WAL mode before distribution.