all: phase out taskguard
Phase out the dumb hack TaskGuard class in favor of yield. For some reason, I was under the impression that yield was horribly slow. It's not, I was just using it wrong. So now TaskGuard is no longer needed.
This commit is contained in:
parent
189f712eaa
commit
78201e55ee
11 changed files with 126 additions and 156 deletions
|
|
@ -24,9 +24,11 @@ import androidx.annotation.StringRes
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
|
@ -40,7 +42,6 @@ import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.recycler.Header
|
import org.oxycblt.auxio.ui.recycler.Header
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.TaskGuard
|
|
||||||
import org.oxycblt.auxio.util.application
|
import org.oxycblt.auxio.util.application
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
@ -70,6 +71,8 @@ class DetailViewModel(application: Application) :
|
||||||
val currentSong: StateFlow<DetailSong?>
|
val currentSong: StateFlow<DetailSong?>
|
||||||
get() = _currentSong
|
get() = _currentSong
|
||||||
|
|
||||||
|
private var currentSongJob: Job? = null
|
||||||
|
|
||||||
private val _currentAlbum = MutableStateFlow<Album?>(null)
|
private val _currentAlbum = MutableStateFlow<Album?>(null)
|
||||||
val currentAlbum: StateFlow<Album?>
|
val currentAlbum: StateFlow<Album?>
|
||||||
get() = _currentAlbum
|
get() = _currentAlbum
|
||||||
|
|
@ -114,8 +117,6 @@ class DetailViewModel(application: Application) :
|
||||||
currentGenre.value?.let(::refreshGenreData)
|
currentGenre.value?.let(::refreshGenreData)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val songGuard = TaskGuard()
|
|
||||||
|
|
||||||
fun setSongUid(uid: Music.UID) {
|
fun setSongUid(uid: Music.UID) {
|
||||||
if (_currentSong.value?.run { song.uid } == uid) return
|
if (_currentSong.value?.run { song.uid } == uid) return
|
||||||
val library = unlikelyToBeNull(musicStore.library)
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
|
|
@ -124,7 +125,6 @@ class DetailViewModel(application: Application) :
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearSong() {
|
fun clearSong() {
|
||||||
songGuard.newHandle()
|
|
||||||
_currentSong.value = null
|
_currentSong.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,11 +159,11 @@ class DetailViewModel(application: Application) :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateDetailSong(song: Song) {
|
private fun generateDetailSong(song: Song) {
|
||||||
|
currentSongJob?.cancel()
|
||||||
_currentSong.value = DetailSong(song, null)
|
_currentSong.value = DetailSong(song, null)
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
currentSongJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
val handle = songGuard.newHandle()
|
|
||||||
val info = generateDetailSongInfo(song)
|
val info = generateDetailSongInfo(song)
|
||||||
songGuard.yield(handle)
|
yield()
|
||||||
_currentSong.value = DetailSong(song, info)
|
_currentSong.value = DetailSong(song, info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import coil.request.Disposable
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.TaskGuard
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility to provide bitmaps in a manner less prone to race conditions.
|
* A utility to provide bitmaps in a manner less prone to race conditions.
|
||||||
|
|
@ -38,7 +37,8 @@ import org.oxycblt.auxio.util.TaskGuard
|
||||||
*/
|
*/
|
||||||
class BitmapProvider(private val context: Context) {
|
class BitmapProvider(private val context: Context) {
|
||||||
private var currentRequest: Request? = null
|
private var currentRequest: Request? = null
|
||||||
private var guard = TaskGuard()
|
private var currentHandle = 0L
|
||||||
|
private var handleLock = Any()
|
||||||
|
|
||||||
/** If this provider is currently attempting to load something. */
|
/** If this provider is currently attempting to load something. */
|
||||||
val isBusy: Boolean
|
val isBusy: Boolean
|
||||||
|
|
@ -50,7 +50,9 @@ class BitmapProvider(private val context: Context) {
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun load(song: Song, target: Target) {
|
fun load(song: Song, target: Target) {
|
||||||
val handle = guard.newHandle()
|
val handle = synchronized(handleLock) {
|
||||||
|
++currentHandle
|
||||||
|
}
|
||||||
|
|
||||||
currentRequest?.run { disposable.dispose() }
|
currentRequest?.run { disposable.dispose() }
|
||||||
currentRequest = null
|
currentRequest = null
|
||||||
|
|
@ -62,15 +64,19 @@ class BitmapProvider(private val context: Context) {
|
||||||
.size(Size.ORIGINAL)
|
.size(Size.ORIGINAL)
|
||||||
.target(
|
.target(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
if (guard.check(handle)) {
|
synchronized(handleLock) {
|
||||||
|
if (currentHandle == handle) {
|
||||||
target.onCompleted(it.toBitmap())
|
target.onCompleted(it.toBitmap())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
if (guard.check(handle)) {
|
synchronized(handleLock) {
|
||||||
|
if (currentHandle == handle) {
|
||||||
target.onCompleted(null)
|
target.onCompleted(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.transformations(SquareFrameTransform.INSTANCE)
|
.transformations(SquareFrameTransform.INSTANCE)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class MetadataLayer(private val context: Context, private val mediaStoreLayer: M
|
||||||
/** Finalize the sub-layers that this layer relies on. */
|
/** Finalize the sub-layers that this layer relies on. */
|
||||||
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreLayer.finalize(rawSongs)
|
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreLayer.finalize(rawSongs)
|
||||||
|
|
||||||
fun parse(emit: (Song.Raw) -> Unit) {
|
suspend fun parse(emit: suspend (Song.Raw) -> Unit) {
|
||||||
while (true) {
|
while (true) {
|
||||||
val raw = Song.Raw()
|
val raw = Song.Raw()
|
||||||
if (mediaStoreLayer.populateRaw(raw) ?: break) {
|
if (mediaStoreLayer.populateRaw(raw) ?: break) {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
|
@ -37,7 +38,6 @@ import org.oxycblt.auxio.music.extractor.Api30MediaStoreLayer
|
||||||
import org.oxycblt.auxio.music.extractor.CacheLayer
|
import org.oxycblt.auxio.music.extractor.CacheLayer
|
||||||
import org.oxycblt.auxio.music.extractor.MetadataLayer
|
import org.oxycblt.auxio.music.extractor.MetadataLayer
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.util.TaskGuard
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
@ -62,14 +62,11 @@ import org.oxycblt.auxio.util.logW
|
||||||
* directly work with music loading, making such redundant.
|
* directly work with music loading, making such redundant.
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*
|
|
||||||
* TODO: Try to replace TaskGuard with yield when possible
|
|
||||||
*/
|
*/
|
||||||
class Indexer {
|
class Indexer {
|
||||||
private var lastResponse: Response? = null
|
private var lastResponse: Response? = null
|
||||||
private var indexingState: Indexing? = null
|
private var indexingState: Indexing? = null
|
||||||
|
|
||||||
private var guard = TaskGuard()
|
|
||||||
private var controller: Controller? = null
|
private var controller: Controller? = null
|
||||||
private var callback: Callback? = null
|
private var callback: Callback? = null
|
||||||
|
|
||||||
|
|
@ -136,21 +133,19 @@ class Indexer {
|
||||||
* complete, a new completion state will be pushed to each callback.
|
* complete, a new completion state will be pushed to each callback.
|
||||||
*/
|
*/
|
||||||
suspend fun index(context: Context) {
|
suspend fun index(context: Context) {
|
||||||
val handle = guard.newHandle()
|
|
||||||
|
|
||||||
val notGranted =
|
val notGranted =
|
||||||
ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
|
ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
|
||||||
PackageManager.PERMISSION_DENIED
|
PackageManager.PERMISSION_DENIED
|
||||||
|
|
||||||
if (notGranted) {
|
if (notGranted) {
|
||||||
emitCompletion(Response.NoPerms, handle)
|
emitCompletion(Response.NoPerms)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val response =
|
val response =
|
||||||
try {
|
try {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val library = indexImpl(context, handle)
|
val library = indexImpl(context)
|
||||||
if (library != null) {
|
if (library != null) {
|
||||||
logD(
|
logD(
|
||||||
"Music indexing completed successfully in " +
|
"Music indexing completed successfully in " +
|
||||||
|
|
@ -171,7 +166,7 @@ class Indexer {
|
||||||
Response.Err(e)
|
Response.Err(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
emitCompletion(response, handle)
|
emitCompletion(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -192,16 +187,14 @@ class Indexer {
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun cancelLast() {
|
fun cancelLast() {
|
||||||
logD("Cancelling last job")
|
logD("Cancelling last job")
|
||||||
val handle = guard.newHandle()
|
emitIndexing(null)
|
||||||
emitIndexing(null, handle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the proper music loading process. [handle] must be a truthful handle of the task calling
|
* Run the proper music loading process.
|
||||||
* this function.
|
|
||||||
*/
|
*/
|
||||||
private fun indexImpl(context: Context, handle: Long): MusicStore.Library? {
|
private suspend fun indexImpl(context: Context): MusicStore.Library? {
|
||||||
emitIndexing(Indexing.Indeterminate, handle)
|
emitIndexing(Indexing.Indeterminate)
|
||||||
|
|
||||||
// Create the chain of layers. Each layer builds on the previous layer and
|
// Create the chain of layers. Each layer builds on the previous layer and
|
||||||
// enables version-specific features in order to create the best possible music
|
// enables version-specific features in order to create the best possible music
|
||||||
|
|
@ -221,7 +214,7 @@ class Indexer {
|
||||||
|
|
||||||
val metadataLayer = MetadataLayer(context, mediaStoreLayer)
|
val metadataLayer = MetadataLayer(context, mediaStoreLayer)
|
||||||
|
|
||||||
val songs = buildSongs(metadataLayer, handle)
|
val songs = buildSongs(metadataLayer)
|
||||||
if (songs.isEmpty()) {
|
if (songs.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -248,12 +241,15 @@ class Indexer {
|
||||||
* [buildGenres] functions must be called with the returned list so that all songs are properly
|
* [buildGenres] functions must be called with the returned list so that all songs are properly
|
||||||
* linked up.
|
* linked up.
|
||||||
*/
|
*/
|
||||||
private fun buildSongs(metadataLayer: MetadataLayer, handle: Long): List<Song> {
|
private suspend fun buildSongs(metadataLayer: MetadataLayer): List<Song> {
|
||||||
|
logD("Starting indexing process")
|
||||||
|
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
|
|
||||||
// Initialize the extractor chain. This also nets us the projected total
|
// Initialize the extractor chain. This also nets us the projected total
|
||||||
// that we can show when loading.
|
// that we can show when loading.
|
||||||
val total = metadataLayer.init()
|
val total = metadataLayer.init()
|
||||||
|
yield()
|
||||||
|
|
||||||
// Note: We use a set here so we can eliminate effective duplicates of
|
// Note: We use a set here so we can eliminate effective duplicates of
|
||||||
// songs (by UID).
|
// songs (by UID).
|
||||||
|
|
@ -263,7 +259,10 @@ class Indexer {
|
||||||
metadataLayer.parse { raw ->
|
metadataLayer.parse { raw ->
|
||||||
songs.add(Song(raw))
|
songs.add(Song(raw))
|
||||||
rawSongs.add(raw)
|
rawSongs.add(raw)
|
||||||
emitIndexing(Indexing.Songs(songs.size, total), handle)
|
|
||||||
|
// Check if we got cancelled after every song addition.
|
||||||
|
yield()
|
||||||
|
emitIndexing(Indexing.Songs(songs.size, total))
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataLayer.finalize(rawSongs)
|
metadataLayer.finalize(rawSongs)
|
||||||
|
|
@ -351,15 +350,7 @@ class Indexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun emitIndexing(indexing: Indexing?, handle: Long) {
|
private fun emitIndexing(indexing: Indexing?) {
|
||||||
guard.yield(handle)
|
|
||||||
|
|
||||||
if (indexing == indexingState) {
|
|
||||||
// Ignore redundant states used when the backends just want to check for
|
|
||||||
// a cancellation
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
indexingState = indexing
|
indexingState = indexing
|
||||||
|
|
||||||
// If we have canceled the loading process, we want to revert to a previous completion
|
// If we have canceled the loading process, we want to revert to a previous completion
|
||||||
|
|
@ -371,8 +362,8 @@ class Indexer {
|
||||||
callback?.onIndexerStateChanged(state)
|
callback?.onIndexerStateChanged(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitCompletion(response: Response, handle: Long) {
|
private suspend fun emitCompletion(response: Response) {
|
||||||
guard.yield(handle)
|
yield()
|
||||||
|
|
||||||
// Swap to the Main thread so that downstream callbacks don't crash from being on
|
// Swap to the Main thread so that downstream callbacks don't crash from being on
|
||||||
// a background thread. Does not occur in emitIndexing due to efficiency reasons.
|
// a background thread. Does not occur in emitIndexing due to efficiency reasons.
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
|
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO)
|
private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO)
|
||||||
|
private var currentIndexJob: Job? = null
|
||||||
|
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
|
|
||||||
|
|
@ -118,10 +119,11 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
|
|
||||||
override fun onStartIndexing() {
|
override fun onStartIndexing() {
|
||||||
if (indexer.isIndexing) {
|
if (indexer.isIndexing) {
|
||||||
|
currentIndexJob?.cancel()
|
||||||
indexer.cancelLast()
|
indexer.cancelLast()
|
||||||
}
|
}
|
||||||
|
|
||||||
indexScope.launch { indexer.index(this@IndexerService) }
|
currentIndexJob = indexScope.launch { indexer.index(this@IndexerService) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIndexerStateChanged(state: Indexer.State?) {
|
override fun onIndexerStateChanged(state: Indexer.State?) {
|
||||||
|
|
|
||||||
|
|
@ -307,6 +307,7 @@ class PlaybackViewModel(application: Application) :
|
||||||
|
|
||||||
override fun onStateChanged(state: InternalPlayer.State) {
|
override fun onStateChanged(state: InternalPlayer.State) {
|
||||||
_isPlaying.value = state.isPlaying
|
_isPlaying.value = state.isPlaying
|
||||||
|
_positionDs.value = state.calculateElapsedPosition().msToDs()
|
||||||
|
|
||||||
// Start watching the position again
|
// Start watching the position again
|
||||||
lastPositionJob?.cancel()
|
lastPositionJob?.cancel()
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ interface InternalPlayer {
|
||||||
/** Whether the player should rewind instead of going to the previous song. */
|
/** Whether the player should rewind instead of going to the previous song. */
|
||||||
val shouldRewindWithPrev: Boolean
|
val shouldRewindWithPrev: Boolean
|
||||||
|
|
||||||
val currentState: State
|
fun makeState(durationMs: Long): State
|
||||||
|
|
||||||
/** Called when a new song should be loaded into the player. */
|
/** Called when a new song should be loaded into the player. */
|
||||||
fun loadSong(song: Song?, play: Boolean)
|
fun loadSong(song: Song?, play: Boolean)
|
||||||
|
|
|
||||||
|
|
@ -342,7 +342,7 @@ class PlaybackStateManager private constructor() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val newState = internalPlayer.currentState
|
val newState = internalPlayer.makeState(song?.durationMs ?: 0)
|
||||||
if (newState != playerState) {
|
if (newState != playerState) {
|
||||||
playerState = newState
|
playerState = newState
|
||||||
notifyStateChanged()
|
notifyStateChanged()
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.widgets.WidgetComponent
|
import org.oxycblt.auxio.widgets.WidgetComponent
|
||||||
import org.oxycblt.auxio.widgets.WidgetProvider
|
import org.oxycblt.auxio.widgets.WidgetProvider
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that manages the system-side aspects of playback, such as:
|
* A service that manages the system-side aspects of playback, such as:
|
||||||
|
|
@ -214,6 +215,60 @@ class PlaybackService :
|
||||||
logD("Service destroyed")
|
logD("Service destroyed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- CONTROLLER OVERRIDES ---
|
||||||
|
|
||||||
|
override val audioSessionId: Int
|
||||||
|
get() = player.audioSessionId
|
||||||
|
|
||||||
|
override val shouldRewindWithPrev: Boolean
|
||||||
|
get() = settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
||||||
|
|
||||||
|
override fun makeState(durationMs: Long) =
|
||||||
|
InternalPlayer.State.new(
|
||||||
|
player.playWhenReady,
|
||||||
|
player.isPlaying,
|
||||||
|
max(min(player.currentPosition, durationMs), 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun loadSong(song: Song?, play: Boolean) {
|
||||||
|
if (song == null) {
|
||||||
|
// Stop the foreground state if there's nothing to play.
|
||||||
|
logD("Nothing playing, stopping playback")
|
||||||
|
player.stop()
|
||||||
|
if (openAudioEffectSession) {
|
||||||
|
// Make sure to close the audio session when we stop playback.
|
||||||
|
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
||||||
|
openAudioEffectSession = false
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAndSave()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logD("Loading ${song.rawName}")
|
||||||
|
player.setMediaItem(MediaItem.fromUri(song.uri))
|
||||||
|
player.prepare()
|
||||||
|
|
||||||
|
if (!openAudioEffectSession) {
|
||||||
|
// Android does not like it if you start an audio effect session without having
|
||||||
|
// something within your player buffer. Make sure we only start one when we load
|
||||||
|
// a song.
|
||||||
|
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
|
||||||
|
openAudioEffectSession = true
|
||||||
|
}
|
||||||
|
|
||||||
|
player.playWhenReady = play
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun seekTo(positionMs: Long) {
|
||||||
|
logD("Seeking to ${positionMs}ms")
|
||||||
|
player.seekTo(positionMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun changePlaying(isPlaying: Boolean) {
|
||||||
|
player.playWhenReady = isPlaying
|
||||||
|
}
|
||||||
|
|
||||||
// --- PLAYER OVERRIDES ---
|
// --- PLAYER OVERRIDES ---
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
|
|
@ -270,51 +325,26 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CONTROLLER OVERRIDES ---
|
// --- MUSICSTORE OVERRIDES ---
|
||||||
|
|
||||||
override val audioSessionId: Int
|
override fun onLibraryChanged(library: MusicStore.Library?) {
|
||||||
get() = player.audioSessionId
|
if (library != null) {
|
||||||
|
playbackManager.requestAction(this)
|
||||||
override val shouldRewindWithPrev: Boolean
|
}
|
||||||
get() = settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
|
||||||
|
|
||||||
override val currentState: InternalPlayer.State
|
|
||||||
get() =
|
|
||||||
InternalPlayer.State.new(
|
|
||||||
player.playWhenReady,
|
|
||||||
player.isPlaying,
|
|
||||||
max(player.currentPosition, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun loadSong(song: Song?, play: Boolean) {
|
|
||||||
if (song == null) {
|
|
||||||
// Stop the foreground state if there's nothing to play.
|
|
||||||
logD("Nothing playing, stopping playback")
|
|
||||||
player.stop()
|
|
||||||
if (openAudioEffectSession) {
|
|
||||||
// Make sure to close the audio session when we stop playback.
|
|
||||||
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
|
||||||
openAudioEffectSession = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stopAndSave()
|
// --- SETTINGSMANAGER OVERRIDES ---
|
||||||
return
|
|
||||||
|
override fun onSettingChanged(key: String) {
|
||||||
|
if (key == getString(R.string.set_key_replay_gain) ||
|
||||||
|
key == getString(R.string.set_key_pre_amp_with) ||
|
||||||
|
key == getString(R.string.set_key_pre_amp_without)
|
||||||
|
) {
|
||||||
|
onTracksChanged(player.currentTracks)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Loading ${song.rawName}")
|
// --- OTHER FUNCTIONS ---
|
||||||
player.setMediaItem(MediaItem.fromUri(song.uri))
|
|
||||||
player.prepare()
|
|
||||||
|
|
||||||
if (!openAudioEffectSession) {
|
|
||||||
// Android does not like it if you start an audio effect session without having
|
|
||||||
// something within your player buffer. Make sure we only start one when we load
|
|
||||||
// a song.
|
|
||||||
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
|
|
||||||
openAudioEffectSession = true
|
|
||||||
}
|
|
||||||
|
|
||||||
player.playWhenReady = play
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun broadcastAudioEffectAction(event: String) {
|
private fun broadcastAudioEffectAction(event: String) {
|
||||||
sendBroadcast(
|
sendBroadcast(
|
||||||
|
|
@ -337,15 +367,6 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekTo(positionMs: Long) {
|
|
||||||
logD("Seeking to ${positionMs}ms")
|
|
||||||
player.seekTo(positionMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun changePlaying(isPlaying: Boolean) {
|
|
||||||
player.playWhenReady = isPlaying
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAction(action: InternalPlayer.Action): Boolean {
|
override fun onAction(action: InternalPlayer.Action): Boolean {
|
||||||
val library = musicStore.library
|
val library = musicStore.library
|
||||||
if (library != null) {
|
if (library != null) {
|
||||||
|
|
@ -397,27 +418,6 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MUSICSTORE OVERRIDES ---
|
|
||||||
|
|
||||||
override fun onLibraryChanged(library: MusicStore.Library?) {
|
|
||||||
if (library != null) {
|
|
||||||
playbackManager.requestAction(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- SETTINGSMANAGER OVERRIDES ---
|
|
||||||
|
|
||||||
override fun onSettingChanged(key: String) {
|
|
||||||
if (key == getString(R.string.set_key_replay_gain) ||
|
|
||||||
key == getString(R.string.set_key_pre_amp_with) ||
|
|
||||||
key == getString(R.string.set_key_pre_amp_without)
|
|
||||||
) {
|
|
||||||
onTracksChanged(player.currentTracks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- OTHER FUNCTIONS ---
|
|
||||||
|
|
||||||
/** A [BroadcastReceiver] for receiving general playback events from the system. */
|
/** A [BroadcastReceiver] for receiving general playback events from the system. */
|
||||||
private inner class PlaybackReceiver : BroadcastReceiver() {
|
private inner class PlaybackReceiver : BroadcastReceiver() {
|
||||||
private var initialHeadsetPlugEventHandled = false
|
private var initialHeadsetPlugEventHandled = false
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,11 @@ import androidx.annotation.IdRes
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
|
@ -38,7 +40,6 @@ import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.recycler.Header
|
import org.oxycblt.auxio.ui.recycler.Header
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.TaskGuard
|
|
||||||
import org.oxycblt.auxio.util.application
|
import org.oxycblt.auxio.util.application
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import java.text.Normalizer
|
import java.text.Normalizer
|
||||||
|
|
@ -62,15 +63,16 @@ class SearchViewModel(application: Application) :
|
||||||
get() = settings.searchFilterMode
|
get() = settings.searchFilterMode
|
||||||
|
|
||||||
private var lastQuery: String? = null
|
private var lastQuery: String? = null
|
||||||
private var guard = TaskGuard()
|
private var currentSearchJob: Job? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use [query] to perform a search of the music library. Will push results to [searchResults].
|
* Use [query] to perform a search of the music library. Will push results to [searchResults].
|
||||||
*/
|
*/
|
||||||
fun search(query: String?) {
|
fun search(query: String?) {
|
||||||
val handle = guard.newHandle()
|
|
||||||
lastQuery = query
|
lastQuery = query
|
||||||
|
|
||||||
|
currentSearchJob?.cancel()
|
||||||
|
|
||||||
val library = musicStore.library
|
val library = musicStore.library
|
||||||
if (query.isNullOrEmpty() || library == null) {
|
if (query.isNullOrEmpty() || library == null) {
|
||||||
logD("No music/query, ignoring search")
|
logD("No music/query, ignoring search")
|
||||||
|
|
@ -81,7 +83,7 @@ class SearchViewModel(application: Application) :
|
||||||
logD("Performing search for $query")
|
logD("Performing search for $query")
|
||||||
|
|
||||||
// Searching can be quite expensive, so get on a co-routine
|
// Searching can be quite expensive, so get on a co-routine
|
||||||
viewModelScope.launch {
|
currentSearchJob = viewModelScope.launch {
|
||||||
val sort = Sort(Sort.Mode.ByName, true)
|
val sort = Sort(Sort.Mode.ByName, true)
|
||||||
val results = mutableListOf<Item>()
|
val results = mutableListOf<Item>()
|
||||||
|
|
||||||
|
|
@ -115,7 +117,7 @@ class SearchViewModel(application: Application) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard.yield(handle)
|
yield()
|
||||||
_searchResults.value = results
|
_searchResults.value = results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import android.os.Looper
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.util.concurrent.CancellationException
|
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
/** Assert that we are on a background thread. */
|
/** Assert that we are on a background thread. */
|
||||||
|
|
@ -57,34 +56,3 @@ fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {
|
||||||
fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy {
|
fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy {
|
||||||
clazz.java.getDeclaredMethod(method).also { it.isAccessible = true }
|
clazz.java.getDeclaredMethod(method).also { it.isAccessible = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* An abstraction that allows cheap cooperative multi-threading in shared object contexts. Every new
|
|
||||||
* task should call [newHandle], while every running task should call [check] or [yield] depending
|
|
||||||
* on the situation to determine if it should continue. Failure to follow the expectations of this
|
|
||||||
* class will result in bugs.
|
|
||||||
*
|
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
|
||||||
class TaskGuard {
|
|
||||||
private var currentHandle = 0L
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new handle to the calling task while invalidating the handle of the previous task.
|
|
||||||
*/
|
|
||||||
@Synchronized fun newHandle() = ++currentHandle
|
|
||||||
|
|
||||||
/** Check if the given [handle] is still valid. */
|
|
||||||
@Synchronized fun check(handle: Long) = handle == currentHandle
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative to [kotlinx.coroutines.yield], that achieves the same behavior but in a much
|
|
||||||
* cheaper manner.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun yield(handle: Long) {
|
|
||||||
if (!check(handle)) {
|
|
||||||
throw CancellationException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue