home: rework loading screen
Move the loading screen into the home view. Previously, we would use a Snackbar to track the music loading state. This ended up being a pretty stupid and buggy idea, and would get even worse with the new music loader changes. Instead, we re-implement the loading screen into the home view to generally be more sane and extendable compared to previously.
This commit is contained in:
parent
f52fa7f338
commit
caaeaca494
10 changed files with 164 additions and 108 deletions
|
@ -35,7 +35,6 @@ android {
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
debuggable true
|
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
versionNameSuffix = "-DEBUG"
|
versionNameSuffix = "-DEBUG"
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,28 +17,23 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio
|
package org.oxycblt.auxio
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
||||||
|
@ -86,11 +81,6 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
||||||
// TODO: Move this to a service [automatic rescanning]
|
// TODO: Move this to a service [automatic rescanning]
|
||||||
musicModel.loadMusic(requireContext())
|
musicModel.loadMusic(requireContext())
|
||||||
|
|
||||||
// Handle the music loader response.
|
|
||||||
musicModel.loaderResponse.observe(viewLifecycleOwner) { response ->
|
|
||||||
handleLoaderResponse(response, permLauncher)
|
|
||||||
}
|
|
||||||
|
|
||||||
navModel.mainNavigationAction.observe(viewLifecycleOwner, ::handleMainNavigation)
|
navModel.mainNavigationAction.observe(viewLifecycleOwner, ::handleMainNavigation)
|
||||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleExploreNavigation)
|
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleExploreNavigation)
|
||||||
|
|
||||||
|
@ -107,44 +97,6 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
||||||
callback?.isEnabled = false
|
callback?.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoaderResponse(
|
|
||||||
response: MusicStore.Response?,
|
|
||||||
permLauncher: ActivityResultLauncher<String>
|
|
||||||
) {
|
|
||||||
val binding = requireBinding()
|
|
||||||
|
|
||||||
// Handle any error cases, showing a useful message.
|
|
||||||
when (response) {
|
|
||||||
is MusicStore.Response.Err -> {
|
|
||||||
logD("Received Response.Err")
|
|
||||||
Snackbar.make(binding.root, R.string.err_load_failed, Snackbar.LENGTH_INDEFINITE)
|
|
||||||
.apply {
|
|
||||||
setAction(R.string.lbl_retry) { musicModel.reloadMusic(context) }
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is MusicStore.Response.NoMusic -> {
|
|
||||||
logD("Received Response.NoMusic")
|
|
||||||
Snackbar.make(binding.root, R.string.err_no_music, Snackbar.LENGTH_INDEFINITE)
|
|
||||||
.apply {
|
|
||||||
setAction(R.string.lbl_retry) { musicModel.reloadMusic(context) }
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is MusicStore.Response.NoPerms -> {
|
|
||||||
logD("Received Response.NoPerms")
|
|
||||||
Snackbar.make(binding.root, R.string.err_no_perms, Snackbar.LENGTH_INDEFINITE)
|
|
||||||
.apply {
|
|
||||||
setAction(R.string.lbl_grant) {
|
|
||||||
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
||||||
}
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleMainNavigation(action: MainNavigationAction?) {
|
private fun handleMainNavigation(action: MainNavigationAction?) {
|
||||||
if (action == null) return
|
if (action == null) return
|
||||||
|
|
||||||
|
|
|
@ -17,12 +17,17 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home
|
package org.oxycblt.auxio.home
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.iterator
|
import androidx.core.view.iterator
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
@ -51,6 +56,8 @@ import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logTraceOrThrow
|
import org.oxycblt.auxio.util.logTraceOrThrow
|
||||||
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
import org.oxycblt.auxio.util.textSafe
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -69,6 +76,12 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
|
||||||
val sortItem: MenuItem
|
val sortItem: MenuItem
|
||||||
|
|
||||||
|
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
||||||
|
val permLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
|
musicModel.reloadMusic(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
binding.homeToolbar.apply {
|
binding.homeToolbar.apply {
|
||||||
sortItem = menu.findItem(R.id.submenu_sorting)
|
sortItem = menu.findItem(R.id.submenu_sorting)
|
||||||
setOnMenuItemClickListener(this@HomeFragment)
|
setOnMenuItemClickListener(this@HomeFragment)
|
||||||
|
@ -76,6 +89,11 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
|
|
||||||
updateTabConfiguration()
|
updateTabConfiguration()
|
||||||
|
|
||||||
|
binding.homeLoadingContainer.setOnApplyWindowInsetsListener { v, insets ->
|
||||||
|
v.updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
binding.homePager.apply {
|
binding.homePager.apply {
|
||||||
adapter = HomePagerAdapter()
|
adapter = HomePagerAdapter()
|
||||||
|
|
||||||
|
@ -104,7 +122,10 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
homeModel.currentTab.observe(viewLifecycleOwner) { tab -> updateCurrentTab(sortItem, tab) }
|
homeModel.currentTab.observe(viewLifecycleOwner) { tab -> updateCurrentTab(sortItem, tab) }
|
||||||
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs)
|
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs)
|
||||||
|
|
||||||
musicModel.loaderResponse.observe(viewLifecycleOwner, ::handleLoaderResponse)
|
musicModel.loaderResponse.observe(viewLifecycleOwner) { response ->
|
||||||
|
handleLoaderResponse(response, permLauncher)
|
||||||
|
}
|
||||||
|
|
||||||
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,15 +257,61 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoaderResponse(response: MusicStore.Response?) {
|
private fun handleLoaderResponse(
|
||||||
|
response: MusicStore.Response?,
|
||||||
|
permLauncher: ActivityResultLauncher<String>
|
||||||
|
) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
when (response) {
|
|
||||||
is MusicStore.Response.Ok -> binding.homeFab.show()
|
|
||||||
|
|
||||||
// While loading or during an error, make sure we keep the shuffle fab hidden so
|
if (response is MusicStore.Response.Ok) {
|
||||||
// that any kind of playback is impossible. PlaybackStateManager also relies on this
|
binding.homeFab.show()
|
||||||
// invariant, so please don't change it.
|
binding.homeLoadingContainer.visibility = View.INVISIBLE
|
||||||
else -> binding.homeFab.hide()
|
binding.homePager.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.homeFab.hide()
|
||||||
|
binding.homePager.visibility = View.INVISIBLE
|
||||||
|
binding.homeLoadingContainer.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
logD("Received non-ok response $response")
|
||||||
|
|
||||||
|
when (response) {
|
||||||
|
is MusicStore.Response.Ok -> error("Unreachable")
|
||||||
|
is MusicStore.Response.Err -> {
|
||||||
|
logD("Received Response.Err")
|
||||||
|
binding.homeLoadingProgress.visibility = View.INVISIBLE
|
||||||
|
binding.homeLoadingStatus.textSafe = getString(R.string.err_load_failed)
|
||||||
|
binding.homeLoadingAction.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
text = getString(R.string.lbl_retry)
|
||||||
|
setOnClickListener { musicModel.reloadMusic(requireContext()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MusicStore.Response.NoMusic -> {
|
||||||
|
binding.homeLoadingProgress.visibility = View.INVISIBLE
|
||||||
|
binding.homeLoadingStatus.textSafe = getString(R.string.err_no_music)
|
||||||
|
binding.homeLoadingAction.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
text = getString(R.string.lbl_retry)
|
||||||
|
setOnClickListener { musicModel.reloadMusic(requireContext()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MusicStore.Response.NoPerms -> {
|
||||||
|
binding.homeLoadingProgress.visibility = View.INVISIBLE
|
||||||
|
binding.homeLoadingStatus.textSafe = getString(R.string.err_no_perms)
|
||||||
|
binding.homeLoadingAction.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
text = getString(R.string.lbl_grant)
|
||||||
|
setOnClickListener {
|
||||||
|
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null -> {
|
||||||
|
binding.homeLoadingStatus.textSafe = getString(R.string.lbl_loading)
|
||||||
|
binding.homeLoadingAction.visibility = View.INVISIBLE
|
||||||
|
binding.homeLoadingProgress.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,17 +53,13 @@ class MusicStore private constructor() {
|
||||||
|
|
||||||
private val callbacks = mutableListOf<Callback>()
|
private val callbacks = mutableListOf<Callback>()
|
||||||
|
|
||||||
/**
|
/** Add a callback to this instance. Make sure to remove it when done. */
|
||||||
* Add a callback to this instance. Make sure to remove it when done.
|
|
||||||
*/
|
|
||||||
fun addCallback(callback: Callback) {
|
fun addCallback(callback: Callback) {
|
||||||
response?.let(callback::onMusicUpdate)
|
response?.let(callback::onMusicUpdate)
|
||||||
callbacks.add(callback)
|
callbacks.add(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Remove a callback from this instance. */
|
||||||
* Remove a callback from this instance.
|
|
||||||
*/
|
|
||||||
fun removeCallback(callback: Callback) {
|
fun removeCallback(callback: Callback) {
|
||||||
callbacks.remove(callback)
|
callbacks.remove(callback)
|
||||||
}
|
}
|
||||||
|
@ -78,7 +74,7 @@ class MusicStore private constructor() {
|
||||||
return newResponse
|
return newResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadImpl(context: Context): Response {
|
private suspend fun loadImpl(context: Context): Response {
|
||||||
val notGranted =
|
val notGranted =
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
||||||
PackageManager.PERMISSION_DENIED
|
PackageManager.PERMISSION_DENIED
|
||||||
|
|
|
@ -62,8 +62,10 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
||||||
val songs = ConcurrentLinkedQueue<Song>()
|
val songs = ConcurrentLinkedQueue<Song>()
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
|
// Note: This call to buildAudio does not populate the genre field. This is
|
||||||
|
// because indexing genres is quite slow with MediaStore, and so keeping the
|
||||||
|
// field blank on unsupported ExoPlayer formats ends up being preferable.
|
||||||
val audio = inner.buildAudio(context, cursor)
|
val audio = inner.buildAudio(context, cursor)
|
||||||
|
|
||||||
val audioUri = requireNotNull(audio.id) { "Malformed audio: No id" }.audioUri
|
val audioUri = requireNotNull(audio.id) { "Malformed audio: No id" }.audioUri
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -182,11 +184,7 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/** The amount of tasks this backend can run efficiently at once. */
|
||||||
* The amount of tasks this backend can run efficiently at one time. Eight was chosen here
|
|
||||||
* as higher values made little difference in speed, and lower values generally caused
|
|
||||||
* bottlenecks.
|
|
||||||
*/
|
|
||||||
private const val TASK_CAPACITY = 8
|
private const val TASK_CAPACITY = 8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,11 +74,11 @@ object Indexer {
|
||||||
// Sanity check: Ensure that all songs are linked up to albums/artists/genres.
|
// Sanity check: Ensure that all songs are linked up to albums/artists/genres.
|
||||||
for (song in songs) {
|
for (song in songs) {
|
||||||
if (song._isMissingAlbum || song._isMissingArtist || song._isMissingGenre) {
|
if (song._isMissingAlbum || song._isMissingArtist || song._isMissingGenre) {
|
||||||
throw IllegalStateException(
|
error(
|
||||||
"Found malformed song: ${song.rawName} [" +
|
"Found unlinked song: ${song.rawName} [" +
|
||||||
"album: ${!song._isMissingAlbum} " +
|
"missing album: ${song._isMissingAlbum} " +
|
||||||
"artist: ${!song._isMissingArtist} " +
|
"missing artist: ${song._isMissingArtist} " +
|
||||||
"genre: ${!song._isMissingGenre}]")
|
"missing genre: ${song._isMissingGenre}]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,8 +99,10 @@ object Indexer {
|
||||||
backend.query(context).use { cursor ->
|
backend.query(context).use { cursor ->
|
||||||
val loadStart = System.currentTimeMillis()
|
val loadStart = System.currentTimeMillis()
|
||||||
logD("Successfully queried media database in ${loadStart - queryStart}ms")
|
logD("Successfully queried media database in ${loadStart - queryStart}ms")
|
||||||
|
|
||||||
val songs = backend.loadSongs(context, cursor)
|
val songs = backend.loadSongs(context, cursor)
|
||||||
logD("Successfully loaded songs in ${System.currentTimeMillis() - loadStart}ms")
|
logD("Successfully loaded songs in ${System.currentTimeMillis() - loadStart}ms")
|
||||||
|
|
||||||
songs
|
songs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +183,6 @@ object Indexer {
|
||||||
for (entry in albumsByArtist) {
|
for (entry in albumsByArtist) {
|
||||||
// The first album will suffice for template metadata.
|
// The first album will suffice for template metadata.
|
||||||
val templateAlbum = entry.value[0]
|
val templateAlbum = entry.value[0]
|
||||||
|
|
||||||
artists.add(Artist(rawName = templateAlbum._artistGroupingName, albums = entry.value))
|
artists.add(Artist(rawName = templateAlbum._artistGroupingName, albums = entry.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,4 +214,10 @@ object Indexer {
|
||||||
/** Create a list of songs from the [Cursor] queried in [query]. */
|
/** Create a list of songs from the [Cursor] queried in [query]. */
|
||||||
fun loadSongs(context: Context, cursor: Cursor): Collection<Song>
|
fun loadSongs(context: Context, cursor: Cursor): Collection<Song>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class Event {
|
||||||
|
object Query : Event()
|
||||||
|
object LoadSongs : Event()
|
||||||
|
object BuildLibrary : Event()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,7 +135,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
|
|
||||||
// The audio is not actually complete at this point, as we cannot obtain a genre
|
// The audio is not actually complete at this point, as we cannot obtain a genre
|
||||||
// through a song query. Instead, we have to do the hack where we iterate through
|
// through a song query. Instead, we have to do the hack where we iterate through
|
||||||
// every genre and assign it's name to each component song.
|
// every genre and assign it's name to audios that match it's child ID.
|
||||||
|
|
||||||
context.contentResolverSafe.useQuery(
|
context.contentResolverSafe.useQuery(
|
||||||
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
|
||||||
|
@ -149,35 +149,24 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
// anyway, so we skip genres that have them.
|
// anyway, so we skip genres that have them.
|
||||||
val id = genreCursor.getLong(idIndex)
|
val id = genreCursor.getLong(idIndex)
|
||||||
val name = genreCursor.getStringOrNull(nameIndex) ?: continue
|
val name = genreCursor.getStringOrNull(nameIndex) ?: continue
|
||||||
linkGenreAudios(context, id, name, audios)
|
|
||||||
|
context.contentResolverSafe.useQuery(
|
||||||
|
MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id),
|
||||||
|
arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor ->
|
||||||
|
val songIdIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val songId = cursor.getLong(songIdIndex)
|
||||||
|
audios.find { it.id == songId }?.let { song -> song.genre = name }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return audios.map { it.toSong() }
|
return audios.map { it.toSong() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Links up the given genre data ([genreId] and [genreName]) to the child audios connected to
|
|
||||||
* [genreId].
|
|
||||||
*/
|
|
||||||
private fun linkGenreAudios(
|
|
||||||
context: Context,
|
|
||||||
genreId: Long,
|
|
||||||
genreName: String,
|
|
||||||
audios: List<Audio>
|
|
||||||
) {
|
|
||||||
context.contentResolverSafe.useQuery(
|
|
||||||
MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, genreId),
|
|
||||||
arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor ->
|
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val id = cursor.getLong(idIndex)
|
|
||||||
audios.find { it.id == id }?.let { song -> song.genre = genreName }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The projection to use when querying media. Add version-specific columns here in an
|
* The projection to use when querying media. Add version-specific columns here in an
|
||||||
* implementation.
|
* implementation.
|
||||||
|
|
|
@ -90,9 +90,7 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
private val callbacks = mutableListOf<Callback>()
|
private val callbacks = mutableListOf<Callback>()
|
||||||
|
|
||||||
/**
|
/** Add a callback to this instance. Make sure to remove it when done. */
|
||||||
* Add a callback to this instance. Make sure to remove it when done.
|
|
||||||
*/
|
|
||||||
fun addCallback(callback: Callback) {
|
fun addCallback(callback: Callback) {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
callback.onNewPlayback(index, queue, parent)
|
callback.onNewPlayback(index, queue, parent)
|
||||||
|
|
|
@ -28,13 +28,62 @@
|
||||||
|
|
||||||
</org.oxycblt.auxio.ui.EdgeAppBarLayout>
|
</org.oxycblt.auxio.ui.EdgeAppBarLayout>
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
<FrameLayout
|
||||||
android:id="@+id/home_pager"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
android:animateLayoutChanges="true"
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||||
tools:layout="@layout/fragment_home_list" />
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/home_loading_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
android:paddingStart="@dimen/spacing_medium"
|
||||||
|
android:paddingEnd="@dimen/spacing_medium"
|
||||||
|
tools:visibility="invisible">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/home_loading_status"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="@dimen/spacing_medium"
|
||||||
|
android:textAppearance="@style/TextAppearance.Auxio.BodyLarge"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/home_loading_action"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
tools:text="Status" />
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
android:id="@+id/home_loading_progress"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:indeterminate="true"
|
||||||
|
app:indeterminateAnimationType="disjoint"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/home_loading_action"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/home_loading_action"
|
||||||
|
app:trackColor="@color/sel_track" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/home_loading_action"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/lbl_retry"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/home_loading_status" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
|
android:id="@+id/home_pager"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||||
|
tools:layout="@layout/fragment_home_list" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
<org.oxycblt.auxio.home.EdgeFabContainer
|
<org.oxycblt.auxio.home.EdgeFabContainer
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<string name="info_widget_desc">View and control music playback</string>
|
<string name="info_widget_desc">View and control music playback</string>
|
||||||
|
|
||||||
<!-- Label Namespace | Static Labels -->
|
<!-- Label Namespace | Static Labels -->
|
||||||
|
<string name="lbl_loading">Loading your music library…</string>
|
||||||
<string name="lbl_retry">Retry</string>
|
<string name="lbl_retry">Retry</string>
|
||||||
<string name="lbl_grant">Grant</string>
|
<string name="lbl_grant">Grant</string>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue