Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,28 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import net.sqlcipher.database.SQLiteDatabase.getBytes
import net.sqlcipher.database.SupportFactory
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
internal class EventDatabaseFactory @Inject constructor(
@ApplicationContext val ctx: Context,
private val securityManager: SecurityManager,
) {
fun build(): EventRoomDatabase {
private lateinit var eventDatabase: EventRoomDatabase

fun get(): EventRoomDatabase {
if (!::eventDatabase.isInitialized) {
build()
}
return eventDatabase
}

private fun build() {
try {
val key = getOrCreateKey(DB_NAME)
val passphrase: ByteArray = getBytes(key)
val factory = SupportFactory(passphrase)
return EventRoomDatabase.getDatabase(
eventDatabase = EventRoomDatabase.getDatabase(
ctx,
factory,
DB_NAME,
Expand All @@ -38,12 +49,14 @@ internal class EventDatabaseFactory @Inject constructor(
securityManager.getLocalDbKeyOrThrow(dbName)
}.value.decodeToString().toCharArray()

fun deleteDatabase() {
fun recreateDatabase() {
// DB corruption detected; either DB file or key is corrupt
// 1. Delete DB file in order to create a new one at next init
ctx.deleteDatabase(DB_NAME)
}

fun recreateDatabaseKey() {
// 2. Recreate the DB key
securityManager.recreateLocalDatabaseKey(DB_NAME)
// 3. Rebuild the DB
build()
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,19 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext

@Singleton
internal open class EventLocalDataSource @Inject constructor(
private val eventDatabaseFactory: EventDatabaseFactory,
private val jsonHelper: JsonHelper,
@DispatcherIO private val readingDispatcher: CoroutineDispatcher,
@NonCancellableIO private val writingContext: CoroutineContext,
) {
private var eventDao: EventRoomDao = eventDatabaseFactory.build().eventDao
private var eventDao: EventRoomDao = eventDatabaseFactory.get().eventDao

private var scopeDao: SessionScopeRoomDao = eventDatabaseFactory.build().scopeDao
private var scopeDao: SessionScopeRoomDao = eventDatabaseFactory.get().scopeDao

private val mutex = Mutex()

Expand All @@ -44,7 +46,7 @@ internal open class EventLocalDataSource @Inject constructor(
block()
} catch (ex: SQLiteException) {
if (isFileCorruption(ex)) {
rebuildDatabase(ex)
recreateDatabase(ex)
// Retry operation with new file and key
block()
} else {
Expand All @@ -60,7 +62,7 @@ internal open class EventLocalDataSource @Inject constructor(
try {
block().catch { cause ->
if (isFileCorruption(cause)) {
rebuildDatabase(cause)
recreateDatabase(cause)
// Recreate flow and re-emit values with the new file and key
emitAll(block())
} else {
Expand All @@ -69,7 +71,7 @@ internal open class EventLocalDataSource @Inject constructor(
}
} catch (ex: SQLiteException) {
if (isFileCorruption(ex)) {
rebuildDatabase(ex)
recreateDatabase(ex)
// Recreate flow with the new file and key
block()
} else {
Expand All @@ -81,17 +83,11 @@ internal open class EventLocalDataSource @Inject constructor(
private fun isFileCorruption(ex: Throwable) = ex is SQLiteDatabaseCorruptException ||
ex.let { it as? SQLiteException }?.message?.contains("file is not a database") == true

private suspend fun rebuildDatabase(ex: Throwable) = mutex.withLock {
// DB corruption detected; either DB file or key is corrupt
// 1. Delete DB file in order to create a new one at next init
eventDatabaseFactory.deleteDatabase()
// 2. Recreate the DB key
eventDatabaseFactory.recreateDatabaseKey()
// 3. Log exception after recreating the key so we get extra info
Simber.e("Rebuilt event DB due to error", ex, tag = DB_CORRUPTION)
// 4. Rebuild database
eventDao = eventDatabaseFactory.build().eventDao
scopeDao = eventDatabaseFactory.build().scopeDao
private suspend fun recreateDatabase(ex: Throwable) = mutex.withLock {
eventDatabaseFactory.recreateDatabase()
Simber.e("Recreated event DB due to error", ex, tag = DB_CORRUPTION)
eventDao = eventDatabaseFactory.get().eventDao
scopeDao = eventDatabaseFactory.get().scopeDao
}

suspend fun saveEventScope(scope: EventScope) = useRoom(writingContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,58 +31,72 @@ internal class EventDatabaseFactoryTest {
}

@Test
fun `test build db success`() = runTest {
fun `test get db success`() = runTest {
// Given
coEvery { securityManager.getLocalDbKeyOrThrow(dbName) } returns localDbKey
mockkObject(EventRoomDatabase)
val db: EventRoomDatabase = mockk()
every { EventRoomDatabase.getDatabase(context, any(), dbName) } returns db
// When
Truth.assertThat(dbEventDatabaseFactory.build()).isEqualTo(db)
Truth.assertThat(dbEventDatabaseFactory.get()).isEqualTo(db)
verify { securityManager.getLocalDbKeyOrThrow(dbName) }
}

@Test
fun `test build db creates key if not exist`() = runTest {
fun `get should return the same db instance on multiple calls`() = runTest {
// Given
coEvery { securityManager.getLocalDbKeyOrThrow(dbName) } returns localDbKey
mockkObject(EventRoomDatabase)
every { EventRoomDatabase.getDatabase(context, any(), dbName) } returns mockk()
// When and Then
val db1 = dbEventDatabaseFactory.get()
val db2 = dbEventDatabaseFactory.get()
Truth.assertThat(db1).isSameInstanceAs(db2)
// Verify that getLocalDbKeyOrThrow is called only once
verify(exactly = 1) {
securityManager.getLocalDbKeyOrThrow(dbName)
EventRoomDatabase.getDatabase(context, any(), dbName)
}
}

@Test
fun `test get db creates key if not exist`() = runTest {
// Given
coEvery { securityManager.getLocalDbKeyOrThrow(dbName) } throws Exception() andThen localDbKey
justRun { securityManager.createLocalDatabaseKeyIfMissing(dbName) }
mockkObject(EventRoomDatabase)
val db: EventRoomDatabase = mockk()
every { EventRoomDatabase.getDatabase(context, any(), dbName) } returns db
// When and Then
Truth.assertThat(dbEventDatabaseFactory.build()).isEqualTo(db)
Truth.assertThat(dbEventDatabaseFactory.get()).isEqualTo(db)
verify(exactly = 2) { securityManager.getLocalDbKeyOrThrow(dbName) }
}

@Test(expected = Exception::class)
fun `test build db falure`() = runTest {
fun `test get db falure`() = runTest {
// Given
coEvery { securityManager.getLocalDbKeyOrThrow(dbName) } throws Exception()
justRun { securityManager.createLocalDatabaseKeyIfMissing(dbName) }
mockkObject(EventRoomDatabase)
val db: EventRoomDatabase = mockk()
every { EventRoomDatabase.getDatabase(context, any(), dbName) } returns db
// When calling build it should throw exception
dbEventDatabaseFactory.build()
}

@Test
fun deleteDatabase() {
// When
dbEventDatabaseFactory.deleteDatabase()

// Then
verify { context.deleteDatabase(dbName) }
dbEventDatabaseFactory.get()
}

@Test
fun recreateDatabaseKey() {
fun recreateDatabase() {
// Given
justRun { securityManager.recreateLocalDatabaseKey(dbName) }
coEvery { securityManager.getLocalDbKeyOrThrow(dbName) } returns localDbKey
justRun { securityManager.createLocalDatabaseKeyIfMissing(dbName) }
mockkObject(EventRoomDatabase)
val db: EventRoomDatabase = mockk()
every { EventRoomDatabase.getDatabase(context, any(), dbName) } returns db
// When
dbEventDatabaseFactory.recreateDatabaseKey()
dbEventDatabaseFactory.recreateDatabase()
// Then
verify { context.deleteDatabase(dbName) }
verify { securityManager.recreateLocalDatabaseKey(dbName) }
}
}
Loading