system: handle fallible databases

Handle errors from databases.

Either way, a crash from a database or a silent error will be equally
nightmarish to debug. May as well keep going if they fail.
This commit is contained in:
Alexander Capehart 2022-11-13 19:24:43 -07:00
parent aa805e351c
commit aa50c82635
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 76 additions and 23 deletions

View file

@ -26,6 +26,7 @@ import androidx.core.database.getStringOrNull
import androidx.core.database.sqlite.transaction import androidx.core.database.sqlite.transaction
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.queryAll import org.oxycblt.auxio.util.queryAll
import org.oxycblt.auxio.util.requireBackgroundThread import org.oxycblt.auxio.util.requireBackgroundThread
import java.io.File import java.io.File
@ -42,7 +43,12 @@ class CacheExtractor(private val context: Context) {
private var shouldWriteCache = false private var shouldWriteCache = false
fun init() { fun init() {
cacheMap = CacheDatabase.getInstance(context).read() try {
cacheMap = CacheDatabase.getInstance(context).read()
} catch (e: Exception) {
logE("Unable to load cache database.")
logE(e.stackTraceToString())
}
} }
/** /**
@ -55,16 +61,21 @@ class CacheExtractor(private val context: Context) {
// If the entire library could not be loaded from the cache, we need to re-write it // If the entire library could not be loaded from the cache, we need to re-write it
// with the new library. // with the new library.
logD("Cache was invalidated during loading, rewriting") logD("Cache was invalidated during loading, rewriting")
CacheDatabase.getInstance(context).write(rawSongs) try {
CacheDatabase.getInstance(context).write(rawSongs)
} catch (e: Exception) {
logE("Unable to save cache database.")
logE(e.stackTraceToString())
}
} }
} }
/** /**
* Maybe copy a cached raw song into this instance, assuming that it has not changed * Maybe copy a cached raw song into this instance, assuming that it has not changed
* since it was last saved. * since it was last saved. Returns true if a song was loaded.
*/ */
fun populateFromCache(rawSong: Song.Raw): Boolean { fun populateFromCache(rawSong: Song.Raw): Boolean {
val map = requireNotNull(cacheMap) { "CacheExtractor was not properly initialized" } val map = cacheMap ?: return false
val cachedRawSong = map[rawSong.mediaStoreId] val cachedRawSong = map[rawSong.mediaStoreId]
if (cachedRawSong != null && cachedRawSong.dateAdded == rawSong.dateAdded && cachedRawSong.dateModified == rawSong.dateModified) { if (cachedRawSong != null && cachedRawSong.dateAdded == rawSong.dateAdded && cachedRawSong.dateModified == rawSong.dateModified) {

View file

@ -248,26 +248,32 @@ class PlaybackViewModel(application: Application) :
// --- SAVE/RESTORE FUNCTIONS --- // --- SAVE/RESTORE FUNCTIONS ---
/** Force save the current [PlaybackStateManager] state to the database. */ /**
fun savePlaybackState(onDone: () -> Unit) { * Force save the current [PlaybackStateManager] state to the database. [onDone]
* will be called with true if it was done, or false if an error occurred.
*/
fun savePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch { viewModelScope.launch {
playbackManager.saveState(PlaybackStateDatabase.getInstance(application)) val saved = playbackManager.saveState(PlaybackStateDatabase.getInstance(application))
onDone() onDone(saved)
} }
} }
/** Wipe the saved playback state (if any). */ /**
fun wipePlaybackState(onDone: () -> Unit) { * Wipe the saved playback state (if any). [onDone] will be called with true if it was
* successfully done, or false if an error occurred.
*/
fun wipePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch { viewModelScope.launch {
playbackManager.wipeState(PlaybackStateDatabase.getInstance(application)) val wiped = playbackManager.wipeState(PlaybackStateDatabase.getInstance(application))
onDone() onDone(wiped)
} }
} }
/** /**
* Force restore the last [PlaybackStateManager] saved state, regardless of if a library exists * Force restore the last [PlaybackStateManager] saved state, regardless of if a library exists
* or not. [onDone] will be called with true if it was successfully done, or false if there was * or not. [onDone] will be called with true if it was successfully done, or false if there was
* no state or if a library was not present. * no state, a library was not present, or there was an error.
*/ */
fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) { fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch { viewModelScope.launch {

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager.Callback import org.oxycblt.auxio.playback.state.PlaybackStateManager.Callback
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import kotlin.math.max import kotlin.math.max
@ -368,7 +369,13 @@ class PlaybackStateManager private constructor() {
val library = musicStore.library ?: return false val library = musicStore.library ?: return false
val internalPlayer = internalPlayer ?: return false val internalPlayer = internalPlayer ?: return false
val state = withContext(Dispatchers.IO) { database.read(library) } val state = try {
withContext(Dispatchers.IO) { database.read(library) }
} catch (e: Exception) {
logE("Unable to restore playback state.")
logE(e.stackTraceToString())
return false
}
synchronized(this) { synchronized(this) {
if (state != null && (!isInitialized || force)) { if (state != null && (!isInitialized || force)) {
@ -397,15 +404,32 @@ class PlaybackStateManager private constructor() {
} }
/** Save the current state to the [database]. */ /** Save the current state to the [database]. */
suspend fun saveState(database: PlaybackStateDatabase) { suspend fun saveState(database: PlaybackStateDatabase): Boolean {
logD("Saving state to DB") logD("Saving state to DB")
val state = synchronized(this) { makeStateImpl() } val state = synchronized(this) { makeStateImpl() }
withContext(Dispatchers.IO) { database.write(state) } return try {
withContext(Dispatchers.IO) { database.write(state) }
true
} catch (e: Exception) {
logE("Unable to save playback state.")
logE(e.stackTraceToString())
false
}
} }
suspend fun wipeState(database: PlaybackStateDatabase) { /** Wipe the current state. */
suspend fun wipeState(database: PlaybackStateDatabase): Boolean {
logD("Wiping state") logD("Wiping state")
withContext(Dispatchers.IO) { database.write(null) }
return try {
withContext(Dispatchers.IO) { database.write(null) }
true
} catch (e: Exception) {
logE("Unable to wipe playback state.")
logE(e.stackTraceToString())
false
}
} }
/** Sanitize the state with [newLibrary]. */ /** Sanitize the state with [newLibrary]. */

View file

@ -113,13 +113,21 @@ class PreferenceFragment : PreferenceFragmentCompat() {
when (preference.key) { when (preference.key) {
context.getString(R.string.set_key_save_state) -> { context.getString(R.string.set_key_save_state) -> {
playbackModel.savePlaybackState { playbackModel.savePlaybackState { saved ->
this.context?.showToast(R.string.lbl_state_saved) if (saved) {
this.context?.showToast(R.string.lbl_state_saved)
} else {
this.context?.showToast(R.string.err_did_not_save)
}
} }
} }
context.getString(R.string.set_key_wipe_state) -> { context.getString(R.string.set_key_wipe_state) -> {
playbackModel.wipePlaybackState { playbackModel.wipePlaybackState { wiped ->
this.context?.showToast(R.string.lbl_state_wiped) if (wiped) {
this.context?.showToast(R.string.lbl_state_wiped)
} else {
this.context?.showToast(R.string.err_did_not_wipe)
}
} }
} }
context.getString(R.string.set_key_restore_state) -> context.getString(R.string.set_key_restore_state) ->

View file

@ -251,7 +251,11 @@
<string name="err_no_dirs">No folders</string> <string name="err_no_dirs">No folders</string>
<string name="err_bad_dir">This folder is not supported</string> <string name="err_bad_dir">This folder is not supported</string>
<!-- Referring to playback state --> <!-- Referring to playback state -->
<string name="err_did_not_restore">No state could be restored</string> <string name="err_did_not_restore">Unable to restore state</string>
<!-- Referring to playback state -->
<string name="err_did_not_wipe">Unable to clear state</string>
<!-- Referring to playback state -->
<string name="err_did_not_save">Unable to save state</string>
<!-- Description Namespace | Accessibility Strings --> <!-- Description Namespace | Accessibility Strings -->