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:
parent
aa805e351c
commit
aa50c82635
5 changed files with 76 additions and 23 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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]. */
|
||||||
|
|
|
@ -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) ->
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue