From 604145fb69c58cd733ec1b485ddac77cfbb2dd4f Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Wed, 11 Nov 2020 19:27:38 -0700 Subject: [PATCH] Improve code slightly Add some documentation and remove useless code/bugs. --- .../oxycblt/auxio/loading/LoadingViewModel.kt | 22 ++------ .../org/oxycblt/auxio/music/MusicStore.kt | 6 +-- .../org/oxycblt/auxio/music/coil/CoilUtils.kt | 31 ++++-------- .../auxio/playback/CompactPlaybackFragment.kt | 7 +++ .../auxio/playback/NotificationUtils.kt | 2 +- .../auxio/playback/PlaybackFragment.kt | 11 +++- .../oxycblt/auxio/playback/PlaybackService.kt | 14 +++++- .../auxio/playback/PlaybackViewModel.kt | 9 +++- .../auxio/playback/queue/QueueDragCallback.kt | 7 +-- .../auxio/playback/queue/QueueFragment.kt | 1 - .../playback/state/PlaybackStateManager.kt | 50 +++++++++++-------- .../org/oxycblt/auxio/recycler/ShowMode.kt | 4 +- 12 files changed, 88 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt index 41d111758..31946a04b 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt @@ -5,21 +5,14 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import kotlinx.coroutines.CoroutineScope +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.processing.MusicLoaderResponse class LoadingViewModel(private val app: Application) : ViewModel() { - // Coroutine - private val loadingJob = Job() - private val ioScope = CoroutineScope( - loadingJob + Dispatchers.IO - ) - // UI control private val mResponse = MutableLiveData() val response: LiveData get() = mResponse @@ -42,10 +35,12 @@ class LoadingViewModel(private val app: Application) : ViewModel() { } private fun doLoad() { - ioScope.launch { + viewModelScope.launch { val musicStore = MusicStore.getInstance() - val response = musicStore.load(app) + val response = withContext(Dispatchers.IO) { + return@withContext musicStore.load(app) + } withContext(Dispatchers.Main) { mResponse.value = response @@ -75,13 +70,6 @@ class LoadingViewModel(private val app: Application) : ViewModel() { 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 { @Suppress("unchecked_cast") override fun create(modelClass: Class): T { diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index 1a9996b7f..1cc16182c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -8,7 +8,9 @@ import org.oxycblt.auxio.music.processing.MusicLoaderResponse import org.oxycblt.auxio.music.processing.MusicSorter 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() { private var mGenres = listOf() val genres: List get() = mGenres @@ -70,9 +72,7 @@ class MusicStore private constructor() { this::class.simpleName, "Music load completed successfully in ${elapsed}ms." ) - } - if (loader.response == MusicLoaderResponse.DONE) { loaded = true } diff --git a/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt b/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt index e0a26010b..dcf31470b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt @@ -98,28 +98,17 @@ fun ImageView.bindGenreImage(genre: Genre) { 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. - for (i in 0..3) { - val artist = genre.artists[i] + // Get the Nth cover from each artist, if possible. + for (i in 0..3) { + val artist = genre.artists[i] - uris.add( - if (artist.albums.size > i) { - artist.albums[i].coverUri - } else { - artist.albums[0].coverUri - } - ) - } + uris.add( + if (artist.albums.size > i) { + artist.albums[i].coverUri + } else { + artist.albums[0].coverUri + } + ) } val fetcher = MosaicFetcher(context) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt index b0bb1d542..552371da9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt @@ -15,6 +15,13 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding 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() { private val playbackModel: PlaybackViewModel by activityViewModels() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/NotificationUtils.kt b/app/src/main/java/org/oxycblt/auxio/playback/NotificationUtils.kt index 6efa89ae4..f37d66821 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/NotificationUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/NotificationUtils.kt @@ -36,7 +36,6 @@ fun NotificationManager.createMediaNotification( context: Context, mediaSession: MediaSessionCompat ): NotificationCompat.Builder { - // Create a notification channel if required if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 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") fun NotificationCompat.Builder.updatePlaying(context: Context) { mActions[2] = newAction(NotificationUtils.ACTION_PLAY_PAUSE, context) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index ec299676c..10c2ec1aa 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -19,7 +19,12 @@ import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.ui.accent 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 { private val playbackModel: PlaybackViewModel by activityViewModels() @@ -30,6 +35,8 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { ): View? { val binding = FragmentPlaybackBinding.inflate(inflater) + // TODO: Add a swipe-to-next-track function using a ViewPager + // Create accents & icons to use val accentColor = ColorStateList.valueOf(accent.first.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) { - if (it.isEmpty() && playbackModel.queue.value!!.isEmpty()) { + if (it.isEmpty() && playbackModel.nextItemsInQueue.value!!.isEmpty()) { queueMenuItem.isEnabled = false queueMenuItem.icon = iconQueueInactive } else { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt index 7995b26c1..dba3c6cad 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt @@ -40,8 +40,18 @@ import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.PlaybackMode 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 { private val player: SimpleExoPlayer by lazy { SimpleExoPlayer.Builder(applicationContext).build() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index ab824a049..96ee14a64 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -15,8 +15,10 @@ import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.PlaybackMode 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 { // Playback private val mSong = MutableLiveData() @@ -198,6 +200,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { from -= 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) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt index b88933e62..04c1e2b88 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt @@ -14,15 +14,12 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ): Int { - // Make header objects unswipable by only returning the swipe flags if the ViewHolder - // is for a queue item. + // Only allow dragging/swiping with the queue item ViewHolder, not the header items. return if (viewHolder is QueueAdapter.ViewHolder) { makeFlag( ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN ) or makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START) - } else { - 0 - } + } else 0 } override fun interpolateOutOfBoundsScroll( diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index fd044979c..d92c8d3a8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -49,7 +49,6 @@ class QueueFragment : Fragment() { } queueAdapter.submitList(createQueueDisplay()) { - binding.queueRecycler.scrollToPosition(0) scrollRecyclerIfNeeded(binding) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index c45e75684..961bb94bf 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -10,12 +10,14 @@ import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song 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/. -// If you want to add system-side things, add to PlaybackService. -// If you want to add ui-side things, add to PlaybackViewModel. -// [Yes, I know MediaSessionCompat exists, but I like having full control over the -// playback state instead of dealing with android's likely buggy code.] +/** + * Master class for the playback state. This should ***not*** be used outside of the playback module. + * - If you want to show the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel]. + * - If you want to add to the system aspects or the exoplayer instance, use [org.oxycblt.auxio.playback.PlaybackService]. + * + * All instantiation should be done with [PlaybackStateManager.from()]. + * @author OxygenCobalt + */ class PlaybackStateManager private constructor() { // Playback private var mSong: Song? = null @@ -438,10 +440,32 @@ class PlaybackStateManager private constructor() { 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) {} + fun onUserQueueUpdate(userQueue: MutableList) {} + 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 { @Volatile private var INSTANCE: PlaybackStateManager? = null + /** + * Get/Instantiate the single instance of [PlaybackStateManager]. + */ fun getInstance(): PlaybackStateManager { 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) {} - fun onUserQueueUpdate(userQueue: MutableList) {} - fun onModeUpdate(mode: PlaybackMode) {} - fun onIndexUpdate(index: Int) {} - fun onPlayingUpdate(isPlaying: Boolean) {} - fun onShuffleUpdate(isShuffling: Boolean) {} - fun onLoopUpdate(mode: LoopMode) {} - fun onSeekConfirm(position: Long) {} - } } diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/ShowMode.kt b/app/src/main/java/org/oxycblt/auxio/recycler/ShowMode.kt index d434cf0c5..0d04a75f4 100644 --- a/app/src/main/java/org/oxycblt/auxio/recycler/ShowMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/recycler/ShowMode.kt @@ -1,8 +1,8 @@ package org.oxycblt.auxio.recycler // TODO: Swap these temp values for actual constants -enum class ShowMode(val constant: Long) { - SHOW_GENRES(0), SHOW_ARTISTS(1), SHOW_ALBUMS(2), SHOW_SONGS(3); +enum class ShowMode { + SHOW_GENRES, SHOW_ARTISTS, SHOW_ALBUMS, SHOW_SONGS; // Make a slice of all the values that this ShowMode covers. // ex. SHOW_ARTISTS would return SHOW_ARTISTS, SHOW_ALBUMS, and SHOW_SONGS