Update documentation

Completely update the documentation throughout the app.
This commit is contained in:
OxygenCobalt 2021-02-25 11:36:47 -07:00
parent b9720286c3
commit e9abee9f64
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
46 changed files with 481 additions and 415 deletions

View file

@ -60,11 +60,11 @@ class MainActivity : AppCompatActivity() {
// to PlaybackViewModel to be used later.
if (intent != null) {
val action = intent.action
val isConsumed = intent.getBooleanExtra(KEY_INTENT_CONSUMED, false)
val isConsumed = intent.getBooleanExtra(KEY_INTENT_USED, false)
if (action == Intent.ACTION_VIEW && !isConsumed) {
// Mark the intent as used so this does not fire again
intent.putExtra(KEY_INTENT_CONSUMED, true)
intent.putExtra(KEY_INTENT_USED, true)
intent.data?.let { fileUri ->
playbackModel.playWithUri(fileUri, this)
@ -100,6 +100,6 @@ class MainActivity : AppCompatActivity() {
}
companion object {
private const val KEY_INTENT_CONSUMED = "KEY_FILE_INTENT_USED"
private const val KEY_INTENT_USED = "KEY_FILE_INTENT_USED"
}
}

View file

@ -20,6 +20,7 @@ import java.io.InputStream
/**
* Fetcher that returns the album art for a given [Album]. Handles settings on whether to use
* quality covers or not.
* @author OxygenCobalt
*/
class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
private val settingsManager = SettingsManager.getInstance()

View file

@ -19,7 +19,7 @@ import org.oxycblt.auxio.settings.SettingsManager
// --- BINDING ADAPTERS ---
/**
* Bind the album art for a [Song].
* Bind the album art for a [song].
*/
@BindingAdapter("albumArt")
fun ImageView.bindAlbumArt(song: Song) {
@ -27,7 +27,7 @@ fun ImageView.bindAlbumArt(song: Song) {
}
/**
* Bind the album art for an [Album].
* Bind the album art for an [album].
*/
@BindingAdapter("albumArt")
fun ImageView.bindAlbumArt(album: Album) {
@ -35,7 +35,7 @@ fun ImageView.bindAlbumArt(album: Album) {
}
/**
* Bind the image for an [Artist]
* Bind the image for an [artist]
*/
@BindingAdapter("artistImage")
fun ImageView.bindArtistImage(artist: Artist) {
@ -43,7 +43,7 @@ fun ImageView.bindArtistImage(artist: Artist) {
}
/**
* Bind the image for a [Genre]
* Bind the image for a [genre]
*/
@BindingAdapter("genreImage")
fun ImageView.bindGenreImage(genre: Genre) {

View file

@ -2,7 +2,6 @@ package org.oxycblt.auxio.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.os.Looper
@ -35,9 +34,7 @@ class PlaybackStateDatabase(context: Context) :
// --- DATABASE CONSTRUCTION FUNCTIONS ---
/**
* Create a table
* @param database DB to create the tables on
* @param tableName The name of the table to create.
* Create a table for this database.
*/
private fun createTable(database: SQLiteDatabase, tableName: String) {
val command = StringBuilder()
@ -106,15 +103,17 @@ class PlaybackStateDatabase(context: Context) :
val stateData = ContentValues(9)
stateData.put(PlaybackState.COLUMN_ID, state.id)
stateData.put(PlaybackState.COLUMN_SONG_NAME, state.songName)
stateData.put(PlaybackState.COLUMN_POSITION, state.position)
stateData.put(PlaybackState.COLUMN_PARENT_NAME, state.parentName)
stateData.put(PlaybackState.COLUMN_INDEX, state.index)
stateData.put(PlaybackState.COLUMN_MODE, state.mode)
stateData.put(PlaybackState.COLUMN_IS_SHUFFLING, state.isShuffling)
stateData.put(PlaybackState.COLUMN_LOOP_MODE, state.loopMode)
stateData.put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue)
stateData.apply {
put(PlaybackState.COLUMN_ID, state.id)
put(PlaybackState.COLUMN_SONG_NAME, state.songName)
put(PlaybackState.COLUMN_POSITION, state.position)
put(PlaybackState.COLUMN_PARENT_NAME, state.parentName)
put(PlaybackState.COLUMN_INDEX, state.index)
put(PlaybackState.COLUMN_MODE, state.mode)
put(PlaybackState.COLUMN_IS_SHUFFLING, state.isShuffling)
put(PlaybackState.COLUMN_LOOP_MODE, state.loopMode)
put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue)
}
database.insert(TABLE_NAME_STATE, null, stateData)
database.setTransactionSuccessful()
@ -135,10 +134,9 @@ class PlaybackStateDatabase(context: Context) :
val database = writableDatabase
var state: PlaybackState? = null
val stateCursor: Cursor
try {
stateCursor = database.query(
val stateCursor = database.query(
TABLE_NAME_STATE,
null, null, null,
null, null, null
@ -179,10 +177,9 @@ class PlaybackStateDatabase(context: Context) :
}
/**
* Write a list of [QueueItem]s to the database, clearing the previous queue present.
* @param queue The list of [QueueItem]s to be written.
* Write a list of [queueItems] to the database, clearing the previous queue present.
*/
fun writeQueue(queue: List<QueueItem>) {
fun writeQueue(queueItems: List<QueueItem>) {
assertBackgroundThread()
val database = readableDatabase
@ -202,21 +199,23 @@ class PlaybackStateDatabase(context: Context) :
var position = 0
// Try to write out the entirety of the queue, any failed inserts will be skipped.
while (position < queue.size) {
while (position < queueItems.size) {
database.beginTransaction()
var i = position
try {
while (i < queue.size) {
val item = queue[i]
while (i < queueItems.size) {
val item = queueItems[i]
val itemData = ContentValues(4)
i++
itemData.put(QueueItem.COLUMN_ID, item.id)
itemData.put(QueueItem.COLUMN_SONG_NAME, item.songName)
itemData.put(QueueItem.COLUMN_ALBUM_NAME, item.albumName)
itemData.put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue)
itemData.apply {
put(QueueItem.COLUMN_ID, item.id)
put(QueueItem.COLUMN_SONG_NAME, item.songName)
put(QueueItem.COLUMN_ALBUM_NAME, item.albumName)
put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue)
}
database.insert(TABLE_NAME_QUEUE, null, itemData)
}
@ -242,12 +241,10 @@ class PlaybackStateDatabase(context: Context) :
assertBackgroundThread()
val database = readableDatabase
val queueItems = mutableListOf<QueueItem>()
val queueCursor: Cursor
try {
queueCursor = database.query(
val queueCursor = database.query(
TABLE_NAME_QUEUE, null, null,
null, null, null, null
)

View file

@ -17,6 +17,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.recycler.CenterSmoothScroller
import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.canScroll
import org.oxycblt.auxio.ui.createToast
import org.oxycblt.auxio.ui.newMenu
@ -83,44 +84,42 @@ class AlbumDetailFragment : DetailFragment() {
}
detailModel.navToItem.observe(viewLifecycleOwner) {
if (it != null) {
when (it) {
// Songs should be scrolled to if the album matches, or a new detail
// fragment should be launched otherwise.
is Song -> {
if (detailModel.currentAlbum.value!!.id == it.album.id) {
scrollToItem(it.id)
when (it) {
// Songs should be scrolled to if the album matches, or a new detail
// fragment should be launched otherwise.
is Song -> {
if (detailModel.currentAlbum.value!!.id == it.album.id) {
scrollToItem(it.id)
detailModel.doneWithNavToItem()
} else {
findNavController().navigate(
AlbumDetailFragmentDirections.actionShowAlbum(it.album.id)
)
}
}
// If the album matches, no need to do anything. Otherwise launch a new
// detail fragment.
is Album -> {
if (detailModel.currentAlbum.value!!.id == it.id) {
binding.detailRecycler.scrollToPosition(0)
detailModel.doneWithNavToItem()
} else {
findNavController().navigate(
AlbumDetailFragmentDirections.actionShowAlbum(it.id)
)
}
}
// Always launch a new ArtistDetailFragment.
is Artist -> {
detailModel.doneWithNavToItem()
} else {
findNavController().navigate(
AlbumDetailFragmentDirections.actionShowArtist(it.id)
AlbumDetailFragmentDirections.actionShowAlbum(it.album.id)
)
}
else -> {}
}
// If the album matches, no need to do anything. Otherwise launch a new
// detail fragment.
is Album -> {
if (detailModel.currentAlbum.value!!.id == it.id) {
binding.detailRecycler.scrollToPosition(0)
detailModel.doneWithNavToItem()
} else {
findNavController().navigate(
AlbumDetailFragmentDirections.actionShowAlbum(it.id)
)
}
}
// Always launch a new ArtistDetailFragment.
is Artist -> {
findNavController().navigate(
AlbumDetailFragmentDirections.actionShowArtist(it.id)
)
}
else -> {}
}
}
@ -148,6 +147,9 @@ class AlbumDetailFragment : DetailFragment() {
return binding.root
}
/**
* Scroll to an song using its [id].
*/
private fun scrollToItem(id: Long) {
// Calculate where the item for the currently played song is
val pos = detailModel.albumSortMode.value!!.getSortedSongList(
@ -164,9 +166,7 @@ class AlbumDetailFragment : DetailFragment() {
// If the recyclerview can scroll, its certain that it will have to scroll to
// correctly center the playing item, so make sure that the Toolbar is lifted in
// that case.
if (binding.detailRecycler.computeVerticalScrollRange() > binding.detailRecycler.height) {
binding.detailAppbar.isLifted = true
}
binding.detailAppbar.isLifted = binding.detailRecycler.canScroll()
}
}
}

View file

@ -51,6 +51,8 @@ abstract class DetailFragment : Fragment() {
/**
* Shortcut method for doing setup of the detail toolbar.
* @param menu Menu resource to use
* @param onMenuClick (Optional) a click listener for that menu
*/
protected fun setupToolbar(
@MenuRes menu: Int = -1,

View file

@ -12,40 +12,43 @@ import org.oxycblt.auxio.recycler.SortMode
/**
* ViewModel that stores data for the [DetailFragment]s, such as what they're showing & what
* [SortMode] they are currently on.
* @author OxygenCobalt
*/
class DetailViewModel : ViewModel() {
private var mIsNavigating = false
val isNavigating: Boolean get() = mIsNavigating
private val mCurrentGenre = MutableLiveData<Genre?>()
private val mCurrentArtist = MutableLiveData<Artist?>()
private val mCurrentAlbum = MutableLiveData<Album?>()
private val mGenreSortMode = MutableLiveData(SortMode.ALPHA_DOWN)
val genreSortMode: LiveData<SortMode> get() = mGenreSortMode
private val mArtistSortMode = MutableLiveData(SortMode.NUMERIC_DOWN)
val artistSortMode: LiveData<SortMode> get() = mArtistSortMode
private val mAlbumSortMode = MutableLiveData(SortMode.NUMERIC_DOWN)
val albumSortMode: LiveData<SortMode> get() = mAlbumSortMode
// Current music models being shown
private val mCurrentGenre = MutableLiveData<Genre?>()
private val mNavToItem = MutableLiveData<BaseModel?>()
private var mIsNavigating = false
val currentGenre: LiveData<Genre?> get() = mCurrentGenre
private val mCurrentArtist = MutableLiveData<Artist?>()
val currentArtist: LiveData<Artist?> get() = mCurrentArtist
private val mCurrentAlbum = MutableLiveData<Album?>()
val currentAlbum: LiveData<Album?> get() = mCurrentAlbum
// Primary navigation flag.
private val mNavToItem = MutableLiveData<BaseModel?>()
val genreSortMode: LiveData<SortMode> get() = mGenreSortMode
val albumSortMode: LiveData<SortMode> get() = mAlbumSortMode
val artistSortMode: LiveData<SortMode> get() = mArtistSortMode
val isNavigating: Boolean get() = mIsNavigating
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
val navToItem: LiveData<BaseModel?> get() = mNavToItem
/**
* Update the current navigation status
* @param value Whether the current [DetailFragment] is navigating or not.
*/
fun updateNavigationStatus(value: Boolean) {
mIsNavigating = value
fun updateGenre(genre: Genre) {
mCurrentGenre.value = genre
}
fun updateArtist(artist: Artist) {
mCurrentArtist.value = artist
}
fun updateAlbum(album: Album) {
mCurrentAlbum.value = album
}
/**
@ -75,7 +78,7 @@ class DetailViewModel : ViewModel() {
}
/**
* Increment the sort mode of the album songs
* Increment the sort mode of the album song
*/
fun incrementAlbumSortMode() {
mAlbumSortMode.value = when (mAlbumSortMode.value) {
@ -86,25 +89,24 @@ class DetailViewModel : ViewModel() {
}
}
fun updateGenre(genre: Genre) {
mCurrentGenre.value = genre
}
fun updateArtist(artist: Artist) {
mCurrentArtist.value = artist
}
fun updateAlbum(album: Album) {
mCurrentAlbum.value = album
}
/** Navigate to an item, whether a song/album/artist */
/**
* Navigate to an item, whether a song/album/artist
*/
fun navToItem(item: BaseModel) {
mNavToItem.value = item
}
/** Mark that the navigation process is done. */
/**
* Mark that the navigation process is done.
*/
fun doneWithNavToItem() {
mNavToItem.value = null
}
/**
* Update the current navigation status to [isNavigating]
*/
fun updateNavigationStatus(isNavigating: Boolean) {
mIsNavigating = isNavigating
}
}

View file

@ -70,23 +70,21 @@ class GenreDetailFragment : DetailFragment() {
}
detailModel.navToItem.observe(viewLifecycleOwner) {
if (it != null) {
when (it) {
// All items will launch new detail fragments.
is Artist -> findNavController().navigate(
GenreDetailFragmentDirections.actionShowArtist(it.id)
)
when (it) {
// All items will launch new detail fragments.
is Artist -> findNavController().navigate(
GenreDetailFragmentDirections.actionShowArtist(it.id)
)
is Album -> findNavController().navigate(
GenreDetailFragmentDirections.actionShowAlbum(it.id)
)
is Album -> findNavController().navigate(
GenreDetailFragmentDirections.actionShowAlbum(it.id)
)
is Song -> findNavController().navigate(
GenreDetailFragmentDirections.actionShowAlbum(it.album.id)
)
is Song -> findNavController().navigate(
GenreDetailFragmentDirections.actionShowAlbum(it.album.id)
)
else -> {}
}
else -> {}
}
}

View file

@ -23,6 +23,7 @@ import org.oxycblt.auxio.ui.setTextColorResource
/**
* An adapter for displaying the details and [Song]s of an [Album]
* @author OxygenCobalt
*/
class AlbumDetailAdapter(
private val detailModel: DetailViewModel,
@ -75,8 +76,8 @@ class AlbumDetailAdapter(
}
/**
* Update the current song that this adapter should be watching for to highlight.
* @param song The [Song] to highlight if found, null to clear any highlighted ViewHolders
* Update the [song] that this adapter should highlight
* @param recycler The recyclerview the highlighting should act on.
*/
fun highlightSong(song: Song?, recycler: RecyclerView) {
// Clear out the last ViewHolder as a song update usually signifies that this current

View file

@ -23,6 +23,7 @@ import org.oxycblt.auxio.ui.setTextColorResource
/**
* An adapter for displaying the [Album]s of an artist.
* @author OxygenCobalt
*/
class ArtistDetailAdapter(
private val detailModel: DetailViewModel,
@ -76,8 +77,8 @@ class ArtistDetailAdapter(
}
/**
* Update the current album that this adapter should be watching for to highlight.
* @param album The [Album] to highlight if found, null to clear any highlighted ViewHolders
* Update the current [album] that this adapter should highlight
* @param recycler The recyclerview the highlighting should act on.
*/
fun setCurrentAlbum(album: Album?, recycler: RecyclerView) {
// Clear out the last ViewHolder as a song update usually signifies that this current

View file

@ -23,6 +23,7 @@ import org.oxycblt.auxio.ui.setTextColorResource
/**
* An adapter for displaying the [Song]s of a genre.
* @author OxygenCobalt
*/
class GenreDetailAdapter(
private val detailModel: DetailViewModel,
@ -76,8 +77,8 @@ class GenreDetailAdapter(
}
/**
* Update the current song that this adapter should be watching for to highlight.
* @param song The [Song] to highlight if found, null to clear any highlighted ViewHolders
* Update the [song] that this adapter should highlight
* @param recycler The recyclerview the highlighting should act on.
*/
fun highlightSong(song: Song?, recycler: RecyclerView) {
// Clear out the last ViewHolder as a song update usually signifies that this current

View file

@ -59,8 +59,7 @@ class LibraryAdapter(
}
/**
* Update the data directly. [notifyDataSetChanged] will be called
* @param newData The new data to be used
* Update the data with [newData]. [notifyDataSetChanged] will be called.
*/
fun updateData(newData: List<Parent>) {
data = newData

View file

@ -24,6 +24,7 @@ import org.oxycblt.auxio.ui.newMenu
/**
* A [Fragment] that shows a custom list of [Genre], [Artist], or [Album] data. Also allows for
* search functionality.
* @author OxygenCobalt
*/
class LibraryFragment : Fragment() {
private val libraryModel: LibraryViewModel by activityViewModels()
@ -36,7 +37,7 @@ class LibraryFragment : Fragment() {
): View {
val binding = FragmentLibraryBinding.inflate(inflater)
val libraryAdapter = LibraryAdapter(::onItemSelection) { view, data -> newMenu(view, data) }
val libraryAdapter = LibraryAdapter(::navToDetail) { view, data -> newMenu(view, data) }
// --- UI SETUP ---
@ -78,9 +79,9 @@ class LibraryFragment : Fragment() {
libraryModel.updateNavigationStatus(false)
if (it is Parent) {
onItemSelection(it)
navToDetail(it)
} else if (it is Song) {
onItemSelection(it.album)
navToDetail(it.album)
}
}
}
@ -103,10 +104,9 @@ class LibraryFragment : Fragment() {
}
/**
* Navigate to a parent UI
* @param parent The parent that should be navigated with
* Navigate to the detail UI for a [parent].
*/
private fun onItemSelection(parent: Parent) {
private fun navToDetail(parent: Parent) {
requireView().rootView.clearFocus()
if (!libraryModel.isNavigating) {

View file

@ -20,14 +20,14 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
private val mLibraryData = MutableLiveData(listOf<Parent>())
val libraryData: LiveData<List<Parent>> get() = mLibraryData
private var mIsNavigating = false
val isNavigating: Boolean get() = mIsNavigating
private var mSortMode = SortMode.ALPHA_DOWN
val sortMode: SortMode get() = mSortMode
private var mDisplayMode = DisplayMode.SHOW_ARTISTS
private var mIsNavigating = false
val isNavigating: Boolean get() = mIsNavigating
private val settingsManager = SettingsManager.getInstance()
private val musicStore = MusicStore.getInstance()
@ -42,8 +42,7 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
}
/**
* Update the current [SortMode] with a menu id.
* @param itemId The id of the menu item selected.
* Update the current [SortMode] using an menu [itemId].
*/
fun updateSortMode(@IdRes itemId: Int) {
val mode = when (itemId) {
@ -64,28 +63,11 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
/**
* Update the current navigation status
* @param value Whether LibraryFragment is navigating or not
*/
fun updateNavigationStatus(value: Boolean) {
mIsNavigating = value
fun updateNavigationStatus(isNavigating: Boolean) {
mIsNavigating = isNavigating
}
// --- OVERRIDES ---
override fun onCleared() {
super.onCleared()
settingsManager.removeCallback(this)
}
override fun onLibDisplayModeUpdate(displayMode: DisplayMode) {
mDisplayMode = displayMode
updateLibraryData()
}
// --- UTILS ---
/**
* Shortcut function for updating the library data with the current [SortMode]/[DisplayMode]
*/
@ -100,4 +82,18 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
else -> error("DisplayMode $mDisplayMode is unsupported.")
}
}
// --- OVERRIDES ---
override fun onCleared() {
super.onCleared()
settingsManager.removeCallback(this)
}
override fun onLibDisplayModeUpdate(displayMode: DisplayMode) {
mDisplayMode = displayMode
updateLibraryData()
}
}

View file

@ -15,6 +15,10 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
import org.oxycblt.auxio.music.MusicStore
/**
* Fragment that handles what to display during the loading process.
* @author OxygenCobalt
*/
class LoadingFragment : Fragment() {
private val loadingModel: LoadingViewModel by viewModels {
LoadingViewModel.Factory(requireActivity().application)
@ -85,6 +89,9 @@ class LoadingFragment : Fragment() {
// --- PERMISSIONS ---
/**
* Check if Auxio has the permissions to load music
*/
private fun hasNoPermissions(): Boolean {
val needRationale = shouldShowRequestPermissionRationale(
Manifest.permission.READ_EXTERNAL_STORAGE
@ -107,16 +114,24 @@ class LoadingFragment : Fragment() {
// --- UI DISPLAY ---
/**
* Hide all error elements and return to the loading view
*/
private fun showLoading(binding: FragmentLoadingBinding) {
binding.apply {
loadingCircle.visibility = View.VISIBLE
loadingErrorIcon.visibility = View.GONE
loadingErrorText.visibility = View.GONE
loadingRetryButton.visibility = View.GONE
loadingGrantButton.visibility = View.GONE
loadingCircle.visibility = View.VISIBLE
}
}
/**
* Show an error prompt.
* @param error The [MusicStore.Response] that this error corresponds to. Ignores
* [MusicStore.Response.SUCCESS]
*/
private fun showError(binding: FragmentLoadingBinding, error: MusicStore.Response) {
binding.loadingCircle.visibility = View.GONE
binding.loadingErrorIcon.visibility = View.VISIBLE

View file

@ -9,17 +9,25 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.oxycblt.auxio.music.MusicStore
/**
* ViewModel responsible for the loading UI and beginning the loading process overall.
* @author OxygenCobalt
*/
class LoadingViewModel(private val app: Application) : ViewModel() {
private val mResponse = MutableLiveData<MusicStore.Response?>(null)
val response: LiveData<MusicStore.Response?> = mResponse
private val mDoGrant = MutableLiveData(false)
/** The last response from [MusicStore]. Null if the loading process is occurring. */
val response: LiveData<MusicStore.Response?> = mResponse
val doGrant: LiveData<Boolean> = mDoGrant
private var isBusy = false
private val musicStore = MusicStore.getInstance()
/**
* Begin the music loading process. The response is pushed to [response]
*/
fun load() {
// Dont start a new load if the last one hasnt finished
if (isBusy) return
@ -33,14 +41,23 @@ class LoadingViewModel(private val app: Application) : ViewModel() {
}
}
/**
* Show the grant prompt.
*/
fun grant() {
mDoGrant.value = true
}
/**
* Mark that the grant prompt is now shown.
*/
fun doneWithGrant() {
mDoGrant.value = false
}
/**
* Notify this viewmodel that there are no permissions
*/
fun notifyNoPermissions() {
mResponse.value = MusicStore.Response.NO_PERMS
}

View file

@ -8,7 +8,6 @@ import android.net.Uri
* The base data object for all music.
* @property id The ID that is assigned to this object
* @property name The name of this object (Such as a song title)
* @author OxygenCobalt
*/
sealed class BaseModel {
abstract val id: Long
@ -34,7 +33,6 @@ sealed class Parent : BaseModel() {
* @property genre The Song's [Genre]
* @property seconds The Song's duration in seconds
* @property formattedDuration The Song's duration as a duration string.
* @author OxygenCobalt
*/
data class Song(
override val id: Long = -1,
@ -74,7 +72,6 @@ data class Song(
* @property artist The Album's parent [Artist]. use this instead of [artistName]
* @property songs The Album's child [Song]s.
* @property totalDuration The combined duration of all of the album's child songs, formatted.
* @author OxygenCobalt
*/
data class Album(
override val id: Long = -1,
@ -108,7 +105,6 @@ data class Album(
* @property albums The list of all [Album]s in this artist
* @property genre The most prominent genre for this artist
* @property songs The list of all [Song]s in this artist
* @author OxygenCobalt
*/
data class Artist(
override val id: Long = -1,
@ -134,14 +130,13 @@ data class Artist(
* The data object for a genre. Inherits [Parent]
* @property songs The list of all [Song]s in this genre.
* @property resolvedName A name that has been resolved from its int-genre form to its named form.
* @author OxygenCobalt
*/
data class Genre(
override val id: Long = -1,
override val name: String,
) : Parent() {
val resolvedName: String by lazy {
if (name.contains(Regex("[0123456789)]"))) {
if (name.contains(Regex("([1-9])"))) {
name.toNamedGenre() ?: name
} else {
name
@ -162,7 +157,6 @@ data class Genre(
/**
* A data object used solely for the "Header" UI element. Inherits [BaseModel].
* @property isAction Value that marks whether this header should have an action attached to it.
* @author OxygenCobalt
*/
data class Header(
override val id: Long = -1,

View file

@ -38,12 +38,12 @@ class MusicStore private constructor() {
}
}
/** Marker for whether the music loading process has successfully completed. */
var loaded = false
private set
/**
* Load/Sort the entire music library.
* ***THIS SHOULD ONLY BE RAN FROM AN IO THREAD.***
* Load/Sort the entire music library. Should always be ran on a coroutine.
* @param app [Application] required to load the music.
*/
suspend fun load(app: Application): Response {
@ -68,8 +68,6 @@ class MusicStore private constructor() {
mArtists = linker.artists.toList()
mGenres = linker.genres.toList()
loaded = true
this@MusicStore.logD(
"Music load completed successfully in ${System.currentTimeMillis() - start}ms."
)
@ -80,12 +78,15 @@ class MusicStore private constructor() {
return@withContext Response.FAILED
}
loaded = true
return@withContext Response.SUCCESS
}
}
/**
* Get the song for a specific URI.
* Get the song for a file [uri].
* @return The corresponding [Song] for this [uri], null if there isnt one.
*/
suspend fun getSongForUri(uri: Uri, resolver: ContentResolver): Song? {
return withContext(Dispatchers.IO) {
@ -98,6 +99,9 @@ class MusicStore private constructor() {
}
}
/**
* Responses that [MusicStore] sends back when a [load] call completes.
*/
enum class Response {
NO_MUSIC, NO_PERMS, FAILED, SUCCESS
}

View file

@ -60,25 +60,17 @@ fun String.toNamedGenre(): String? {
}
/**
* Convert a song id to its URI
* @return The [Uri] for this song/
* Convert an id to its corresponding URI
*/
fun Long.toURI(): Uri {
return ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
this
)
return ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this)
}
/**
* Get the URI for an album's cover art.
* @return The [Uri] for the album's cover art/
* Get the URI for an album's cover art, corresponds to MediaStore.
*/
fun Long.toAlbumArtURI(): Uri {
return ContentUris.withAppendedId(
Uri.parse("content://media/external/audio/albumart"),
this
)
return ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), this)
}
/**

View file

@ -10,7 +10,9 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
/**
* Object that links music data to one-another,
* Object that links music data, such as grouping songs into their albums & genres and creating
* artists out of the albums.
* @author OxygenCobalt
*/
class MusicLinker(
private val context: Context,
@ -21,6 +23,10 @@ class MusicLinker(
private val resolver = context.contentResolver
val artists = mutableListOf<Artist>()
/**
* Begin the linking process.
* Modified models are pushed to [songs], [albums], [artists], and [genres]
*/
fun link() {
linkAlbums()
linkArtists()

View file

@ -17,6 +17,7 @@ import org.oxycblt.auxio.music.toAlbumArtURI
/**
* Class that loads/constructs [Genre]s, [Album]s, and [Song] objects from the filesystem
* Artists are constructed in [MusicLinker], as they are only really containers for [Album]s
* @author OxygenCobalt
*/
class MusicLoader(private val app: Application) {
var genres = mutableListOf<Genre>()
@ -25,6 +26,9 @@ class MusicLoader(private val app: Application) {
private val resolver = app.contentResolver
/**
* Begin the loading process. Resulting models are pushed to [genres], [albums], and [songs].
*/
fun loadMusic() {
loadGenres()
loadAlbums()

View file

@ -12,6 +12,7 @@ import org.oxycblt.auxio.ui.toAnimDrawable
/**
* Custom [AppCompatImageButton] that handles the animated play/pause icons.
* @author OxygenCobalt
*/
class PlayPauseButton @JvmOverloads constructor(
context: Context,
@ -27,6 +28,10 @@ class PlayPauseButton @JvmOverloads constructor(
}
}
/**
* Update the play/pause icon to reflect [isPlaying]
* @param animate Whether the icon change should be animated or not.
*/
fun setPlaying(isPlaying: Boolean, animate: Boolean) {
if (isPlaying) {
if (animate) {

View file

@ -121,7 +121,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
playbackManager.playSong(song, mode)
}
/** Play an album.*/
/**
* Play an [album].
* @param shuffled Whether to shuffle the new queue
*/
fun playAlbum(album: Album, shuffled: Boolean) {
if (album.songs.isEmpty()) {
logE("Album is empty, Not playing.")
@ -132,7 +135,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
playbackManager.playParent(album, shuffled)
}
/** Play an Artist */
/**
* Play an [artist].
* @param shuffled Whether to shuffle the new queue
*/
fun playArtist(artist: Artist, shuffled: Boolean) {
if (artist.songs.isEmpty()) {
logE("Artist is empty, Not playing.")
@ -143,7 +149,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
playbackManager.playParent(artist, shuffled)
}
/** Play a genre. */
/**
* Play a [genre].
* @param shuffled Whether to shuffle the new queue
*/
fun playGenre(genre: Genre, shuffled: Boolean) {
if (genre.songs.isEmpty()) {
logE("Genre is empty, Not playing.")
@ -154,7 +163,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
playbackManager.playParent(genre, shuffled)
}
/** Play using a file URI. This will not play instantly during the initial startup sequence.*/
/**
* Play using a file [uri].
* This will not play instantly during the initial startup sequence.
*/
fun playWithUri(uri: Uri, context: Context) {
// Check if everything is already running to run the URI play
if (playbackManager.isRestored && musicStore.loaded) {
@ -164,7 +176,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
}
}
/** Actually play with a given URI internally. The frontend doesn't play instantly. */
/**
* Play with a file URI.
* This is called after [playWithUri] once its deemed safe to do so.
*/
private fun playWithUriInternal(uri: Uri, context: Context) {
viewModelScope.launch {
musicStore.getSongForUri(uri, context.contentResolver)?.let { song ->
@ -173,14 +188,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
}
}
/** Shuffle all songs */
/**
* Shuffle all songs
*/
fun shuffleAll() {
playbackManager.shuffleAll()
}
// --- POSITION FUNCTIONS ---
/** Update the position and push it to [PlaybackStateManager] */
/**
* Update the position and push it to [PlaybackStateManager]
*/
fun setPosition(progress: Int) {
playbackManager.seekTo((progress * 1000).toLong())
}
@ -196,22 +215,25 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// --- QUEUE FUNCTIONS ---
/** Skip to the next song. */
/**
* Skip to the next song.
*/
fun skipNext() {
playbackManager.next()
}
/** Skip to the previous song */
/**
* Skip to the previous song.
*/
fun skipPrev() {
playbackManager.prev()
}
/**
* Remove a queue OR user queue item, given a QueueAdapter index.
* @param adapterIndex The [QueueAdapter] index to remove
* @param queueAdapter The [QueueAdapter] itself to push changes to when successful.
* Remove an item at [adapterIndex], being a non-header data index.
* @param queueAdapter [QueueAdapter] instance to push changes to when successful.
*/
fun removeQueueAdapterItem(adapterIndex: Int, queueAdapter: QueueAdapter) {
fun removeQueueDataItem(adapterIndex: Int, queueAdapter: QueueAdapter) {
var index = adapterIndex.dec()
// If the item is in the user queue, then remove it from there after accounting for the header.
@ -234,12 +256,11 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
}
/**
* Move queue OR user queue items, given QueueAdapter indices.
* @param adapterFrom The [QueueAdapter] index that needs to be moved
* @param adapterTo The destination [QueueAdapter] index.
* @param queueAdapter the [QueueAdapter] to push changes to when successful.
* Move a queue OR user queue item from [adapterFrom] to [adapterTo], as long as both
* indices are non-header data indices.
* @param queueAdapter [QueueAdapter] instance to push changes to when successful.
*/
fun moveQueueAdapterItems(
fun moveQueueDataItems(
adapterFrom: Int,
adapterTo: Int,
queueAdapter: QueueAdapter
@ -283,38 +304,50 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
return true
}
/** Add a [Song] to the user queue.*/
/**
* Add a [Song] to the user queue.
*/
fun addToUserQueue(song: Song) {
playbackManager.addToUserQueue(song)
}
/** Add an [Album] to the user queue */
/**
* Add an [Album] to the user queue
*/
fun addToUserQueue(album: Album) {
val songs = SortMode.NUMERIC_DOWN.getSortedSongList(album.songs)
playbackManager.addToUserQueue(songs)
}
/** Clear the user queue entirely */
/**
* Clear the user queue entirely
*/
fun clearUserQueue() {
playbackManager.clearUserQueue()
}
// --- STATUS FUNCTIONS ---
/** Flip the playing status, e.g from playing to paused */
/**
* Flip the playing status, e.g from playing to paused
*/
fun invertPlayingStatus() {
enableAnimation()
playbackManager.setPlaying(!playbackManager.isPlaying)
}
/** Flip the shuffle status, e.g from on to off. Will keep song by default. */
/**
* Flip the shuffle status, e.g from on to off. Will keep song by default.
*/
fun invertShuffleStatus() {
playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true)
}
/** Increment the loop status, e.g from off to loop once */
/**
* Increment the loop status, e.g from off to loop once
*/
fun incrementLoopStatus() {
playbackManager.setLoopMode(playbackManager.loopMode.increment())
}
@ -322,8 +355,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// --- SAVE/RESTORE FUNCTIONS ---
/**
* Force save the current [PlaybackStateManager] state to the database. Called by SettingsListFragment.
* @param context [Context] required.
* Force save the current [PlaybackStateManager] state to the database.
* Called by SettingsListFragment.
*/
fun savePlaybackState(context: Context) {
viewModelScope.launch {
@ -334,7 +367,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
}
/**
* Handle the file last-saved file intent, or restore playback, depending on the situation.
* Restore playback on startup. This can do one of two things:
* - Play a file intent that was given by MainActivity in [playWithUri]
* - Restore the last playback state if there is no active file intent.
*/
fun setupPlayback(context: Context) {
val intentUri = mIntentUri
@ -347,13 +382,17 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// Remove the uri after finishing the calls so that this does not fire again.
mIntentUri = null
} else if (!playbackManager.isRestored) {
// Otherwise just restore
viewModelScope.launch {
playbackManager.getStateFromDatabase(context)
playbackManager.restoreFromDatabase(context)
}
}
}
/** Attempt to restore the current playback state from an existing [PlaybackStateManager] instance */
/**
* Attempt to restore the current playback state from an existing
* [PlaybackStateManager] instance.
*/
private fun restorePlaybackState() {
logD("Attempting to restore playback state.")
@ -371,17 +410,23 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// --- OTHER FUNCTIONS ---
/** Set whether the seeking indicator should be highlighted */
fun setSeekingStatus(value: Boolean) {
mIsSeeking.value = value
/**
* Set whether the seeking indicator should be highlighted
*/
fun setSeekingStatus(isSeeking: Boolean) {
mIsSeeking.value = isSeeking
}
/** Enable animation on CompactPlaybackFragment */
/**
* Enable animation on CompactPlaybackFragment
*/
fun enableAnimation() {
mCanAnimate = true
}
/** Disable animation on CompactPlaybackFragment */
/**
* Disable animation on CompactPlaybackFragment
*/
fun disableAnimation() {
mCanAnimate = false
}

View file

@ -38,13 +38,15 @@ class QueueAdapter(
override fun getItemViewType(position: Int): Int {
val item = data[position]
return if (item is Header)
if (item.isAction)
return if (item is Header) {
if (item.isAction) {
USER_QUEUE_HEADER_ITEM_TYPE
else
} else {
HeaderViewHolder.ITEM_TYPE
else
}
} else {
QUEUE_SONG_ITEM_TYPE
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@ -80,7 +82,8 @@ class QueueAdapter(
}
/**
* Submit data using [AsyncListDiffer]. **Only use this if you have no idea what changes occurred to the data**
* Submit data using [AsyncListDiffer].
* **Only use this if you have no idea what changes occurred to the data**
*/
fun submitList(newData: MutableList<BaseModel>) {
if (data != newData) {
@ -91,7 +94,8 @@ class QueueAdapter(
}
/**
* Move Items. Used since [submitList] will cause QueueAdapter to freak-out here.
* Move Items.
* Used since [submitList] will cause QueueAdapter to freak-out here.
*/
fun moveItems(adapterFrom: Int, adapterTo: Int) {
val item = data.removeAt(adapterFrom)
@ -101,17 +105,20 @@ class QueueAdapter(
}
/**
* Remove an item. Used since [submitList] will cause QueueAdapter to freak-out here.
* Remove an item.
* Used since [submitList] will cause QueueAdapter to freak-out here.
*/
fun removeItem(adapterIndex: Int) {
data.removeAt(adapterIndex)
/*
* Check for two things:
* If the data from the next queue is now entirely empty [Signified by a header at the end]
* Or if the data from the last queue is now entirely empty [Signified by there being
* 2 headers with no items in between]
* If so, remove the header and the removed item in a range. Otherwise just remove the item.
* If the data from the next queue is now entirely empty [Signified by a header at the
* end, remove the next queue header as notify as such.
*
* If the user queue is empty [Signified by there being two headers at the beginning with
* nothing in between], then remove the user queue header and notify as such.
*
* Otherwise just remove the item as usual.
*/
if (data[data.lastIndex] is Header) {
val lastIndex = data.lastIndex

View file

@ -11,7 +11,6 @@ import kotlin.math.sign
/**
* The Drag callback used by the queue recyclerview. Delivers updates to [PlaybackViewModel]
* and [QueueAdapter] simultaneously.
* @param playbackModel The [PlaybackViewModel] required to dispatch updates to.
* @author OxygenCobalt
*/
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
@ -58,7 +57,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return playbackModel.moveQueueAdapterItems(
return playbackModel.moveQueueDataItems(
viewHolder.adapterPosition,
target.adapterPosition,
queueAdapter
@ -66,7 +65,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
playbackModel.removeQueueAdapterItem(viewHolder.adapterPosition, queueAdapter)
playbackModel.removeQueueDataItem(viewHolder.adapterPosition, queueAdapter)
}
/**

View file

@ -22,8 +22,6 @@ import org.oxycblt.auxio.ui.isIrregularLandscape
/**
* A [Fragment] that contains both the user queue and the next queue, with the ability to
* edit them as well.
*
* Instantiation is done by the navigation component, **do not instantiate this fragment manually.**
* @author OxygenCobalt
*/
class QueueFragment : Fragment() {

View file

@ -2,6 +2,7 @@ package org.oxycblt.auxio.playback.state
/**
* Enum that determines the playback repeat mode.
* @author OxygenCobalt
*/
enum class LoopMode {
NONE, ONCE, INFINITE;
@ -35,7 +36,7 @@ enum class LoopMode {
const val CONSTANT_INFINITE = 0xA052
/**
* Convert an int constant into a LoopMode
* Convert an int [constant] into a LoopMode
* @return The corresponding LoopMode. Null if it corresponds to nothing.
*/
fun fromInt(constant: Int): LoopMode? {

View file

@ -1,6 +1,9 @@
package org.oxycblt.auxio.playback.state
// Enum that instructs how the queue should be constructed
/**
* Enum that indicates how the queue should be constructed.
* @author OxygenCobalt
*/
enum class PlaybackMode {
/** Construct the queue from the genre's songs */
IN_GENRE,
@ -31,7 +34,7 @@ enum class PlaybackMode {
const val CONSTANT_ALL_SONGS = 0xA043
/**
* Get a [PlaybackMode] for an int constant
* Get a [PlaybackMode] for an int [constant]
* @return The mode, null if there isnt one for this.
*/
fun fromInt(constant: Int): PlaybackMode? {

View file

@ -144,8 +144,7 @@ class PlaybackStateManager private constructor() {
// --- PLAYING FUNCTIONS ---
/**
* Play a song.
* @param song The song to be played
* Play a [song].
* @param mode The [PlaybackMode] to construct the queue off of.
*/
fun playSong(song: Song, mode: PlaybackMode) {
@ -190,9 +189,8 @@ class PlaybackStateManager private constructor() {
}
/**
* Play a parent model, e.g an artist or an album.
* @param parent The model to use
* @param shuffled Whether to shuffle the queue or not
* Play a [parent], such as an artist or album.
* @param shuffled Whether the queue is shuffled or not
*/
fun playParent(parent: Parent, shuffled: Boolean) {
logD("Playing ${parent.name}")
@ -221,8 +219,19 @@ class PlaybackStateManager private constructor() {
}
/**
* Shortcut function for updating what song is being played. ***USE THIS INSTEAD OF WRITING OUT ALL THE CODE YOURSELF!!!***
* @param song The song to play
* Shuffle all songs.
*/
fun shuffleAll() {
mMode = PlaybackMode.ALL_SONGS
mQueue = musicStore.songs.toMutableList()
mParent = null
setShuffling(true, keepSong = false)
updatePlayback(mQueue[0])
}
/**
* Update the playback to a new [song], doing all the required logic.
*/
private fun updatePlayback(song: Song) {
mIsInUserQueue = false
@ -326,8 +335,7 @@ class PlaybackStateManager private constructor() {
// --- QUEUE EDITING FUNCTIONS ---
/**
* Remove a queue item at a QUEUE index. Will log an error if the index is out of bounds
* @param index The index at which the item should be removed.
* Remove a queue item at [index]. Will ignore invalid indexes.
*/
fun removeQueueItem(index: Int): Boolean {
logD("Removing item ${mQueue[index].name}.")
@ -346,10 +354,7 @@ class PlaybackStateManager private constructor() {
}
/**
* Move a queue item from a QUEUE INDEX to a QUEUE INDEX. Will log an error if one of the indices
* is out of bounds.
* @param from The starting item's index
* @param to The destination index.
* Move a queue item at [from] to a position at [to]. Will ignore invalid indexes.
*/
fun moveQueueItems(from: Int, to: Int): Boolean {
if (from > mQueue.size || from < 0 || to > mQueue.size || to < 0) {
@ -367,8 +372,7 @@ class PlaybackStateManager private constructor() {
}
/**
* Add a song to the user queue.
* @param song The song to add
* Add a [song] to the user queue.
*/
fun addToUserQueue(song: Song) {
mUserQueue.add(song)
@ -377,8 +381,7 @@ class PlaybackStateManager private constructor() {
}
/**
* Add a list of songs to the user queue.
* @param songs The songs to add.
* Add a list of [songs] to the user queue.
*/
fun addToUserQueue(songs: List<Song>) {
mUserQueue.addAll(songs)
@ -387,8 +390,7 @@ class PlaybackStateManager private constructor() {
}
/**
* Remove a USER QUEUE item at a USER QUEUE index. Will log an error if the index is out of bounds.
* @param index The index at which the item should be removed.
* Remove a USER queue item at [index]. Will ignore invalid indexes.
*/
fun removeUserQueueItem(index: Int) {
logD("Removing item ${mUserQueue[index].name}.")
@ -405,10 +407,7 @@ class PlaybackStateManager private constructor() {
}
/**
* Move a USER QUEUE item from a USER QUEUE index to another USER QUEUE index. Will log an error if one of the indices
* is out of bounds.
* @param from The starting item's index
* @param to The destination index.
* Move a USER queue item at [from] to a position at [to]. Will ignore invalid indexes.
*/
fun moveUserQueueItems(from: Int, to: Int) {
if (from > mUserQueue.size || from < 0 || to > mUserQueue.size || to < 0) {
@ -449,24 +448,11 @@ class PlaybackStateManager private constructor() {
// --- SHUFFLE FUNCTIONS ---
/**
* Shuffle all songs.
*/
fun shuffleAll() {
mMode = PlaybackMode.ALL_SONGS
mQueue = musicStore.songs.toMutableList()
mParent = null
setShuffling(true, keepSong = false)
updatePlayback(mQueue[0])
}
/**
* Set the shuffle status. Updates the queue accordingly
* @param shuffling Whether the queue should be shuffled or not.
* Set whether this instance is [shuffled]. Updates the queue accordingly
* @param keepSong Whether the current song should be kept as the queue is shuffled/unshuffled
*/
fun setShuffling(shuffling: Boolean, keepSong: Boolean) {
mIsShuffling = shuffling
fun setShuffling(shuffled: Boolean, keepSong: Boolean) {
mIsShuffling = shuffled
if (mIsShuffling) {
genShuffle(keepSong, mIsInUserQueue)
@ -525,8 +511,7 @@ class PlaybackStateManager private constructor() {
// --- STATE FUNCTIONS ---
/**
* Set the current playing status
* @param playing Whether the playback should be playing or paused.
* Set whether this instance is currently [playing].
*/
fun setPlaying(playing: Boolean) {
if (mIsPlaying != playing) {
@ -539,7 +524,7 @@ class PlaybackStateManager private constructor() {
}
/**
* Update the current position. Will not notify any listeners of a seek event, that's what [seekTo] is for.
* Update the current [position]. Will not notify listeners of a seek event.
* @param position The new position in millis.
* @see seekTo
*/
@ -553,10 +538,9 @@ class PlaybackStateManager private constructor() {
}
/**
* **Seek** to a position, this calls [PlaybackStateManager.Callback.onSeek] to notify
* **Seek** to a [position], this calls [PlaybackStateManager.Callback.onSeek] to notify
* elements that rely on that.
* @param position The position to seek to in millis.
* @see setPosition
*/
fun seekTo(position: Long) {
mPosition = position
@ -573,15 +557,14 @@ class PlaybackStateManager private constructor() {
}
/**
* Set the [LoopMode]
* @param mode The [LoopMode] to be used
* Set the [LoopMode] to [mode].
*/
fun setLoopMode(mode: LoopMode) {
mLoopMode = mode
}
/**
* Reset the current [LoopMode], if needed.
* Reset the current [LoopMode] from [LoopMode.ONCE], if needed.
* Use this instead of duplicating the code manually.
*/
fun clearLoopMode() {
@ -614,45 +597,42 @@ class PlaybackStateManager private constructor() {
suspend fun saveStateToDatabase(context: Context) {
logD("Saving state to DB.")
val start = System.currentTimeMillis()
// Pack the entire state and save it to the database.
withContext(Dispatchers.IO) {
val playbackState = packToPlaybackState()
val queueItems = packQueue()
val start = System.currentTimeMillis()
val database = PlaybackStateDatabase.getInstance(context)
database.writeState(playbackState)
database.writeQueue(queueItems)
database.writeState(packToPlaybackState())
database.writeQueue(packQueue())
this@PlaybackStateManager.logD(
"Save finished in ${System.currentTimeMillis() - start}ms"
)
}
val time = System.currentTimeMillis() - start
logD("Save finished in ${time}ms")
}
/**
* Restore the state from the database
* @param context [Context] required.
*/
suspend fun getStateFromDatabase(context: Context) {
suspend fun restoreFromDatabase(context: Context) {
logD("Getting state from DB.")
val start: Long
val now: Long
val state: PlaybackState?
// The coroutine call is locked at queueItems so that this function does not
// go ahead until EVERYTHING is read.
val queueItems = withContext(Dispatchers.IO) {
start = System.currentTimeMillis()
now = System.currentTimeMillis()
val database = PlaybackStateDatabase.getInstance(context)
state = database.readState()
database.readQueue()
}
val loadTime = System.currentTimeMillis() - start
logD("Load finished in ${loadTime}ms")
// Get off the IO coroutine since it will cause LiveData updates to throw an exception
state?.let {
logD("Valid playback state $it")
@ -663,11 +643,9 @@ class PlaybackStateManager private constructor() {
doParentSanityCheck()
}
val time = System.currentTimeMillis() - start
logD("Restore finished in ${System.currentTimeMillis() - now}ms")
logD("Restore finished in ${time}ms")
mIsRestored = true
setRestored()
}
/**
@ -693,8 +671,7 @@ class PlaybackStateManager private constructor() {
}
/**
* Unpack the state from a [PlaybackState]
* @param playbackState The state to unpack.
* Unpack a [playbackState] into this instance.
*/
private fun unpackFromPlaybackState(playbackState: PlaybackState) {
// Turn the simplified information from PlaybackState into values that can be used

View file

@ -14,6 +14,7 @@ import org.oxycblt.auxio.settings.SettingsManager
/**
* Object that manages the AudioFocus state.
* Adapted from NewPipe (https://github.com/TeamNewPipe/NewPipe)
* @author OxygenCobalt
*/
class AudioReactor(
context: Context,

View file

@ -10,6 +10,7 @@ import android.os.Build
import android.support.v4.media.session.MediaSessionCompat
import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat
import androidx.media.app.NotificationCompat.MediaStyle
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.MainActivity
import org.oxycblt.auxio.R
@ -18,7 +19,6 @@ import org.oxycblt.auxio.music.Parent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import androidx.media.app.NotificationCompat as MediaNotificationCompat
/**
* The unified notification for [PlaybackService]. This is not self-sufficient, updates have
@ -51,7 +51,7 @@ class PlaybackNotification private constructor(
addAction(buildAction(context, ACTION_EXIT, R.drawable.ic_exit))
setStyle(
MediaNotificationCompat.MediaStyle()
MediaStyle()
.setMediaSession(mediaToken)
.setShowActionsInCompactView(1, 2, 3)
)
@ -186,7 +186,7 @@ class PlaybackNotification private constructor(
const val ACTION_EXIT = "ACTION_AUXIO_EXIT_" + BuildConfig.BUILD_TYPE
/**
* Build a new instance of [PlaybackNotification].
* Build a new instance of [PlaybackNotification] from a [context] and [mediaSession]
*/
fun from(context: Context, mediaSession: MediaSessionCompat): PlaybackNotification {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

View file

@ -49,7 +49,6 @@ import org.oxycblt.auxio.settings.SettingsManager
* - The single [SimpleExoPlayer] instance.
* - The [MediaSessionCompat]
* - The Media Notification
* - Audio Focus
* - Headset management
*
* This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback],
@ -485,6 +484,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
private inner class SystemEventReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// --- NOTIFICATION CASES ---
PlaybackNotification.ACTION_PLAY_PAUSE -> playbackManager.setPlaying(
!playbackManager.isPlaying
)

View file

@ -11,7 +11,6 @@ import org.oxycblt.auxio.music.Song
/**
* An enum for the current sorting mode. Contains helper functions to sort lists based
* off the given sorting mode.
* TODO: Improve sorting by separating UP/DOWN from what should be sorted (Names, Tracks, etc)
* @property iconRes The icon for this [SortMode]
* @author OxygenCobalt
*/

View file

@ -9,8 +9,8 @@ import org.oxycblt.auxio.music.BaseModel
* A [RecyclerView.ViewHolder] that streamlines a lot of the common things across all viewholders.
* @param T The datatype, inheriting [BaseModel] for this ViewHolder.
* @param binding Basic [ViewDataBinding] required to set up click listeners & sizing.
* @param doOnClick (Optional, defaults to null) Function that specifies what to do on a click. Null if nothing should be done.
* @param doOnLongClick (Optional, defaults to null) Functions that specifics what to do on a long click. Null if nothing should be done.
* @param doOnClick (Optional) Function that calls on a click.
* @param doOnLongClick (Optional) Functions that calls on a long-click.
* @author OxygenCobalt
*/
abstract class BaseViewHolder<T : BaseModel>(
@ -38,8 +38,8 @@ abstract class BaseViewHolder<T : BaseModel>(
}
doOnLongClick?.let { onLongClick ->
binding.root.setOnLongClickListener {
onLongClick(binding.root, data)
binding.root.setOnLongClickListener { view ->
onLongClick(view, data)
true
}

View file

@ -76,6 +76,7 @@ class SearchFragment : Fragment() {
}
binding.searchEditText.addTextChangedListener {
// Run the search with the updated text as the query
searchModel.doSearch(it?.toString() ?: "", requireContext())
}
@ -144,29 +145,29 @@ class SearchFragment : Fragment() {
}
/**
* Navigate to an item, or play it, depending on what the given item is.
* @param baseModel The data the action should be done with
* Function that handles when an [item] is selected.
* Handles all datatypes that are selectable.
*/
private fun onItemSelection(baseModel: BaseModel) {
if (baseModel is Song) {
playbackModel.playSong(baseModel)
private fun onItemSelection(item: BaseModel) {
if (item is Song) {
playbackModel.playSong(item)
return
}
// Get rid of the keyboard
// Get rid of the keyboard if we are navigating
requireView().rootView.clearFocus()
if (!searchModel.isNavigating) {
searchModel.updateNavigationStatus(true)
logD("Navigating to the detail fragment for ${baseModel.name}")
logD("Navigating to the detail fragment for ${item.name}")
findNavController().navigate(
when (baseModel) {
is Genre -> SearchFragmentDirections.actionShowGenre(baseModel.id)
is Artist -> SearchFragmentDirections.actionShowArtist(baseModel.id)
is Album -> SearchFragmentDirections.actionShowAlbum(baseModel.id)
when (item) {
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
// If given model wasn't valid, then reset the navigation status
// and abort the navigation.

View file

@ -20,15 +20,14 @@ import org.oxycblt.auxio.settings.SettingsManager
*/
class SearchViewModel : ViewModel() {
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
private var mIsNavigating = false
private var mFilterMode = DisplayMode.SHOW_ALL
val filterMode: DisplayMode get() = mFilterMode
private var mLastQuery = ""
private var mIsNavigating = false
/** Current search results from the last [doSearch] call. */
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
val isNavigating: Boolean get() = mIsNavigating
val filterMode: DisplayMode get() = mFilterMode
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
@ -38,9 +37,8 @@ class SearchViewModel : ViewModel() {
}
/**
* Perform a search of the music library. Will push results to [searchResults].
* @param query The query to use
* @param context [Context] required to create the headers
* Use [query] to perform a search of the music library.
* Will push results to [searchResults].
*/
fun doSearch(query: String, context: Context) {
mLastQuery = query
@ -86,6 +84,10 @@ class SearchViewModel : ViewModel() {
}
}
/**
* Update the current filter mode with a menu [id].
* New value will be pushed to [filterMode].
*/
fun updateFilterModeWithId(@IdRes id: Int, context: Context) {
mFilterMode = DisplayMode.fromId(id)
@ -94,6 +96,10 @@ class SearchViewModel : ViewModel() {
doSearch(mLastQuery, context)
}
/**
* Shortcut that will run a ignoreCase filter on a list and only return
* a value if the resulting list is empty.
*/
private fun List<BaseModel>.filterByOrNull(value: String): List<BaseModel>? {
val filtered = filter { it.name.contains(value, ignoreCase = true) }
@ -101,10 +107,9 @@ class SearchViewModel : ViewModel() {
}
/**
* Update the current navigation status
* @param value Whether LibraryFragment is navigating or not
* Update the current navigation status to [isNavigating]
*/
fun updateNavigationStatus(value: Boolean) {
mIsNavigating = value
fun updateNavigationStatus(isNavigating: Boolean) {
mIsNavigating = isNavigating
}
}

View file

@ -45,6 +45,9 @@ class SettingsListFragment : PreferenceFragmentCompat() {
setPreferencesFromResource(R.xml.prefs_main, rootKey)
}
/**
* Recursively call [handlePreference] on a preference.
*/
private fun recursivelyHandleChildren(pref: Preference) {
if (pref is PreferenceCategory) {
// If this preference is a category of its own, handle its own children
@ -54,6 +57,9 @@ class SettingsListFragment : PreferenceFragmentCompat() {
}
}
/**
* Handle a preference, doing any specific actions on it.
*/
private fun handlePreference(pref: Preference) {
pref.apply {
when (key) {
@ -134,6 +140,9 @@ class SettingsListFragment : PreferenceFragmentCompat() {
}
}
/**
* Show the accent dialog to the user
*/
private fun showAccentDialog() {
MaterialDialog(requireActivity()).show {
title(R.string.setting_accent)

View file

@ -23,15 +23,11 @@ class SettingsManager private constructor(context: Context) :
// --- VALUES ---
/**
* The current theme.
*/
/** The current theme */
val theme: Int
get() = sharedPrefs.getString(Keys.KEY_THEME, EntryValues.THEME_AUTO)!!.toThemeInt()
/**
* The current accent.
*/
/** The current accent. */
var accent: Accent
get() {
// Accent is stored as an index [to be efficient], so retrieve it when done.
@ -47,9 +43,7 @@ class SettingsManager private constructor(context: Context) :
.apply()
}
/**
* Whether to colorize the notification
*/
/** Whether to colorize the notification */
val colorizeNotif: Boolean
get() = sharedPrefs.getBoolean(Keys.KEY_COLORIZE_NOTIFICATION, true)
@ -60,9 +54,7 @@ class SettingsManager private constructor(context: Context) :
val useAltNotifAction: Boolean
get() = sharedPrefs.getBoolean(Keys.KEY_USE_ALT_NOTIFICATION_ACTION, false)
/**
* What to display on the library.
*/
/** What to display on the library. */
val libraryDisplayMode: DisplayMode
get() = DisplayMode.valueOfOrFallback(
sharedPrefs.getString(
@ -78,27 +70,19 @@ class SettingsManager private constructor(context: Context) :
val showCovers: Boolean
get() = sharedPrefs.getBoolean(Keys.KEY_SHOW_COVERS, true)
/**
* Whether to ignore MediaStore covers
*/
/** Whether to ignore MediaStore covers */
val useQualityCovers: Boolean
get() = sharedPrefs.getBoolean(Keys.KEY_QUALITY_COVERS, false)
/**
* Whether to do Audio focus.
*/
/** Whether to do Audio focus. */
val doAudioFocus: Boolean
get() = sharedPrefs.getBoolean(Keys.KEY_AUDIO_FOCUS, true)
/**
* Whether to resume/stop playback when a headset is connected/disconnected.
*/
/** Whether to resume/stop playback when a headset is connected/disconnected. */
val doPlugMgt: Boolean
get() = sharedPrefs.getBoolean(Keys.KEY_PLUG_MANAGEMENT, true)
/**
* What queue to create when a song is selected (ex. From All Songs or Search)
*/
/** What queue to create when a song is selected (ex. From All Songs or Search) */
val songPlaybackMode: PlaybackMode
get() = PlaybackMode.valueOfOrFallback(
sharedPrefs.getString(
@ -107,28 +91,20 @@ class SettingsManager private constructor(context: Context) :
)
)
/**
* What to do at the end of a playlist.
*/
/** What to do at the end of a playlist. */
val doAtEnd: String
get() = sharedPrefs.getString(Keys.KEY_AT_END, EntryValues.AT_END_LOOP_PAUSE)
?: EntryValues.AT_END_LOOP_PAUSE
/**
* Whether shuffle should stay on when a new song is selected.
*/
/** Whether shuffle should stay on when a new song is selected. */
val keepShuffle: Boolean
get() = sharedPrefs.getBoolean(Keys.KEY_KEEP_SHUFFLE, true)
/**
* Whether to rewind when the back button is pressed.
*/
/** Whether to rewind when the back button is pressed. */
val rewindWithPrev: Boolean
get() = sharedPrefs.getBoolean(Keys.KEY_PREV_REWIND, true)
/**
* The current [SortMode] of the library.
*/
/** The current [SortMode] of the library. */
var librarySortMode: SortMode
get() = SortMode.fromInt(
sharedPrefs.getInt(
@ -143,9 +119,7 @@ class SettingsManager private constructor(context: Context) :
.apply()
}
/**
* The current filter mode of the search tab
*/
/** The current filter mode of the search tab */
var searchFilterMode: DisplayMode
get() = DisplayMode.valueOfOrFallback(
sharedPrefs.getString(

View file

@ -46,7 +46,8 @@ class AboutDialog : BottomSheetDialogFragment() {
}
/**
* Go through the process of opening one of the about links in a browser.
* Go through the process of opening a [link] in a browser. Only supports the links
* in [AboutDialog.Companion.LINKS].
*/
private fun openLinkInBrowser(link: String) {
check(link in LINKS) { "Invalid link." }
@ -83,9 +84,9 @@ class AboutDialog : BottomSheetDialogFragment() {
}
} catch (e: Exception) {
logE("Browser intent launching failed [Probably android's fault]")
e.printStackTrace()
logE(e.stackTraceToString())
// Sometimes people have """""Browsers""""" on their phone according to android,
// Sometimes people have """Browsers""" on their phone according to android,
// but they actually don't so here's a fallback for that.
getString(R.string.error_no_browser).createToast(requireContext())
}

View file

@ -11,6 +11,7 @@ import org.oxycblt.auxio.recycler.viewholders.SongViewHolder
* @param data List of [Song]s to be shown
* @param doOnClick What to do on a click action
* @param doOnLongClick What to do on a long click action
* @author OxygenCobalt
*/
class SongsAdapter(
private val data: List<Song>,

View file

@ -18,23 +18,25 @@ import org.oxycblt.auxio.logD
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.Accent
import org.oxycblt.auxio.ui.canScroll
import org.oxycblt.auxio.ui.getSpans
import org.oxycblt.auxio.ui.newMenu
import kotlin.math.ceil
/**
* A [Fragment] that shows a list of all songs on the device. Contains options to search/shuffle
* them.
* A [Fragment] that shows a list of all songs on the device.
* Contains options to search/shuffle them.
* @author OxygenCobalt
*/
class SongsFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val musicStore = MusicStore.getInstance()
// Lazy init the text size so that it doesn't have to be calculated every time.
private val indicatorTextSize: Float by lazy {
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 14F,
requireContext().resources.displayMetrics
resources.displayMetrics
)
}
@ -44,8 +46,6 @@ class SongsFragment : Fragment() {
savedInstanceState: Bundle?
): View {
val binding = FragmentSongsBinding.inflate(inflater)
val musicStore = MusicStore.getInstance()
val songAdapter = SongsAdapter(musicStore.songs, playbackModel::playSong) { view, data ->
newMenu(view, data)
}
@ -54,13 +54,12 @@ class SongsFragment : Fragment() {
binding.songToolbar.apply {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_shuffle -> {
playbackModel.shuffleAll()
}
if (it.itemId == R.id.action_shuffle) {
playbackModel.shuffleAll()
true
}
true
false
}
}
@ -76,7 +75,7 @@ class SongsFragment : Fragment() {
post {
// Disable fast scrolling if there is nothing to scroll
if (computeVerticalScrollRange() < height) {
if (!canScroll()) {
binding.songFastScroll.visibility = View.GONE
binding.songFastScrollThumb.visibility = View.GONE
}
@ -101,8 +100,6 @@ class SongsFragment : Fragment() {
* @param binding Binding required
*/
private fun setupFastScroller(binding: FragmentSongsBinding) {
val musicStore = MusicStore.getInstance()
binding.songFastScroll.apply {
var concatInterval = -1
@ -172,25 +169,27 @@ class SongsFragment : Fragment() {
useDefaultScroller = false
itemIndicatorSelectedCallbacks.add(
object : FastScrollerView.ItemIndicatorSelectedCallback {
override fun onItemIndicatorSelected(
indicator: FastScrollItemIndicator,
indicatorCenterY: Int,
itemPosition: Int
) {
binding.songRecycler.apply {
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
itemPosition, 0
)
addIndicatorCallback { pos ->
binding.songRecycler.apply {
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, 0)
stopScroll()
}
}
stopScroll()
}
)
}
}
binding.songFastScrollThumb.setupWithFastScroller(binding.songFastScroll)
}
private fun FastScrollerView.addIndicatorCallback(callback: (pos: Int) -> Unit) {
itemIndicatorSelectedCallbacks.add(
object : FastScrollerView.ItemIndicatorSelectedCallback {
override fun onItemIndicatorSelected(
indicator: FastScrollItemIndicator,
indicatorCenterY: Int,
itemPosition: Int
) = callback(itemPosition)
}
)
}
}

View file

@ -44,6 +44,7 @@ val ACCENTS = arrayOf(
* @property color The color resource for this accent
* @property theme The theme resource for this accent
* @property name The name of this accent
* @author OxygenCobalt
*/
data class Accent(@ColorRes val color: Int, @StyleRes val theme: Int, @StringRes val name: Int) {
/**

View file

@ -34,7 +34,8 @@ fun Fragment.newMenu(anchor: View, data: BaseModel, flag: Int = ActionMenu.FLAG_
* @param anchor [View] This should be centered around
* @param data [BaseModel] this menu corresponds to
* @param flag Any extra flags to accompany the data. See [FLAG_NONE], [FLAG_IN_ALBUM], [FLAG_IN_ARTIST], [FLAG_IN_GENRE] for more details.
* @throws IllegalArgumentException When there is no menu for this specific datatype/flag
* @throws IllegalStateException When there is no menu for this specific datatype/flag
* @author OxygenCobalt
*/
class ActionMenu(
activity: AppCompatActivity,

View file

@ -167,6 +167,11 @@ fun RecyclerView.getSpans(): Int {
}
}
/**
* Returns whether a recyclerview can scroll.
*/
fun RecyclerView.canScroll() = computeVerticalScrollRange() > height
/**
* Check if we are in the "Irregular" landscape mode (e.g landscape, but nav bar is on the sides)
* Used to disable most of edge-to-edge if that's the case, as I cant get it to work on this mode.

View file

@ -26,6 +26,7 @@ fun <T : ViewDataBinding> Fragment.memberBinding(
/**
* The delegate for the [memberBinding] shortcut function.
* Adapted from KAHelpers (https://github.com/FunkyMuse/KAHelpers/tree/master/viewbinding)
* @author OxygenCobalt
*/
class MemberBinder<T : ViewDataBinding>(
private val fragment: Fragment,

View file

@ -17,6 +17,7 @@ import java.lang.reflect.Field
*
* Adapted from this StackOverflow answer:
* https://stackoverflow.com/a/35087229
* @author OxygenCobalt
*/
class SlideLinearLayout @JvmOverloads constructor(
context: Context,