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 org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.queryAll
import org.oxycblt.auxio.util.requireBackgroundThread
import java.io.File
@ -42,7 +43,12 @@ class CacheExtractor(private val context: Context) {
private var shouldWriteCache = false
fun init() {
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
// with the new library.
logD("Cache was invalidated during loading, rewriting")
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
* since it was last saved.
* since it was last saved. Returns true if a song was loaded.
*/
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]
if (cachedRawSong != null && cachedRawSong.dateAdded == rawSong.dateAdded && cachedRawSong.dateModified == rawSong.dateModified) {

View file

@ -248,26 +248,32 @@ class PlaybackViewModel(application: Application) :
// --- 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 {
playbackManager.saveState(PlaybackStateDatabase.getInstance(application))
onDone()
val saved = playbackManager.saveState(PlaybackStateDatabase.getInstance(application))
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 {
playbackManager.wipeState(PlaybackStateDatabase.getInstance(application))
onDone()
val wiped = playbackManager.wipeState(PlaybackStateDatabase.getInstance(application))
onDone(wiped)
}
}
/**
* 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
* 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) {
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.settings.Settings
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW
import kotlin.math.max
@ -368,7 +369,13 @@ class PlaybackStateManager private constructor() {
val library = musicStore.library ?: 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) {
if (state != null && (!isInitialized || force)) {
@ -397,15 +404,32 @@ class PlaybackStateManager private constructor() {
}
/** Save the current state to the [database]. */
suspend fun saveState(database: PlaybackStateDatabase) {
suspend fun saveState(database: PlaybackStateDatabase): Boolean {
logD("Saving state to DB")
val state = synchronized(this) { makeStateImpl() }
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")
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]. */

View file

@ -113,13 +113,21 @@ class PreferenceFragment : PreferenceFragmentCompat() {
when (preference.key) {
context.getString(R.string.set_key_save_state) -> {
playbackModel.savePlaybackState {
playbackModel.savePlaybackState { 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) -> {
playbackModel.wipePlaybackState {
playbackModel.wipePlaybackState { 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) ->

View file

@ -251,7 +251,11 @@
<string name="err_no_dirs">No folders</string>
<string name="err_bad_dir">This folder is not supported</string>
<!-- 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 -->