Improve code slightly

Add some documentation and remove useless code/bugs.
This commit is contained in:
OxygenCobalt 2020-11-11 19:27:38 -07:00
parent 2c783beaba
commit 604145fb69
12 changed files with 88 additions and 76 deletions

View file

@ -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<MusicLoaderResponse>()
val response: LiveData<MusicLoaderResponse> 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 <T : ViewModel?> create(modelClass: Class<T>): T {

View file

@ -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<Genre>()
val genres: List<Genre> 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
}

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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 {

View file

@ -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()

View file

@ -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<Song?>()
@ -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)

View file

@ -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(

View file

@ -49,7 +49,6 @@ class QueueFragment : Fragment() {
}
queueAdapter.submitList(createQueueDisplay()) {
binding.queueRecycler.scrollToPosition(0)
scrollRecyclerIfNeeded(binding)
}
}

View file

@ -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<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 {
@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<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) {}
}
}

View file

@ -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