all: audit synchronized usages

Audit usages of Synchronized throughout the app to prevent deadlocks.

This is primarily composed of not making long-running work
synchronized, instead only making mutations and reads synchronized.
This does cause minor issues, such as during a sanitization event
the playback state could be feasibly saved, changed, and then restored
back to the previous state unintentionally. However, preventing
deadlocks is generally better than trying to fix those.
This commit is contained in:
OxygenCobalt 2022-07-03 09:09:56 -06:00
parent 3bdf7b136e
commit cd00950a5c
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 39 additions and 48 deletions

View file

@ -47,8 +47,6 @@ import org.oxycblt.auxio.util.logD
*
* TODO: Add file observing
*
* TODO: Audit usages of synchronized
*
* TODO: Rework UI flow once again
*/
class IndexerService : Service(), Indexer.Controller, Settings.Callback {

View file

@ -35,12 +35,14 @@ class MusicStore private constructor() {
private set
/** Add a callback to this instance. Make sure to remove it when done. */
@Synchronized
fun addCallback(callback: Callback) {
callback.onLibraryChanged(library)
callbacks.add(callback)
}
/** Remove a callback from this instance. */
@Synchronized
fun removeCallback(callback: Callback) {
callbacks.remove(callback)
}
@ -82,7 +84,6 @@ class MusicStore private constructor() {
}
fun sanitize(song: Song) = songs.find { it.id == song.id }
fun sanitize(songs: List<Song>) = songs.mapNotNull { sanitize(it) }
fun sanitize(album: Album) = albums.find { it.id == album.id }
fun sanitize(artist: Artist) = artists.find { it.id == artist.id }
fun sanitize(genre: Genre) = genres.find { it.id == genre.id }

View file

@ -101,7 +101,6 @@ class PlaybackStateDatabase(context: Context) :
// --- INTERFACE FUNCTIONS ---
@Synchronized
fun read(library: MusicStore.Library): SavedState? {
requireBackgroundThread()
@ -186,7 +185,6 @@ class PlaybackStateDatabase(context: Context) :
}
/** Clear the previously written [SavedState] and write a new one. */
@Synchronized
fun write(state: SavedState) {
requireBackgroundThread()

View file

@ -360,48 +360,59 @@ class PlaybackStateManager private constructor() {
/** Restore the state from the [database] */
suspend fun restoreState(database: PlaybackStateDatabase) {
val library = musicStore.library ?: return
withContext(Dispatchers.IO) { readImpl(database, library) }?.let(::restoreImpl)
isInitialized = true
val state = withContext(Dispatchers.IO) { database.read(library) }
synchronized(this) {
if (state != null) {
applyStateImpl(state)
}
isInitialized = true
}
}
/** Save the current state to the [database]. */
suspend fun saveState(database: PlaybackStateDatabase) {
logD("Saving state to DB")
// Pack the entire state and save it to the database.
withContext(Dispatchers.IO) { saveImpl(database) }
val state = synchronized(this) { makeStateImpl() }
withContext(Dispatchers.IO) { database.write(state) }
}
suspend fun sanitize(database: PlaybackStateDatabase, newLibrary: MusicStore.Library) {
// Since we need to sanitize the state and re-save it for consistency, take the
// easy way out and just write a new state and restore from it. Don't really care.
logD("Sanitizing state")
isPlaying = false
val state =
withContext(Dispatchers.IO) {
saveImpl(database)
readImpl(database, newLibrary)
synchronized(this) {
isPlaying = false
makeStateImpl()
}
state?.let(::restoreImpl)
val sanitizedState =
withContext(Dispatchers.IO) {
database.write(state)
database.read(newLibrary)
}
synchronized(this) {
if (sanitizedState != null) {
applyStateImpl(state)
}
}
}
private fun readImpl(
database: PlaybackStateDatabase,
library: MusicStore.Library
): PlaybackStateDatabase.SavedState? {
logD("Getting state from DB")
private fun makeStateImpl() =
PlaybackStateDatabase.SavedState(
index = index,
parent = parent,
queue = _queue,
positionMs = positionMs,
isShuffled = isShuffled,
repeatMode = repeatMode)
val start = System.currentTimeMillis()
val state = database.read(library)
logD("State read completed successfully in ${System.currentTimeMillis() - start}ms")
return state
}
@Synchronized
private fun restoreImpl(state: PlaybackStateDatabase.SavedState) {
private fun applyStateImpl(state: PlaybackStateDatabase.SavedState) {
index = state.index
parent = state.parent
_queue = state.queue.toMutableList()
@ -414,23 +425,6 @@ class PlaybackStateManager private constructor() {
notifyShuffledChanged()
}
@Synchronized
private fun saveImpl(database: PlaybackStateDatabase) {
val start = System.currentTimeMillis()
database.write(
PlaybackStateDatabase.SavedState(
index = index,
parent = parent,
queue = _queue,
positionMs = positionMs,
isShuffled = isShuffled,
repeatMode = repeatMode))
this@PlaybackStateManager.logD(
"State save completed successfully in ${System.currentTimeMillis() - start}ms")
}
// --- CALLBACKS ---
private fun notifyIndexMoved() {

View file

@ -236,8 +236,8 @@ val AndroidViewModel.application: Application
fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
query(tableName, null, null, null, null, null, null)?.use(block)
// Note: ViewCompat.setOnApplyWindowInsets is a horrible buggy mess, so we use the native method
// and convert the insets as needed to their compat forms.
// Note: WindowInsetsCompat and it's related methods are a non-functional mess that does not
// work for Auxio's use-case. Use our own methods instead.
/**
* Resolve system bar insets in a version-aware manner. This can be used to apply padding to a view