Improve code slightly
Add some documentation and remove useless code/bugs.
This commit is contained in:
parent
2c783beaba
commit
604145fb69
12 changed files with 88 additions and 76 deletions
|
@ -5,21 +5,14 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||||
|
|
||||||
class LoadingViewModel(private val app: Application) : ViewModel() {
|
class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||||
// Coroutine
|
|
||||||
private val loadingJob = Job()
|
|
||||||
private val ioScope = CoroutineScope(
|
|
||||||
loadingJob + Dispatchers.IO
|
|
||||||
)
|
|
||||||
|
|
||||||
// UI control
|
// UI control
|
||||||
private val mResponse = MutableLiveData<MusicLoaderResponse>()
|
private val mResponse = MutableLiveData<MusicLoaderResponse>()
|
||||||
val response: LiveData<MusicLoaderResponse> get() = mResponse
|
val response: LiveData<MusicLoaderResponse> get() = mResponse
|
||||||
|
@ -42,10 +35,12 @@ class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doLoad() {
|
private fun doLoad() {
|
||||||
ioScope.launch {
|
viewModelScope.launch {
|
||||||
val musicStore = MusicStore.getInstance()
|
val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
val response = musicStore.load(app)
|
val response = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext musicStore.load(app)
|
||||||
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
mResponse.value = response
|
mResponse.value = response
|
||||||
|
@ -75,13 +70,6 @@ class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||||
mDoGrant.value = false
|
mDoGrant.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
|
|
||||||
// Cancel the current loading job if the app has been stopped
|
|
||||||
loadingJob.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Factory(private val application: Application) : ViewModelProvider.Factory {
|
class Factory(private val application: Application) : ViewModelProvider.Factory {
|
||||||
@Suppress("unchecked_cast")
|
@Suppress("unchecked_cast")
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
|
|
@ -8,7 +8,9 @@ import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||||
import org.oxycblt.auxio.music.processing.MusicSorter
|
import org.oxycblt.auxio.music.processing.MusicSorter
|
||||||
import org.oxycblt.auxio.recycler.ShowMode
|
import org.oxycblt.auxio.recycler.ShowMode
|
||||||
|
|
||||||
// Storage for Music Data. Only use getInstance() to access this object.
|
/**
|
||||||
|
* The main storage for music items. Use [MusicStore.from()] to get the instance.
|
||||||
|
*/
|
||||||
class MusicStore private constructor() {
|
class MusicStore private constructor() {
|
||||||
private var mGenres = listOf<Genre>()
|
private var mGenres = listOf<Genre>()
|
||||||
val genres: List<Genre> get() = mGenres
|
val genres: List<Genre> get() = mGenres
|
||||||
|
@ -70,9 +72,7 @@ class MusicStore private constructor() {
|
||||||
this::class.simpleName,
|
this::class.simpleName,
|
||||||
"Music load completed successfully in ${elapsed}ms."
|
"Music load completed successfully in ${elapsed}ms."
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
if (loader.response == MusicLoaderResponse.DONE) {
|
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,16 +98,6 @@ fun ImageView.bindGenreImage(genre: Genre) {
|
||||||
|
|
||||||
Log.d(this::class.simpleName, genre.numAlbums.toString())
|
Log.d(this::class.simpleName, genre.numAlbums.toString())
|
||||||
|
|
||||||
// Try to create a 4x4 mosaic if possible, if not, just create a 2x2 mosaic.
|
|
||||||
if (genre.numAlbums >= 16) {
|
|
||||||
while (uris.size < 16) {
|
|
||||||
genre.artists.forEach { artist ->
|
|
||||||
artist.albums.forEach {
|
|
||||||
uris.add(it.coverUri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Get the Nth cover from each artist, if possible.
|
// Get the Nth cover from each artist, if possible.
|
||||||
for (i in 0..3) {
|
for (i in 0..3) {
|
||||||
val artist = genre.artists[i]
|
val artist = genre.artists[i]
|
||||||
|
@ -120,7 +110,6 @@ fun ImageView.bindGenreImage(genre: Genre) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val fetcher = MosaicFetcher(context)
|
val fetcher = MosaicFetcher(context)
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,13 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
|
import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Fragment] that displays the currently played song at a glance, with some basic controls.
|
||||||
|
* Extends into [PlaybackFragment] when clicked on.
|
||||||
|
*
|
||||||
|
* Instantiation is done by the navigation component, **do not instantiate this fragment manually.**
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
class CompactPlaybackFragment : Fragment() {
|
class CompactPlaybackFragment : Fragment() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,6 @@ fun NotificationManager.createMediaNotification(
|
||||||
context: Context,
|
context: Context,
|
||||||
mediaSession: MediaSessionCompat
|
mediaSession: MediaSessionCompat
|
||||||
): NotificationCompat.Builder {
|
): NotificationCompat.Builder {
|
||||||
|
|
||||||
// Create a notification channel if required
|
// Create a notification channel if required
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
|
@ -97,6 +96,7 @@ fun NotificationCompat.Builder.setMetadata(song: Song, context: Context, onDone:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I have no idea how to update actions on the fly so I have to use these restricted APIs.
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
fun NotificationCompat.Builder.updatePlaying(context: Context) {
|
fun NotificationCompat.Builder.updatePlaying(context: Context) {
|
||||||
mActions[2] = newAction(NotificationUtils.ACTION_PLAY_PAUSE, context)
|
mActions[2] = newAction(NotificationUtils.ACTION_PLAY_PAUSE, context)
|
||||||
|
|
|
@ -19,7 +19,12 @@ import org.oxycblt.auxio.playback.state.LoopMode
|
||||||
import org.oxycblt.auxio.ui.accent
|
import org.oxycblt.auxio.ui.accent
|
||||||
import org.oxycblt.auxio.ui.toColor
|
import org.oxycblt.auxio.ui.toColor
|
||||||
|
|
||||||
// TODO: Add a swipe-to-next-track function using a ViewPager
|
/**
|
||||||
|
* A [Fragment] that displays more information about the song, along with more media controls.
|
||||||
|
*
|
||||||
|
* Instantiation is done by the navigation component, **do not instantiate this fragment manually.**
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
||||||
|
@ -30,6 +35,8 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
): View? {
|
): View? {
|
||||||
val binding = FragmentPlaybackBinding.inflate(inflater)
|
val binding = FragmentPlaybackBinding.inflate(inflater)
|
||||||
|
|
||||||
|
// TODO: Add a swipe-to-next-track function using a ViewPager
|
||||||
|
|
||||||
// Create accents & icons to use
|
// Create accents & icons to use
|
||||||
val accentColor = ColorStateList.valueOf(accent.first.toColor(requireContext()))
|
val accentColor = ColorStateList.valueOf(accent.first.toColor(requireContext()))
|
||||||
val controlColor = ColorStateList.valueOf(R.color.control_color.toColor(requireContext()))
|
val controlColor = ColorStateList.valueOf(R.color.control_color.toColor(requireContext()))
|
||||||
|
@ -173,7 +180,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackModel.userQueue.observe(viewLifecycleOwner) {
|
playbackModel.userQueue.observe(viewLifecycleOwner) {
|
||||||
if (it.isEmpty() && playbackModel.queue.value!!.isEmpty()) {
|
if (it.isEmpty() && playbackModel.nextItemsInQueue.value!!.isEmpty()) {
|
||||||
queueMenuItem.isEnabled = false
|
queueMenuItem.isEnabled = false
|
||||||
queueMenuItem.icon = iconQueueInactive
|
queueMenuItem.icon = iconQueueInactive
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -40,8 +40,18 @@ import org.oxycblt.auxio.playback.state.LoopMode
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
|
||||||
// A Service that manages the single ExoPlayer instance and manages the system-side
|
/**
|
||||||
// aspects of playback.
|
* A service that manages the system-side aspects of playback, such as:
|
||||||
|
* - The single [SimpleExoPlayer] instance.
|
||||||
|
* - The [MediaSessionCompat]
|
||||||
|
* - The Media Notification
|
||||||
|
* - Audio Focus
|
||||||
|
* - Headset management
|
||||||
|
*
|
||||||
|
* This service relies on [PlaybackStateManager.Callback], so therefore there's no need to bind
|
||||||
|
* to it to deliver commands.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Callback {
|
class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Callback {
|
||||||
private val player: SimpleExoPlayer by lazy {
|
private val player: SimpleExoPlayer by lazy {
|
||||||
SimpleExoPlayer.Builder(applicationContext).build()
|
SimpleExoPlayer.Builder(applicationContext).build()
|
||||||
|
|
|
@ -15,8 +15,10 @@ import org.oxycblt.auxio.playback.state.LoopMode
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
|
||||||
// A ViewModel that acts as an intermediary between the UI and PlaybackStateManager
|
/**
|
||||||
// TODO: Implement Persistence through a Database
|
* The ViewModel that provides a UI-Focused frontend for [PlaybackStateManager].
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
// Playback
|
// Playback
|
||||||
private val mSong = MutableLiveData<Song?>()
|
private val mSong = MutableLiveData<Song?>()
|
||||||
|
@ -198,6 +200,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
|
||||||
from -= mUserQueue.value!!.size.inc()
|
from -= mUserQueue.value!!.size.inc()
|
||||||
to -= mUserQueue.value!!.size.inc()
|
to -= mUserQueue.value!!.size.inc()
|
||||||
|
|
||||||
|
// Ignore movements that are past the next songs
|
||||||
|
if (to <= mIndex.value!!) return false
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackManager.moveQueueItems(from, to)
|
playbackManager.moveQueueItems(from, to)
|
||||||
|
|
|
@ -14,15 +14,12 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
viewHolder: RecyclerView.ViewHolder
|
viewHolder: RecyclerView.ViewHolder
|
||||||
): Int {
|
): Int {
|
||||||
// Make header objects unswipable by only returning the swipe flags if the ViewHolder
|
// Only allow dragging/swiping with the queue item ViewHolder, not the header items.
|
||||||
// is for a queue item.
|
|
||||||
return if (viewHolder is QueueAdapter.ViewHolder) {
|
return if (viewHolder is QueueAdapter.ViewHolder) {
|
||||||
makeFlag(
|
makeFlag(
|
||||||
ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
||||||
) or makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)
|
) or makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)
|
||||||
} else {
|
} else 0
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun interpolateOutOfBoundsScroll(
|
override fun interpolateOutOfBoundsScroll(
|
||||||
|
|
|
@ -49,7 +49,6 @@ class QueueFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
queueAdapter.submitList(createQueueDisplay()) {
|
queueAdapter.submitList(createQueueDisplay()) {
|
||||||
binding.queueRecycler.scrollToPosition(0)
|
|
||||||
scrollRecyclerIfNeeded(binding)
|
scrollRecyclerIfNeeded(binding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,14 @@ import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
// The manager of the current playback state [Current Song, Queue, Shuffling]
|
/**
|
||||||
// This class is for sole use by the code in /playback/.
|
* Master class for the playback state. This should ***not*** be used outside of the playback module.
|
||||||
// If you want to add system-side things, add to PlaybackService.
|
* - If you want to show the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel].
|
||||||
// If you want to add ui-side things, add to PlaybackViewModel.
|
* - If you want to add to the system aspects or the exoplayer instance, use [org.oxycblt.auxio.playback.PlaybackService].
|
||||||
// [Yes, I know MediaSessionCompat exists, but I like having full control over the
|
*
|
||||||
// playback state instead of dealing with android's likely buggy code.]
|
* All instantiation should be done with [PlaybackStateManager.from()].
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
class PlaybackStateManager private constructor() {
|
class PlaybackStateManager private constructor() {
|
||||||
// Playback
|
// Playback
|
||||||
private var mSong: Song? = null
|
private var mSong: Song? = null
|
||||||
|
@ -438,10 +440,32 @@ class PlaybackStateManager private constructor() {
|
||||||
return final
|
return final
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interface for receiving updates from [PlaybackStateManager].
|
||||||
|
* Add the callback to [PlaybackStateManager] using [addCallback],
|
||||||
|
* remove them on destruction with [removeCallback].
|
||||||
|
*/
|
||||||
|
interface Callback {
|
||||||
|
fun onSongUpdate(song: Song?) {}
|
||||||
|
fun onParentUpdate(parent: BaseModel?) {}
|
||||||
|
fun onPositionUpdate(position: Long) {}
|
||||||
|
fun onQueueUpdate(queue: MutableList<Song>) {}
|
||||||
|
fun onUserQueueUpdate(userQueue: MutableList<Song>) {}
|
||||||
|
fun onModeUpdate(mode: PlaybackMode) {}
|
||||||
|
fun onIndexUpdate(index: Int) {}
|
||||||
|
fun onPlayingUpdate(isPlaying: Boolean) {}
|
||||||
|
fun onShuffleUpdate(isShuffling: Boolean) {}
|
||||||
|
fun onLoopUpdate(mode: LoopMode) {}
|
||||||
|
fun onSeekConfirm(position: Long) {}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile
|
@Volatile
|
||||||
private var INSTANCE: PlaybackStateManager? = null
|
private var INSTANCE: PlaybackStateManager? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/Instantiate the single instance of [PlaybackStateManager].
|
||||||
|
*/
|
||||||
fun getInstance(): PlaybackStateManager {
|
fun getInstance(): PlaybackStateManager {
|
||||||
val currentInstance = INSTANCE
|
val currentInstance = INSTANCE
|
||||||
|
|
||||||
|
@ -456,18 +480,4 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
fun onSongUpdate(song: Song?) {}
|
|
||||||
fun onParentUpdate(parent: BaseModel?) {}
|
|
||||||
fun onPositionUpdate(position: Long) {}
|
|
||||||
fun onQueueUpdate(queue: MutableList<Song>) {}
|
|
||||||
fun onUserQueueUpdate(userQueue: MutableList<Song>) {}
|
|
||||||
fun onModeUpdate(mode: PlaybackMode) {}
|
|
||||||
fun onIndexUpdate(index: Int) {}
|
|
||||||
fun onPlayingUpdate(isPlaying: Boolean) {}
|
|
||||||
fun onShuffleUpdate(isShuffling: Boolean) {}
|
|
||||||
fun onLoopUpdate(mode: LoopMode) {}
|
|
||||||
fun onSeekConfirm(position: Long) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package org.oxycblt.auxio.recycler
|
package org.oxycblt.auxio.recycler
|
||||||
|
|
||||||
// TODO: Swap these temp values for actual constants
|
// TODO: Swap these temp values for actual constants
|
||||||
enum class ShowMode(val constant: Long) {
|
enum class ShowMode {
|
||||||
SHOW_GENRES(0), SHOW_ARTISTS(1), SHOW_ALBUMS(2), SHOW_SONGS(3);
|
SHOW_GENRES, SHOW_ARTISTS, SHOW_ALBUMS, SHOW_SONGS;
|
||||||
|
|
||||||
// Make a slice of all the values that this ShowMode covers.
|
// Make a slice of all the values that this ShowMode covers.
|
||||||
// ex. SHOW_ARTISTS would return SHOW_ARTISTS, SHOW_ALBUMS, and SHOW_SONGS
|
// ex. SHOW_ARTISTS would return SHOW_ARTISTS, SHOW_ALBUMS, and SHOW_SONGS
|
||||||
|
|
Loading…
Reference in a new issue