all: rework logging

Rework logging to be clearer and more standardized.

Rework all usages of lossing to follow a single unified style,
introducing a new "warn" option alongside this.
This commit is contained in:
OxygenCobalt 2022-02-22 17:08:57 -07:00
parent e1dbe6c40c
commit 3aaa2ab0e0
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
69 changed files with 436 additions and 339 deletions

View file

@ -29,7 +29,6 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.oxycblt.auxio.accent.Accent
import org.oxycblt.auxio.databinding.ActivityMainBinding import org.oxycblt.auxio.databinding.ActivityMainBinding
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.system.PlaybackService import org.oxycblt.auxio.playback.system.PlaybackService
@ -56,7 +55,7 @@ class MainActivity : AppCompatActivity() {
applyEdgeToEdgeWindow(binding) applyEdgeToEdgeWindow(binding)
logD("Activity created.") logD("Activity created")
} }
override fun onStart() { override fun onStart() {
@ -94,26 +93,29 @@ class MainActivity : AppCompatActivity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12, let dynamic colors be our accent and only enable the black theme option // Android 12, let dynamic colors be our accent and only enable the black theme option
if (isNight && settingsManager.useBlackTheme) { if (isNight && settingsManager.useBlackTheme) {
logD("Applying black theme [dynamic colors]")
setTheme(R.style.Theme_Auxio_Black) setTheme(R.style.Theme_Auxio_Black)
} }
} else { } else {
// Below android 12, load the accent and enable theme customization // Below android 12, load the accent and enable theme customization
AppCompatDelegate.setDefaultNightMode(settingsManager.theme) AppCompatDelegate.setDefaultNightMode(settingsManager.theme)
val newAccent = Accent.set(settingsManager.accent) val accent = settingsManager.accent
// The black theme has a completely separate set of styles since style attributes cannot // The black theme has a completely separate set of styles since style attributes cannot
// be modified at runtime. // be modified at runtime.
if (isNight && settingsManager.useBlackTheme) { if (isNight && settingsManager.useBlackTheme) {
setTheme(newAccent.blackTheme) logD("Applying black theme [with accent $accent]")
setTheme(accent.blackTheme)
} else { } else {
setTheme(newAccent.theme) logD("Applying normal theme [with accent $accent]")
setTheme(accent.theme)
} }
} }
} }
private fun applyEdgeToEdgeWindow(binding: ViewBinding) { private fun applyEdgeToEdgeWindow(binding: ViewBinding) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
logD("Doing R+ edge-to-edge.") logD("Doing R+ edge-to-edge")
window?.setDecorFitsSystemWindows(false) window?.setDecorFitsSystemWindows(false)
@ -136,7 +138,7 @@ class MainActivity : AppCompatActivity() {
} }
} else { } else {
// Do old edge-to-edge otherwise. // Do old edge-to-edge otherwise.
logD("Doing legacy edge-to-edge.") logD("Doing legacy edge-to-edge")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
binding.root.apply { binding.root.apply {

View file

@ -36,6 +36,7 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
/** /**
* A wrapper around the home fragment that shows the playback fragment and controls * A wrapper around the home fragment that shows the playback fragment and controls
@ -110,7 +111,7 @@ class MainFragment : Fragment() {
// Error, show the error to the user // Error, show the error to the user
is MusicStore.Response.Err -> { is MusicStore.Response.Err -> {
logD("Received Error") logW("Received Error")
val errorRes = when (response.kind) { val errorRes = when (response.kind) {
MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music
@ -142,7 +143,7 @@ class MainFragment : Fragment() {
} }
} }
logD("Fragment Created.") logD("Fragment Created")
return binding.root return binding.root
} }

View file

@ -100,6 +100,9 @@ private val ACCENT_PRIMARY_COLORS = arrayOf(
/** /**
* The data object for an accent. In the UI this is known as a "Color Scheme." * The data object for an accent. In the UI this is known as a "Color Scheme."
* This can be nominally used to gleam some attributes about a given color scheme, but this
* is not recommended. Attributes are usually the better option in nearly all cases.
*
* @property name The name of this accent * @property name The name of this accent
* @property theme The theme resource for this accent * @property theme The theme resource for this accent
* @property blackTheme The black theme resource for this accent * @property blackTheme The black theme resource for this accent
@ -111,36 +114,4 @@ data class Accent(val index: Int) {
val theme: Int get() = ACCENT_THEMES[index] val theme: Int get() = ACCENT_THEMES[index]
val blackTheme: Int get() = ACCENT_BLACK_THEMES[index] val blackTheme: Int get() = ACCENT_BLACK_THEMES[index]
val primary: Int get() = ACCENT_PRIMARY_COLORS[index] val primary: Int get() = ACCENT_PRIMARY_COLORS[index]
companion object {
@Volatile
private var CURRENT: Accent? = null
/**
* Get the current accent.
* @return The current accent
* @throws IllegalStateException When the accent has not been set.
*/
fun get(): Accent {
val cur = CURRENT
if (cur != null) {
return cur
}
error("Accent must be set before retrieving it.")
}
/**
* Set the current accent.
* @return The new accent
*/
fun set(accent: Accent): Accent {
synchronized(this) {
CURRENT = accent
}
return accent
}
}
} }

View file

@ -77,12 +77,10 @@ class AccentAdapter(
val context = binding.accent.context val context = binding.accent.context
binding.accent.isEnabled = !isSelected binding.accent.isEnabled = !isSelected
binding.accent.imageTintList = if (isSelected) { binding.accent.imageTintList = if (isSelected) {
// Switch out the currently selected ViewHolder with this one. // Switch out the currently selected ViewHolder with this one.
selectedViewHolder?.setSelected(false) selectedViewHolder?.setSelected(false)
selectedViewHolder = this selectedViewHolder = this
context.getAttrColorSafe(R.attr.colorSurface).stateList context.getAttrColorSafe(R.attr.colorSurface).stateList
} else { } else {
context.getColorSafe(android.R.color.transparent).stateList context.getColorSafe(android.R.color.transparent).stateList

View file

@ -34,9 +34,9 @@ import org.oxycblt.auxio.util.logD
* Dialog responsible for showing the list of accents to select. * Dialog responsible for showing the list of accents to select.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AccentDialog : LifecycleDialog() { class AccentCustomizeDialog : LifecycleDialog() {
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private var pendingAccent = Accent.get() private var pendingAccent = settingsManager.accent
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -53,18 +53,18 @@ class AccentDialog : LifecycleDialog() {
binding.accentRecycler.apply { binding.accentRecycler.apply {
adapter = AccentAdapter(pendingAccent) { accent -> adapter = AccentAdapter(pendingAccent) { accent ->
logD("Switching selected accent to $accent")
pendingAccent = accent pendingAccent = accent
} }
} }
logD("Dialog created.") logD("Dialog created")
return binding.root return binding.root
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putInt(KEY_PENDING_ACCENT, pendingAccent.index) outState.putInt(KEY_PENDING_ACCENT, pendingAccent.index)
} }
@ -72,9 +72,9 @@ class AccentDialog : LifecycleDialog() {
builder.setTitle(R.string.set_accent) builder.setTitle(R.string.set_accent)
builder.setPositiveButton(android.R.string.ok) { _, _ -> builder.setPositiveButton(android.R.string.ok) { _, _ ->
if (pendingAccent != Accent.get()) { if (pendingAccent != settingsManager.accent) {
logD("Applying new accent")
settingsManager.accent = pendingAccent settingsManager.accent = pendingAccent
requireActivity().recreate() requireActivity().recreate()
} }

View file

@ -30,7 +30,7 @@ import kotlin.math.max
* of the RecyclerView. * of the RecyclerView.
* Adapted from this StackOverflow answer: https://stackoverflow.com/a/30256880/14143986 * Adapted from this StackOverflow answer: https://stackoverflow.com/a/30256880/14143986
*/ */
class AutoGridLayoutManager( class AccentGridLayoutManager(
context: Context, context: Context,
attrs: AttributeSet, attrs: AttributeSet,
defStyleAttr: Int, defStyleAttr: Int,

View file

@ -27,6 +27,7 @@ import okio.source
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import android.util.Size as AndroidSize import android.util.Size as AndroidSize
@ -55,6 +56,7 @@ abstract class AuxioFetcher : Fetcher {
fetchMediaStoreCovers(context, album) fetchMediaStoreCovers(context, album)
} }
} catch (e: Exception) { } catch (e: Exception) {
logW("Unable to extract album art due to an error")
null null
} }
} }
@ -80,7 +82,6 @@ abstract class AuxioFetcher : Fetcher {
// music app which relies on proprietary OneUI extensions instead of AOSP. That means // music app which relies on proprietary OneUI extensions instead of AOSP. That means
// we have to have another layer of redundancy to retain quality. Thanks samsung. Prick. // we have to have another layer of redundancy to retain quality. Thanks samsung. Prick.
val result = fetchAospMetadataCovers(context, album) val result = fetchAospMetadataCovers(context, album)
if (result != null) { if (result != null) {
return result return result
} }
@ -88,7 +89,6 @@ abstract class AuxioFetcher : Fetcher {
// Our next fallback is to rely on ExoPlayer's largely half-baked and undocumented // Our next fallback is to rely on ExoPlayer's largely half-baked and undocumented
// metadata system. // metadata system.
val exoResult = fetchExoplayerCover(context, album) val exoResult = fetchExoplayerCover(context, album)
if (exoResult != null) { if (exoResult != null) {
return exoResult return exoResult
} }
@ -97,7 +97,6 @@ abstract class AuxioFetcher : Fetcher {
// going against the point of this setting. The previous two calls are just too unreliable // going against the point of this setting. The previous two calls are just too unreliable
// and we can't do any filesystem traversing due to scoped storage. // and we can't do any filesystem traversing due to scoped storage.
val mediaStoreResult = fetchMediaStoreCovers(context, album) val mediaStoreResult = fetchMediaStoreCovers(context, album)
if (mediaStoreResult != null) { if (mediaStoreResult != null) {
return mediaStoreResult return mediaStoreResult
} }
@ -192,7 +191,7 @@ abstract class AuxioFetcher : Fetcher {
} else if (stream != null) { } else if (stream != null) {
// In the case a front cover is not found, use the first image in the tag instead. // In the case a front cover is not found, use the first image in the tag instead.
// This can be corrected later on if a front cover frame is found. // This can be corrected later on if a front cover frame is found.
logD("No front cover image, using image of type $type instead") logW("No front cover image, using image of type $type instead")
stream = ByteArrayInputStream(pic) stream = ByteArrayInputStream(pic)
} }
@ -223,9 +222,10 @@ abstract class AuxioFetcher : Fetcher {
val increment = AndroidSize(mosaicSize.width / 2, mosaicSize.height / 2) val increment = AndroidSize(mosaicSize.width / 2, mosaicSize.height / 2)
val mosaicBitmap = Bitmap.createBitmap( val mosaicBitmap = Bitmap.createBitmap(
mosaicSize.width, mosaicSize.height, Bitmap.Config.ARGB_8888 mosaicSize.width,
mosaicSize.height,
Bitmap.Config.ARGB_8888
) )
val canvas = Canvas(mosaicBitmap) val canvas = Canvas(mosaicBitmap)
var x = 0 var x = 0

View file

@ -79,7 +79,6 @@ class ArtistImageFetcher private constructor(
override suspend fun fetch(): FetchResult? { override suspend fun fetch(): FetchResult? {
val albums = Sort.ByName(true) val albums = Sort.ByName(true)
.sortAlbums(artist.albums) .sortAlbums(artist.albums)
val results = albums.mapAtMost(4) { album -> val results = albums.mapAtMost(4) { album ->
fetchArt(context, album) fetchArt(context, album)
} }

View file

@ -40,6 +40,7 @@ import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
/** /**
@ -111,10 +112,11 @@ class AlbumDetailFragment : DetailFragment() {
// fragment should be launched otherwise. // fragment should be launched otherwise.
is Song -> { is Song -> {
if (detailModel.curAlbum.value!!.id == item.album.id) { if (detailModel.curAlbum.value!!.id == item.album.id) {
logD("Navigating to a song in this album")
scrollToItem(item.id, detailAdapter) scrollToItem(item.id, detailAdapter)
detailModel.finishNavToItem() detailModel.finishNavToItem()
} else { } else {
logD("Navigating to another album")
findNavController().navigate( findNavController().navigate(
AlbumDetailFragmentDirections.actionShowAlbum(item.album.id) AlbumDetailFragmentDirections.actionShowAlbum(item.album.id)
) )
@ -125,9 +127,11 @@ class AlbumDetailFragment : DetailFragment() {
// detail fragment. // detail fragment.
is Album -> { is Album -> {
if (detailModel.curAlbum.value!!.id == item.id) { if (detailModel.curAlbum.value!!.id == item.id) {
logD("Navigating to the top of this album")
binding.detailRecycler.scrollToPosition(0) binding.detailRecycler.scrollToPosition(0)
detailModel.finishNavToItem() detailModel.finishNavToItem()
} else { } else {
logD("Navigating to another album")
findNavController().navigate( findNavController().navigate(
AlbumDetailFragmentDirections.actionShowAlbum(item.id) AlbumDetailFragmentDirections.actionShowAlbum(item.id)
) )
@ -136,13 +140,14 @@ class AlbumDetailFragment : DetailFragment() {
// Always launch a new ArtistDetailFragment. // Always launch a new ArtistDetailFragment.
is Artist -> { is Artist -> {
logD("Navigating to another artist")
findNavController().navigate( findNavController().navigate(
AlbumDetailFragmentDirections.actionShowArtist(item.id) AlbumDetailFragmentDirections.actionShowArtist(item.id)
) )
} }
else -> { null -> {}
} else -> logW("Unsupported navigation item ${item::class.java}")
} }
} }
@ -161,7 +166,7 @@ class AlbumDetailFragment : DetailFragment() {
} }
} }
logD("Fragment created.") logD("Fragment created")
return binding.root return binding.root
} }

View file

@ -35,6 +35,7 @@ import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.ActionMenu import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
/** /**
* The [DetailFragment] for an artist. * The [DetailFragment] for an artist.
@ -98,25 +99,33 @@ class ArtistDetailFragment : DetailFragment() {
when (item) { when (item) {
is Artist -> { is Artist -> {
if (item.id == detailModel.curArtist.value?.id) { if (item.id == detailModel.curArtist.value?.id) {
logD("Navigating to the top of this artist")
binding.detailRecycler.scrollToPosition(0) binding.detailRecycler.scrollToPosition(0)
detailModel.finishNavToItem() detailModel.finishNavToItem()
} else { } else {
logD("Navigating to another artist")
findNavController().navigate( findNavController().navigate(
ArtistDetailFragmentDirections.actionShowArtist(item.id) ArtistDetailFragmentDirections.actionShowArtist(item.id)
) )
} }
} }
is Album -> findNavController().navigate( is Album -> {
ArtistDetailFragmentDirections.actionShowAlbum(item.id) logD("Navigating to another album")
) findNavController().navigate(
ArtistDetailFragmentDirections.actionShowAlbum(item.id)
is Song -> findNavController().navigate( )
ArtistDetailFragmentDirections.actionShowAlbum(item.album.id)
)
else -> {
} }
is Song -> {
logD("Navigating to another album")
findNavController().navigate(
ArtistDetailFragmentDirections.actionShowAlbum(item.album.id)
)
}
null -> {}
else -> logW("Unsupported navigation item ${item::class.java}")
} }
} }
@ -141,7 +150,7 @@ class ArtistDetailFragment : DetailFragment() {
} }
} }
logD("Fragment created.") logD("Fragment created")
return binding.root return binding.root
} }

View file

@ -14,6 +14,9 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.EdgeAppBarLayout import org.oxycblt.auxio.ui.EdgeAppBarLayout
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logTraceOrThrow
import java.lang.Exception
/** /**
* An [EdgeAppBarLayout] variant that also shows the name of the toolbar whenever the detail * An [EdgeAppBarLayout] variant that also shows the name of the toolbar whenever the detail
@ -39,9 +42,8 @@ class DetailAppBarLayout @JvmOverloads constructor(
(layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context) (layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context)
} }
private fun findTitleView(): AppCompatTextView { private fun findTitleView(): AppCompatTextView? {
val titleView = mTitleView val titleView = mTitleView
if (titleView != null) { if (titleView != null) {
return titleView return titleView
} }
@ -49,13 +51,18 @@ class DetailAppBarLayout @JvmOverloads constructor(
val toolbar = findViewById<Toolbar>(R.id.detail_toolbar) val toolbar = findViewById<Toolbar>(R.id.detail_toolbar)
// Reflect to get the actual title view to do transformations on // Reflect to get the actual title view to do transformations on
val newTitleView = Toolbar::class.java.getDeclaredField("mTitleTextView").run { val newTitleView = try {
isAccessible = true Toolbar::class.java.getDeclaredField("mTitleTextView").run {
get(toolbar) as AppCompatTextView isAccessible = true
get(toolbar) as AppCompatTextView
}
} catch (e: Exception) {
logE("Could not get toolbar title view (likely an internal code change)")
e.logTraceOrThrow()
return null
} }
newTitleView.alpha = 0f newTitleView.alpha = 0f
mTitleView = newTitleView mTitleView = newTitleView
return newTitleView return newTitleView
} }
@ -95,11 +102,11 @@ class DetailAppBarLayout @JvmOverloads constructor(
to = 0f to = 0f
} }
if (titleView.alpha == to) return if (titleView?.alpha == to) return
mTitleAnimator = ValueAnimator.ofFloat(from, to).apply { mTitleAnimator = ValueAnimator.ofFloat(from, to).apply {
addUpdateListener { addUpdateListener {
titleView.alpha = it.animatedValue as Float titleView?.alpha = it.animatedValue as Float
} }
duration = resources.getInteger(R.integer.app_bar_elevation_anim_duration).toLong() duration = resources.getInteger(R.integer.app_bar_elevation_anim_duration).toLong()

View file

@ -31,6 +31,7 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.memberBinding import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.logD
/** /**
* A Base [Fragment] implementing the base features shared across all detail fragments. * A Base [Fragment] implementing the base features shared across all detail fragments.
@ -43,13 +44,11 @@ abstract class DetailFragment : Fragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
detailModel.setNavigating(false) detailModel.setNavigating(false)
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
// Cancel all pending menus when this fragment stops to prevent bugs/crashes // Cancel all pending menus when this fragment stops to prevent bugs/crashes
detailModel.finishShowMenu(null) detailModel.finishShowMenu(null)
} }
@ -94,7 +93,6 @@ abstract class DetailFragment : Fragment() {
binding.detailRecycler.apply { binding.detailRecycler.apply {
adapter = detailAdapter adapter = detailAdapter
setHasFixedSize(true) setHasFixedSize(true)
applySpans(gridLookup) applySpans(gridLookup)
} }
} }
@ -105,6 +103,8 @@ abstract class DetailFragment : Fragment() {
* @param showItem Which menu items to keep * @param showItem Which menu items to keep
*/ */
protected fun showMenu(config: DetailViewModel.MenuConfig, showItem: ((Int) -> Boolean)? = null) { protected fun showMenu(config: DetailViewModel.MenuConfig, showItem: ((Int) -> Boolean)? = null) {
logD("Launching menu [$config]")
PopupMenu(config.anchor.context, config.anchor).apply { PopupMenu(config.anchor.context, config.anchor).apply {
inflate(R.menu.menu_detail_sort) inflate(R.menu.menu_detail_sort)

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.logD
/** /**
* ViewModel that stores data for the [DetailFragment]s. This includes: * ViewModel that stores data for the [DetailFragment]s. This includes:
@ -77,12 +78,10 @@ class DetailViewModel : ViewModel() {
private set private set
private var currentMenuContext: DisplayMode? = null private var currentMenuContext: DisplayMode? = null
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
fun setGenre(id: Long) { fun setGenre(id: Long) {
if (mCurGenre.value?.id == id) return if (mCurGenre.value?.id == id) return
val musicStore = MusicStore.requireInstance() val musicStore = MusicStore.requireInstance()
mCurGenre.value = musicStore.genres.find { it.id == id } mCurGenre.value = musicStore.genres.find { it.id == id }
refreshGenreData() refreshGenreData()
@ -90,7 +89,6 @@ class DetailViewModel : ViewModel() {
fun setArtist(id: Long) { fun setArtist(id: Long) {
if (mCurArtist.value?.id == id) return if (mCurArtist.value?.id == id) return
val musicStore = MusicStore.requireInstance() val musicStore = MusicStore.requireInstance()
mCurArtist.value = musicStore.artists.find { it.id == id } mCurArtist.value = musicStore.artists.find { it.id == id }
refreshArtistData() refreshArtistData()
@ -98,7 +96,6 @@ class DetailViewModel : ViewModel() {
fun setAlbum(id: Long) { fun setAlbum(id: Long) {
if (mCurAlbum.value?.id == id) return if (mCurAlbum.value?.id == id) return
val musicStore = MusicStore.requireInstance() val musicStore = MusicStore.requireInstance()
mCurAlbum.value = musicStore.albums.find { it.id == id } mCurAlbum.value = musicStore.albums.find { it.id == id }
refreshAlbumData() refreshAlbumData()
@ -112,6 +109,7 @@ class DetailViewModel : ViewModel() {
mShowMenu.value = null mShowMenu.value = null
if (newMode != null) { if (newMode != null) {
logD("Applying new sort mode")
when (currentMenuContext) { when (currentMenuContext) {
DisplayMode.SHOW_ALBUMS -> { DisplayMode.SHOW_ALBUMS -> {
settingsManager.detailAlbumSort = newMode settingsManager.detailAlbumSort = newMode
@ -154,7 +152,9 @@ class DetailViewModel : ViewModel() {
} }
private fun refreshGenreData() { private fun refreshGenreData() {
val data = mutableListOf<BaseModel>(curGenre.value!!) logD("Refreshing genre data")
val genre = requireNotNull(curGenre.value)
val data = mutableListOf<BaseModel>(genre)
data.add( data.add(
ActionHeader( ActionHeader(
@ -175,7 +175,8 @@ class DetailViewModel : ViewModel() {
} }
private fun refreshArtistData() { private fun refreshArtistData() {
val artist = curArtist.value!! logD("Refreshing artist data")
val artist = requireNotNull(curArtist.value)
val data = mutableListOf<BaseModel>(artist) val data = mutableListOf<BaseModel>(artist)
data.add( data.add(
@ -206,7 +207,9 @@ class DetailViewModel : ViewModel() {
} }
private fun refreshAlbumData() { private fun refreshAlbumData() {
val data = mutableListOf<BaseModel>(curAlbum.value!!) logD("Refreshing album data")
val album = requireNotNull(curAlbum.value)
val data = mutableListOf<BaseModel>(album)
data.add( data.add(
ActionHeader( ActionHeader(

View file

@ -35,6 +35,7 @@ import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.ActionMenu import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
/** /**
* The [DetailFragment] for a genre. * The [DetailFragment] for a genre.
@ -79,20 +80,29 @@ class GenreDetailFragment : DetailFragment() {
detailModel.navToItem.observe(viewLifecycleOwner) { item -> detailModel.navToItem.observe(viewLifecycleOwner) { item ->
when (item) { when (item) {
// All items will launch new detail fragments. // All items will launch new detail fragments.
is Artist -> findNavController().navigate( is Artist -> {
GenreDetailFragmentDirections.actionShowArtist(item.id) logD("Navigating to another artist")
) findNavController().navigate(
GenreDetailFragmentDirections.actionShowArtist(item.id)
is Album -> findNavController().navigate( )
GenreDetailFragmentDirections.actionShowAlbum(item.id)
)
is Song -> findNavController().navigate(
GenreDetailFragmentDirections.actionShowAlbum(item.album.id)
)
else -> {
} }
is Album -> {
logD("Navigating to another album")
findNavController().navigate(
GenreDetailFragmentDirections.actionShowAlbum(item.id)
)
}
is Song -> {
logD("Navigating to another song")
findNavController().navigate(
GenreDetailFragmentDirections.actionShowAlbum(item.album.id)
)
}
null -> {}
else -> logW("Unsupported navigation command ${item::class.java}")
} }
} }
@ -115,7 +125,7 @@ class GenreDetailFragment : DetailFragment() {
} }
} }
logD("Fragment created.") logD("Fragment created")
return binding.root return binding.root
} }

View file

@ -58,7 +58,6 @@ class AlbumDetailAdapter(
is Album -> ALBUM_DETAIL_ITEM_TYPE is Album -> ALBUM_DETAIL_ITEM_TYPE
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
is Song -> ALBUM_SONG_ITEM_TYPE is Song -> ALBUM_SONG_ITEM_TYPE
else -> -1 else -> -1
} }
} }
@ -86,7 +85,6 @@ class AlbumDetailAdapter(
is Album -> (holder as AlbumDetailViewHolder).bind(item) is Album -> (holder as AlbumDetailViewHolder).bind(item)
is Song -> (holder as AlbumSongViewHolder).bind(item) is Song -> (holder as AlbumSongViewHolder).bind(item)
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item) is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
else -> { else -> {
} }
} }
@ -127,7 +125,6 @@ class AlbumDetailAdapter(
recycler.layoutManager?.findViewByPosition(pos)?.let { child -> recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
recycler.getChildViewHolder(child)?.let { recycler.getChildViewHolder(child)?.let {
currentHolder = it as Highlightable currentHolder = it as Highlightable
currentHolder?.setHighlighted(true) currentHolder?.setHighlighted(true)
} }
} }

View file

@ -33,12 +33,12 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.bindArtistInfo
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ActionHeaderViewHolder import org.oxycblt.auxio.ui.ActionHeaderViewHolder
import org.oxycblt.auxio.ui.BaseViewHolder import org.oxycblt.auxio.ui.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.ui.HeaderViewHolder import org.oxycblt.auxio.ui.HeaderViewHolder
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
@ -64,7 +64,6 @@ class ArtistDetailAdapter(
is Song -> ARTIST_SONG_ITEM_TYPE is Song -> ARTIST_SONG_ITEM_TYPE
is Header -> HeaderViewHolder.ITEM_TYPE is Header -> HeaderViewHolder.ITEM_TYPE
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
else -> -1 else -> -1
} }
} }
@ -174,7 +173,6 @@ class ArtistDetailAdapter(
recycler.layoutManager?.findViewByPosition(pos)?.let { child -> recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
recycler.getChildViewHolder(child)?.let { recycler.getChildViewHolder(child)?.let {
currentSongHolder = it as Highlightable currentSongHolder = it as Highlightable
currentSongHolder?.setHighlighted(true) currentSongHolder?.setHighlighted(true)
} }
} }
@ -205,11 +203,7 @@ class ArtistDetailAdapter(
.entries.maxByOrNull { it.value.size } .entries.maxByOrNull { it.value.size }
?.key ?: context.getString(R.string.def_genre) ?.key ?: context.getString(R.string.def_genre)
binding.detailInfo.text = context.getString( binding.detailInfo.bindArtistInfo(data)
R.string.fmt_counts,
context.getPluralSafe(R.plurals.fmt_album_count, data.albums.size),
context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size)
)
binding.detailPlayButton.setOnClickListener { binding.detailPlayButton.setOnClickListener {
playbackModel.playArtist(data, false) playbackModel.playArtist(data, false)

View file

@ -30,11 +30,11 @@ import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.bindGenreInfo
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ActionHeaderViewHolder import org.oxycblt.auxio.ui.ActionHeaderViewHolder
import org.oxycblt.auxio.ui.BaseViewHolder import org.oxycblt.auxio.ui.BaseViewHolder
import org.oxycblt.auxio.ui.DiffCallback import org.oxycblt.auxio.ui.DiffCallback
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
@ -54,7 +54,6 @@ class GenreDetailAdapter(
is Genre -> GENRE_DETAIL_ITEM_TYPE is Genre -> GENRE_DETAIL_ITEM_TYPE
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
is Song -> GENRE_SONG_ITEM_TYPE is Song -> GENRE_SONG_ITEM_TYPE
else -> -1 else -> -1
} }
} }
@ -121,7 +120,6 @@ class GenreDetailAdapter(
recycler.layoutManager?.findViewByPosition(pos)?.let { child -> recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
recycler.getChildViewHolder(child)?.let { recycler.getChildViewHolder(child)?.let {
currentHolder = it as Highlightable currentHolder = it as Highlightable
currentHolder?.setHighlighted(true) currentHolder?.setHighlighted(true)
} }
} }
@ -143,11 +141,7 @@ class GenreDetailAdapter(
} }
binding.detailName.text = data.resolvedName binding.detailName.text = data.resolvedName
binding.detailSubhead.bindGenreInfo(data)
binding.detailSubhead.apply {
text = context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size)
}
binding.detailInfo.text = data.totalDuration binding.detailInfo.text = data.totalDuration
binding.detailPlayButton.setOnClickListener { binding.detailPlayButton.setOnClickListener {

View file

@ -55,7 +55,6 @@ class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
writableDatabase.transaction { writableDatabase.transaction {
delete(TABLE_NAME, null, null) delete(TABLE_NAME, null, null)
logD("Deleted paths db") logD("Deleted paths db")
for (path in paths) { for (path in paths) {
@ -66,6 +65,8 @@ class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
} }
) )
} }
logD("Successfully wrote ${paths.size} paths to db")
} }
} }
@ -76,17 +77,20 @@ class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
assertBackgroundThread() assertBackgroundThread()
val paths = mutableListOf<String>() val paths = mutableListOf<String>()
readableDatabase.queryAll(TABLE_NAME) { cursor -> readableDatabase.queryAll(TABLE_NAME) { cursor ->
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
paths.add(cursor.getString(0)) paths.add(cursor.getString(0))
} }
} }
logD("Successfully read ${paths.size} paths from db")
return paths return paths
} }
companion object { companion object {
// Blacklist is still used here for compatibility reasons, please don't get
// your pants in a twist about it.
const val DB_VERSION = 1 const val DB_VERSION = 1
const val DB_NAME = "auxio_blacklist_database.db" const val DB_NAME = "auxio_blacklist_database.db"

View file

@ -77,13 +77,16 @@ class ExcludedDialog : LifecycleDialog() {
dialog.setOnShowListener { dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener { dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener {
logD("Opening launcher")
launcher.launch(null) launcher.launch(null)
} }
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener { dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
if (excludedModel.isModified) { if (excludedModel.isModified) {
logD("Committing changes")
saveAndRestart() saveAndRestart()
} else { } else {
logD("Dropping changes")
dismiss() dismiss()
} }
} }
@ -93,11 +96,10 @@ class ExcludedDialog : LifecycleDialog() {
excludedModel.paths.observe(viewLifecycleOwner) { paths -> excludedModel.paths.observe(viewLifecycleOwner) { paths ->
adapter.submitList(paths) adapter.submitList(paths)
binding.excludedEmpty.isVisible = paths.isEmpty() binding.excludedEmpty.isVisible = paths.isEmpty()
} }
logD("Dialog created.") logD("Dialog created")
return binding.root return binding.root
} }
@ -114,6 +116,7 @@ class ExcludedDialog : LifecycleDialog() {
private fun addDocTreePath(uri: Uri?) { private fun addDocTreePath(uri: Uri?) {
// A null URI means that the user left the file picker without picking a directory // A null URI means that the user left the file picker without picking a directory
if (uri == null) { if (uri == null) {
logD("No URI given (user closed the dialog)")
return return
} }
@ -142,6 +145,7 @@ class ExcludedDialog : LifecycleDialog() {
return getRootPath() + "/" + typeAndPath.last() return getRootPath() + "/" + typeAndPath.last()
} }
logD("Unsupported volume ${typeAndPath[0]}")
return null return null
} }
@ -156,7 +160,6 @@ class ExcludedDialog : LifecycleDialog() {
/** /**
* Get *just* the root path, nothing else is really needed. * Get *just* the root path, nothing else is really needed.
*/ */
@Suppress("DEPRECATION")
private fun getRootPath(): String { private fun getRootPath(): String {
return Environment.getExternalStorageDirectory().absolutePath return Environment.getExternalStorageDirectory().absolutePath
} }

View file

@ -27,6 +27,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.util.logD
/** /**
* ViewModel that acts as a wrapper around [ExcludedDatabase], allowing for the addition/removal * ViewModel that acts as a wrapper around [ExcludedDatabase], allowing for the addition/removal
@ -73,10 +74,13 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
*/ */
fun save(onDone: () -> Unit) { fun save(onDone: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val start = System.currentTimeMillis()
excludedDatabase.writePaths(mPaths.value!!) excludedDatabase.writePaths(mPaths.value!!)
dbPaths = mPaths.value!! dbPaths = mPaths.value!!
onDone() onDone()
this@ExcludedViewModel.logD(
"Path save completed successfully in ${System.currentTimeMillis() - start}ms"
)
} }
} }
@ -85,11 +89,14 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
*/ */
private fun loadDatabasePaths() { private fun loadDatabasePaths() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val start = System.currentTimeMillis()
dbPaths = excludedDatabase.readPaths() dbPaths = excludedDatabase.readPaths()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
mPaths.value = dbPaths.toMutableList() mPaths.value = dbPaths.toMutableList()
} }
this@ExcludedViewModel.logD(
"Path load completed successfully in ${System.currentTimeMillis() - start}ms"
)
} }
} }

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import org.oxycblt.auxio.util.getDimenSizeSafe import org.oxycblt.auxio.util.getDimenSizeSafe
import org.oxycblt.auxio.util.logD
import com.google.android.material.R as MaterialR import com.google.android.material.R as MaterialR
/** /**
@ -20,7 +21,10 @@ class AdaptiveFloatingActionButton @JvmOverloads constructor(
init { init {
size = SIZE_NORMAL size = SIZE_NORMAL
// Use a large FAB on large screens, as it makes it easier to touch.
if (resources.configuration.smallestScreenWidthDp >= 640) { if (resources.configuration.smallestScreenWidthDp >= 640) {
logD("Using large FAB configuration")
val largeFabSize = context.getDimenSizeSafe( val largeFabSize = context.getDimenSizeSafe(
MaterialR.dimen.m3_large_fab_size MaterialR.dimen.m3_large_fab_size
) )
@ -29,7 +33,6 @@ class AdaptiveFloatingActionButton @JvmOverloads constructor(
MaterialR.dimen.m3_large_fab_max_image_size MaterialR.dimen.m3_large_fab_max_image_size
) )
// Use a large FAB on large screens, as it makes it easier to touch.
customSize = largeFabSize customSize = largeFabSize
setMaxImageSize(largeImageSize) setMaxImageSize(largeImageSize)
} }

View file

@ -3,6 +3,7 @@ package org.oxycblt.auxio.home
import android.content.Context import android.content.Context
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import org.oxycblt.auxio.util.logD
/** /**
* A tag configuration strategy that automatically adapts the tab layout to the screen size. * A tag configuration strategy that automatically adapts the tab layout to the screen size.
@ -20,15 +21,22 @@ class AdaptiveTabStrategy(
val tabMode = homeModel.tabs[position] val tabMode = homeModel.tabs[position]
when { when {
width < 370 -> width < 370 -> {
logD("Using icon-only configuration")
tab.setIcon(tabMode.icon) tab.setIcon(tabMode.icon)
.setContentDescription(tabMode.string) .setContentDescription(tabMode.string)
}
width < 640 -> tab.setText(tabMode.string) width < 640 -> {
logD("Using text-only configuration")
tab.setText(tabMode.string)
}
else -> else -> {
logD("Using icon-and-text configuration")
tab.setIcon(tabMode.icon) tab.setIcon(tabMode.icon)
.setText(tabMode.string) .setText(tabMode.string)
}
} }
} }
} }

View file

@ -49,6 +49,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
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
/** /**
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail * The main "Launching Point" fragment of Auxio, allowing navigation to the detail
@ -77,16 +78,19 @@ class HomeFragment : Fragment() {
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.action_search -> { R.id.action_search -> {
logD("Navigating to search")
findNavController().navigate(HomeFragmentDirections.actionShowSearch()) findNavController().navigate(HomeFragmentDirections.actionShowSearch())
} }
R.id.action_settings -> { R.id.action_settings -> {
logD("Navigating to settings")
parentFragment?.parentFragment?.findNavController()?.navigate( parentFragment?.parentFragment?.findNavController()?.navigate(
MainFragmentDirections.actionShowSettings() MainFragmentDirections.actionShowSettings()
) )
} }
R.id.action_about -> { R.id.action_about -> {
logD("Navigating to about")
parentFragment?.parentFragment?.findNavController()?.navigate( parentFragment?.parentFragment?.findNavController()?.navigate(
MainFragmentDirections.actionShowAbout() MainFragmentDirections.actionShowAbout()
) )
@ -96,20 +100,16 @@ class HomeFragment : Fragment() {
R.id.option_sort_asc -> { R.id.option_sort_asc -> {
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
val new = homeModel.getSortForDisplay(homeModel.curTab.value!!) val new = homeModel.getSortForDisplay(homeModel.curTab.value!!)
.ascending(item.isChecked) .ascending(item.isChecked)
homeModel.updateCurrentSort(new) homeModel.updateCurrentSort(new)
} }
// Sorting option was selected, mark it as selected and update the mode // Sorting option was selected, mark it as selected and update the mode
else -> { else -> {
item.isChecked = true item.isChecked = true
val new = homeModel.getSortForDisplay(homeModel.curTab.value!!) val new = homeModel.getSortForDisplay(homeModel.curTab.value!!)
.assignId(item.itemId) .assignId(item.itemId)
homeModel.updateCurrentSort(requireNotNull(new)) homeModel.updateCurrentSort(requireNotNull(new))
} }
} }
@ -141,8 +141,8 @@ class HomeFragment : Fragment() {
set(recycler, slop * 3) // 3x seems to be the best fit here set(recycler, slop * 3) // 3x seems to be the best fit here
} }
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to reduce ViewPager sensitivity") logE("Unable to reduce ViewPager sensitivity (likely an internal code change)")
logE(e.stackTraceToString()) e.logTraceOrThrow()
} }
// We know that there will only be a fixed amount of tabs, so we manually set this // We know that there will only be a fixed amount of tabs, so we manually set this
@ -174,7 +174,7 @@ class HomeFragment : Fragment() {
is MusicStore.Response.Ok -> binding.homeFab.show() is MusicStore.Response.Ok -> binding.homeFab.show()
// While loading or during an error, make sure we keep the shuffle fab hidden so // While loading or during an error, make sure we keep the shuffle fab hidden so
// that any kind of loading is impossible. PlaybackStateManager also relies on this // that any kind of playback is impossible. PlaybackStateManager also relies on this
// invariant, so please don't change it. // invariant, so please don't change it.
else -> binding.homeFab.hide() else -> binding.homeFab.hide()
} }
@ -207,7 +207,7 @@ class HomeFragment : Fragment() {
homeModel.curTab.observe(viewLifecycleOwner) { t -> homeModel.curTab.observe(viewLifecycleOwner) { t ->
val tab = requireNotNull(t) val tab = requireNotNull(t)
// Make sure that we update the scrolling view and allowed menu items before whenever // Make sure that we update the scrolling view and allowed menu items whenever
// the tab changes. // the tab changes.
when (tab) { when (tab) {
DisplayMode.SHOW_SONGS -> updateSortMenu(sortItem, tab) DisplayMode.SHOW_SONGS -> updateSortMenu(sortItem, tab)
@ -229,8 +229,9 @@ class HomeFragment : Fragment() {
} }
detailModel.navToItem.observe(viewLifecycleOwner) { item -> detailModel.navToItem.observe(viewLifecycleOwner) { item ->
// The AppBarLayout gets confused and collapses when we navigate too fast, wait for it // The AppBarLayout gets confused when we navigate too fast, wait for it to draw
// to draw before we continue. // before we navigate.
// This is only here just in case a collapsing toolbar is re-added.
binding.homeAppbar.post { binding.homeAppbar.post {
when (item) { when (item) {
is Song -> findNavController().navigate( is Song -> findNavController().navigate(
@ -255,7 +256,7 @@ class HomeFragment : Fragment() {
} }
} }
logD("Fragment Created.") logD("Fragment Created")
return binding.root return binding.root
} }

View file

@ -32,6 +32,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.logD
/** /**
* The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state. * The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state.
@ -78,7 +79,6 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
viewModelScope.launch { viewModelScope.launch {
val musicStore = MusicStore.awaitInstance() val musicStore = MusicStore.awaitInstance()
mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs) mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs)
mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums) mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums)
mArtists.value = settingsManager.libArtistSort.sortParents(musicStore.artists) mArtists.value = settingsManager.libArtistSort.sortParents(musicStore.artists)
@ -90,6 +90,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
* Update the current tab based off of the new ViewPager position. * Update the current tab based off of the new ViewPager position.
*/ */
fun updateCurrentTab(pos: Int) { fun updateCurrentTab(pos: Int) {
logD("Updating current tab to ${tabs[pos]}")
mCurTab.value = tabs[pos] mCurTab.value = tabs[pos]
} }
@ -110,6 +111,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
* Update the currently displayed item's [Sort]. * Update the currently displayed item's [Sort].
*/ */
fun updateCurrentSort(sort: Sort) { fun updateCurrentSort(sort: Sort) {
logD("Updating ${mCurTab.value} sort to $sort")
when (mCurTab.value) { when (mCurTab.value) {
DisplayMode.SHOW_SONGS -> { DisplayMode.SHOW_SONGS -> {
settingsManager.libSongSort = sort settingsManager.libSongSort = sort

View file

@ -109,7 +109,7 @@ sealed class Tab(open val mode: DisplayMode) {
// For safety, return null if we have an empty or larger-than-expected tab array. // For safety, return null if we have an empty or larger-than-expected tab array.
if (distinct.isEmpty() || distinct.size < SEQUENCE_LEN) { if (distinct.isEmpty() || distinct.size < SEQUENCE_LEN) {
logE("Sequence size was ${distinct.size}, which is invalid.") logE("Sequence size was ${distinct.size}, which is invalid")
return null return null
} }

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.databinding.DialogTabsBinding
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.LifecycleDialog import org.oxycblt.auxio.ui.LifecycleDialog
import org.oxycblt.auxio.util.logD
/** /**
* The dialog for customizing library tabs. This dialog does not rely on any specific ViewModel * The dialog for customizing library tabs. This dialog does not rely on any specific ViewModel
@ -49,7 +50,6 @@ class TabCustomizeDialog : LifecycleDialog() {
if (savedInstanceState != null) { if (savedInstanceState != null) {
// Restore any pending tab configurations // Restore any pending tab configurations
val tabs = Tab.fromSequence(savedInstanceState.getInt(KEY_TABS)) val tabs = Tab.fromSequence(savedInstanceState.getInt(KEY_TABS))
if (tabs != null) { if (tabs != null) {
pendingTabs = tabs pendingTabs = tabs
} }
@ -66,10 +66,9 @@ class TabCustomizeDialog : LifecycleDialog() {
// of how ViewHolders are bound], but instead simply look for the mode in // of how ViewHolders are bound], but instead simply look for the mode in
// the list of pending tabs and update that instead. // the list of pending tabs and update that instead.
val index = pendingTabs.indexOfFirst { it.mode == tab.mode } val index = pendingTabs.indexOfFirst { it.mode == tab.mode }
if (index != -1) { if (index != -1) {
val curTab = pendingTabs[index] val curTab = pendingTabs[index]
logD("Updating tab $curTab to $tab")
pendingTabs[index] = when (curTab) { pendingTabs[index] = when (curTab) {
is Tab.Visible -> Tab.Invisible(curTab.mode) is Tab.Visible -> Tab.Invisible(curTab.mode)
is Tab.Invisible -> Tab.Visible(curTab.mode) is Tab.Invisible -> Tab.Visible(curTab.mode)
@ -93,7 +92,6 @@ class TabCustomizeDialog : LifecycleDialog() {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putInt(KEY_TABS, Tab.toSequence(pendingTabs)) outState.putInt(KEY_TABS, Tab.toSequence(pendingTabs))
} }
@ -101,6 +99,7 @@ class TabCustomizeDialog : LifecycleDialog() {
builder.setTitle(R.string.set_lib_tabs) builder.setTitle(R.string.set_lib_tabs)
builder.setPositiveButton(android.R.string.ok) { _, _ -> builder.setPositiveButton(android.R.string.ok) { _, _ ->
logD("Committing tab changes")
settingsManager.libTabs = pendingTabs settingsManager.libTabs = pendingTabs
} }

View file

@ -124,8 +124,12 @@ data class Song(
val internalGroupingArtistName: String get() = internalMediaStoreAlbumArtistName val internalGroupingArtistName: String get() = internalMediaStoreAlbumArtistName
?: internalMediaStoreArtistName ?: MediaStore.UNKNOWN_STRING ?: internalMediaStoreArtistName ?: MediaStore.UNKNOWN_STRING
/** Internal field. Do not use. */
val internalIsMissingAlbum: Boolean get() = mAlbum == null
/** Internal field. Do not use. */
val internalIsMissingArtist: Boolean get() = mAlbum?.internalIsMissingArtist ?: true
/** Internal field. Do not use. **/ /** Internal field. Do not use. **/
val internalMissingGenre: Boolean get() = mGenre == null val internalIsMissingGenre: Boolean get() = mGenre == null
/** Internal method. Do not use. */ /** Internal method. Do not use. */
fun internalLinkAlbum(album: Album) { fun internalLinkAlbum(album: Album) {
@ -180,6 +184,9 @@ data class Album(
val resolvedArtistName: String get() = val resolvedArtistName: String get() =
artist.resolvedName artist.resolvedName
/** Internal field. Do not use. */
val internalIsMissingArtist: Boolean = mArtist != null
/** Internal method. Do not use. */ /** Internal method. Do not use. */
fun internalLinkArtist(artist: Artist) { fun internalLinkArtist(artist: Artist) {
mArtist = artist mArtist = artist

View file

@ -7,8 +7,8 @@ import android.provider.MediaStore
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.excluded.ExcludedDatabase import org.oxycblt.auxio.excluded.ExcludedDatabase
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import java.lang.Exception
/** /**
* This class acts as the base for most the black magic required to get a remotely sensible music * This class acts as the base for most the black magic required to get a remotely sensible music
@ -26,7 +26,7 @@ import java.lang.Exception
* have to query for each genre, query all the songs in each genre, and then iterate through those * have to query for each genre, query all the songs in each genre, and then iterate through those
* songs to link every song with their genre. This is not documented anywhere, and the * songs to link every song with their genre. This is not documented anywhere, and the
* O(mom im scared) algorithm you have to run to get it working single-handedly DOUBLES Auxio's * O(mom im scared) algorithm you have to run to get it working single-handedly DOUBLES Auxio's
* loading times. At no point have the devs considered that this column is absolutely insane, and * loading times. At no point have the devs considered that this system is absolutely insane, and
* instead focused on adding infuriat- I mean nice proprietary extensions to MediaStore for their * instead focused on adding infuriat- I mean nice proprietary extensions to MediaStore for their
* own Google Play Music, and of course every Google Play Music user knew how great that turned * own Google Play Music, and of course every Google Play Music user knew how great that turned
* out! * out!
@ -88,14 +88,14 @@ class MusicLoader {
val artists = buildArtists(context, albums) val artists = buildArtists(context, albums)
val genres = readGenres(context, songs) val genres = readGenres(context, songs)
// Sanity check: Ensure that all songs are well-formed. // Sanity check: Ensure that all songs are linked up to albums/artists/genres.
for (song in songs) { for (song in songs) {
try { if (song.internalIsMissingAlbum ||
song.album.artist song.internalIsMissingArtist ||
song.genre song.internalIsMissingGenre
} catch (e: Exception) { ) {
logE("Found malformed song: ${song.name}") logE("Found malformed song: ${song.name}")
throw e throw IllegalStateException()
} }
} }
@ -204,6 +204,8 @@ class MusicLoader {
it.internalMediaStoreAlbumArtistName to it.track to it.duration it.internalMediaStoreAlbumArtistName to it.track to it.duration
}.toMutableList() }.toMutableList()
logD("Successfully loaded ${songs.size} songs")
return songs return songs
} }
@ -247,6 +249,8 @@ class MusicLoader {
) )
} }
logD("Successfully built ${albums.size} albums")
return albums return albums
} }
@ -264,14 +268,15 @@ class MusicLoader {
// Album deduplication does not eliminate every case of fragmented artists, do // Album deduplication does not eliminate every case of fragmented artists, do
// we deduplicate in the artist creation step as well. // we deduplicate in the artist creation step as well.
// Note that we actually don't do this in groupBy. This is generally because we // Note that we actually don't do this in groupBy. This is generally because using
// only want to default to a lowercase artist name when we have no other choice. // a template song may not result in the best possible artist name in all cases.
val previousArtistIndex = artists.indexOfFirst { artist -> val previousArtistIndex = artists.indexOfFirst { artist ->
artist.name.lowercase() == artistName.lowercase() artist.name.lowercase() == artistName.lowercase()
} }
if (previousArtistIndex > -1) { if (previousArtistIndex > -1) {
val previousArtist = artists[previousArtistIndex] val previousArtist = artists[previousArtistIndex]
logD("Merging duplicate artist into pre-existing artist ${previousArtist.name}")
artists[previousArtistIndex] = Artist( artists[previousArtistIndex] = Artist(
previousArtist.name, previousArtist.name,
previousArtist.resolvedName, previousArtist.resolvedName,
@ -288,6 +293,8 @@ class MusicLoader {
} }
} }
logD("Successfully built ${artists.size} artists")
return artists return artists
} }
@ -327,7 +334,7 @@ class MusicLoader {
} }
} }
val songsWithoutGenres = songs.filter { it.internalMissingGenre } val songsWithoutGenres = songs.filter { it.internalIsMissingGenre }
if (songsWithoutGenres.isNotEmpty()) { if (songsWithoutGenres.isNotEmpty()) {
// Songs that don't have a genre will be thrown into an unknown genre. // Songs that don't have a genre will be thrown into an unknown genre.
@ -340,6 +347,8 @@ class MusicLoader {
genres.add(unknownGenre) genres.add(unknownGenre)
} }
logD("Successfully loaded ${genres.size} genres")
return genres return genres
} }

View file

@ -55,7 +55,7 @@ class MusicStore private constructor() {
* Load/Sort the entire music library. Should always be ran on a coroutine. * Load/Sort the entire music library. Should always be ran on a coroutine.
*/ */
private fun load(context: Context): Response { private fun load(context: Context): Response {
logD("Starting initial music load...") logD("Starting initial music load")
val notGranted = ContextCompat.checkSelfPermission( val notGranted = ContextCompat.checkSelfPermission(
context, Manifest.permission.READ_EXTERNAL_STORAGE context, Manifest.permission.READ_EXTERNAL_STORAGE
@ -76,11 +76,10 @@ class MusicStore private constructor() {
mArtists = library.artists mArtists = library.artists
mGenres = library.genres mGenres = library.genres
logD("Music load completed successfully in ${System.currentTimeMillis() - start}ms.") logD("Music load completed successfully in ${System.currentTimeMillis() - start}ms")
} catch (e: Exception) { } catch (e: Exception) {
logE("Something went horribly wrong.") logE("Something went horribly wrong")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
return Response.Err(ErrorKind.FAILED) return Response.Err(ErrorKind.FAILED)
} }
@ -117,6 +116,7 @@ class MusicStore private constructor() {
/** /**
* A response that [MusicStore] returns when loading music. * A response that [MusicStore] returns when loading music.
* And before you ask, yes, I do like rust. * And before you ask, yes, I do like rust.
* TODO: Replace this with the kotlin builtin
*/ */
sealed class Response { sealed class Response {
class Ok(val musicStore: MusicStore) : Response() class Ok(val musicStore: MusicStore) : Response()
@ -201,7 +201,7 @@ class MusicStore private constructor() {
*/ */
fun requireInstance(): MusicStore { fun requireInstance(): MusicStore {
return requireNotNull(maybeGetInstance()) { return requireNotNull(maybeGetInstance()) {
"Required MusicStore instance was not available." "Required MusicStore instance was not available"
} }
} }

View file

@ -25,6 +25,8 @@ import androidx.core.text.isDigitsOnly
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getPluralSafe import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
/** /**
* A complete array of all the hardcoded genre values for ID3(v2), contains standard genres and * A complete array of all the hardcoded genre values for ID3(v2), contains standard genres and
@ -98,6 +100,7 @@ fun String.getGenreNameCompat(): String? {
*/ */
fun Long.toDuration(isElapsed: Boolean): String { fun Long.toDuration(isElapsed: Boolean): String {
if (!isElapsed && this == 0L) { if (!isElapsed && this == 0L) {
logD("Non-elapsed duration is zero, using --:--")
return "--:--" return "--:--"
} }
@ -121,14 +124,53 @@ fun Int.toDate(context: Context): String {
// --- BINDING ADAPTERS --- // --- BINDING ADAPTERS ---
/** @BindingAdapter("songInfo")
* Bind the album + song counts for an artist fun TextView.bindSongInfo(song: Song?) {
*/ if (song == null) {
@BindingAdapter("artistCounts") logW("Song was null, not applying info")
fun TextView.bindArtistCounts(artist: Artist) { return
}
text = context.getString(
R.string.fmt_two,
song.resolvedArtistName,
song.resolvedAlbumName
)
}
@BindingAdapter("albumInfo")
fun TextView.bindAlbumInfo(album: Album?) {
if (album == null) {
logW("Album was null, not applying info")
return
}
text = context.getString(
R.string.fmt_two, album.resolvedArtistName,
context.getPluralSafe(R.plurals.fmt_song_count, album.songs.size)
)
}
@BindingAdapter("artistInfo")
fun TextView.bindArtistInfo(artist: Artist?) {
if (artist == null) {
logW("Artist was null, not applying info")
return
}
text = context.getString( text = context.getString(
R.string.fmt_counts, R.string.fmt_counts,
context.getPluralSafe(R.plurals.fmt_album_count, artist.albums.size), context.getPluralSafe(R.plurals.fmt_album_count, artist.albums.size),
context.getPluralSafe(R.plurals.fmt_song_count, artist.songs.size) context.getPluralSafe(R.plurals.fmt_song_count, artist.songs.size)
) )
} }
@BindingAdapter("genreInfo")
fun TextView.bindGenreInfo(genre: Genre?) {
if (genre == null) {
logW("Genre was null, not applying info")
return
}
text = context.getPluralSafe(R.plurals.fmt_song_count, genre.songs.size)
}

View file

@ -24,6 +24,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.util.logD
class MusicViewModel : ViewModel() { class MusicViewModel : ViewModel() {
private val mLoaderResponse = MutableLiveData<MusicStore.Response?>(null) private val mLoaderResponse = MutableLiveData<MusicStore.Response?>(null)
@ -37,6 +38,7 @@ class MusicViewModel : ViewModel() {
*/ */
fun loadMusic(context: Context) { fun loadMusic(context: Context) {
if (mLoaderResponse.value != null || isBusy) { if (mLoaderResponse.value != null || isBusy) {
logD("Loader is busy/already completed, not reloading")
return return
} }
@ -45,15 +47,14 @@ class MusicViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
val result = MusicStore.initInstance(context) val result = MusicStore.initInstance(context)
isBusy = false
mLoaderResponse.value = result mLoaderResponse.value = result
isBusy = false
} }
} }
fun reloadMusic(context: Context) { fun reloadMusic(context: Context) {
logD("Reloading music library")
mLoaderResponse.value = null mLoaderResponse.value = null
loadMusic(context) loadMusic(context)
} }
} }

View file

@ -92,7 +92,6 @@ class PlaybackFragment : Fragment() {
// Make marquee of song title work // Make marquee of song title work
binding.playbackSong.isSelected = true binding.playbackSong.isSelected = true
binding.playbackSeekBar.onConfirmListener = playbackModel::setPosition binding.playbackSeekBar.onConfirmListener = playbackModel::setPosition
binding.playbackPlayPause.post { binding.playbackPlayPause.post {
binding.playbackPlayPause.stateListAnimator = null binding.playbackPlayPause.stateListAnimator = null
} }
@ -101,11 +100,11 @@ class PlaybackFragment : Fragment() {
playbackModel.song.observe(viewLifecycleOwner) { song -> playbackModel.song.observe(viewLifecycleOwner) { song ->
if (song != null) { if (song != null) {
logD("Updating song display to ${song.name}.") logD("Updating song display to ${song.name}")
binding.song = song binding.song = song
binding.playbackSeekBar.setDuration(song.seconds) binding.playbackSeekBar.setDuration(song.seconds)
} else { } else {
logD("No song is being played, leaving.") logD("No song is being played, leaving")
findNavController().navigateUp() findNavController().navigateUp()
} }
} }
@ -152,7 +151,7 @@ class PlaybackFragment : Fragment() {
} }
} }
logD("Fragment Created.") logD("Fragment Created")
return binding.root return binding.root
} }

View file

@ -27,6 +27,7 @@ import org.oxycblt.auxio.util.disableDropShadowCompat
import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.getDimenSafe import org.oxycblt.auxio.util.getDimenSafe
import org.oxycblt.auxio.util.getDrawableSafe import org.oxycblt.auxio.util.getDrawableSafe
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.pxOfDp import org.oxycblt.auxio.util.pxOfDp
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
import org.oxycblt.auxio.util.stateList import org.oxycblt.auxio.util.stateList
@ -225,6 +226,8 @@ class PlaybackLayout @JvmOverloads constructor(
} }
private fun applyState(state: PanelState) { private fun applyState(state: PanelState) {
logD("Applying panel state $state")
// Dragging events are really complex and we don't want to mess up the state // Dragging events are really complex and we don't want to mess up the state
// while we are in one. // while we are in one.
if (state == panelState || panelState == PanelState.DRAGGING) { if (state == panelState || panelState == PanelState.DRAGGING) {
@ -357,10 +360,8 @@ class PlaybackLayout @JvmOverloads constructor(
// bottom navigation is consumed by a bar. To fix this, we modify the bottom insets // bottom navigation is consumed by a bar. To fix this, we modify the bottom insets
// to reflect the presence of the panel [at least in it's collapsed state] // to reflect the presence of the panel [at least in it's collapsed state]
playbackContainerView.dispatchApplyWindowInsets(insets) playbackContainerView.dispatchApplyWindowInsets(insets)
lastInsets = insets lastInsets = insets
applyContentWindowInsets() applyContentWindowInsets()
return insets return insets
} }
@ -370,7 +371,6 @@ class PlaybackLayout @JvmOverloads constructor(
*/ */
private fun applyContentWindowInsets() { private fun applyContentWindowInsets() {
val insets = lastInsets val insets = lastInsets
if (insets != null) { if (insets != null) {
contentView.dispatchApplyWindowInsets(adjustInsets(insets)) contentView.dispatchApplyWindowInsets(adjustInsets(insets))
} }
@ -386,8 +386,9 @@ class PlaybackLayout @JvmOverloads constructor(
val bars = insets.systemBarInsetsCompat val bars = insets.systemBarInsetsCompat
val consumedByPanel = computePanelTopPosition(panelOffset) - measuredHeight val consumedByPanel = computePanelTopPosition(panelOffset) - measuredHeight
val adjustedBottomInset = (consumedByPanel + bars.bottom).coerceAtLeast(0) val adjustedBottomInset = (consumedByPanel + bars.bottom).coerceAtLeast(0)
return insets.replaceSystemBarInsetsCompat(
return insets.replaceSystemBarInsetsCompat(bars.left, bars.top, bars.right, adjustedBottomInset) bars.left, bars.top, bars.right, adjustedBottomInset
)
} }
override fun onSaveInstanceState(): Parcelable = Bundle().apply { override fun onSaveInstanceState(): Parcelable = Bundle().apply {
@ -586,6 +587,8 @@ class PlaybackLayout @JvmOverloads constructor(
(computePanelTopPosition(0f) - topPosition).toFloat() / panelRange (computePanelTopPosition(0f) - topPosition).toFloat() / panelRange
private fun smoothSlideTo(offset: Float) { private fun smoothSlideTo(offset: Float) {
logD("Smooth sliding to $offset")
val okay = dragHelper.smoothSlideViewTo( val okay = dragHelper.smoothSlideViewTo(
playbackContainerView, playbackContainerView.left, computePanelTopPosition(offset) playbackContainerView, playbackContainerView.left, computePanelTopPosition(offset)
) )

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.databinding.ViewSeekBarBinding
import org.oxycblt.auxio.music.toDuration import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.stateList import org.oxycblt.auxio.util.stateList
/** /**
@ -73,6 +74,7 @@ class PlaybackSeekBar @JvmOverloads constructor(
// - The duration of the song was so low as to be rounded to zero when converted // - The duration of the song was so low as to be rounded to zero when converted
// to seconds. // to seconds.
// In either of these cases, the seekbar is more or less useless. Disable it. // In either of these cases, the seekbar is more or less useless. Disable it.
logD("Duration is 0, entering disabled state")
binding.seekBar.apply { binding.seekBar.apply {
valueTo = 1f valueTo = 1f
isEnabled = false isEnabled = false

View file

@ -111,7 +111,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
*/ */
fun playAlbum(album: Album, shuffled: Boolean) { fun playAlbum(album: Album, shuffled: Boolean) {
if (album.songs.isEmpty()) { if (album.songs.isEmpty()) {
logE("Album is empty, Not playing.") logE("Album is empty, Not playing")
return return
} }
@ -125,7 +125,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
*/ */
fun playArtist(artist: Artist, shuffled: Boolean) { fun playArtist(artist: Artist, shuffled: Boolean) {
if (artist.songs.isEmpty()) { if (artist.songs.isEmpty()) {
logE("Artist is empty, Not playing.") logE("Artist is empty, Not playing")
return return
} }
@ -139,7 +139,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
*/ */
fun playGenre(genre: Genre, shuffled: Boolean) { fun playGenre(genre: Genre, shuffled: Boolean) {
if (genre.songs.isEmpty()) { if (genre.songs.isEmpty()) {
logE("Genre is empty, Not playing.") logE("Genre is empty, Not playing")
return return
} }
@ -156,7 +156,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
if (playbackManager.isRestored && MusicStore.loaded()) { if (playbackManager.isRestored && MusicStore.loaded()) {
playWithUriInternal(uri, context) playWithUriInternal(uri, context)
} else { } else {
logD("Cant play this URI right now, waiting...") logD("Cant play this URI right now, waiting")
mIntentUri = uri mIntentUri = uri
} }
@ -213,12 +213,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* [apply] is called just before the change is committed so that the adapter can be updated. * [apply] is called just before the change is committed so that the adapter can be updated.
*/ */
fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) { fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) {
val adjusted = adapterIndex + (playbackManager.queue.size - mNextUp.value!!.size) val index = adapterIndex + (playbackManager.queue.size - mNextUp.value!!.size)
logD("$adjusted") if (index in playbackManager.queue.indices) {
if (adjusted in playbackManager.queue.indices) {
apply() apply()
playbackManager.removeQueueItem(adjusted) playbackManager.removeQueueItem(index)
} }
} }
/** /**
@ -227,10 +225,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
*/ */
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean { fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean {
val delta = (playbackManager.queue.size - mNextUp.value!!.size) val delta = (playbackManager.queue.size - mNextUp.value!!.size)
val from = adapterFrom + delta val from = adapterFrom + delta
val to = adapterTo + delta val to = adapterTo + delta
if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) { if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) {
apply() apply()
playbackManager.moveQueueItems(from, to) playbackManager.moveQueueItems(from, to)
@ -332,7 +328,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* [PlaybackStateManager] instance. * [PlaybackStateManager] instance.
*/ */
private fun restorePlaybackState() { private fun restorePlaybackState() {
logD("Attempting to restore playback state.") logD("Attempting to restore playback state")
onSongUpdate(playbackManager.song) onSongUpdate(playbackManager.song)
onPositionUpdate(playbackManager.position) onPositionUpdate(playbackManager.position)

View file

@ -70,11 +70,9 @@ class QueueAdapter(
QUEUE_SONG_ITEM_TYPE -> QueueSongViewHolder( QUEUE_SONG_ITEM_TYPE -> QueueSongViewHolder(
ItemQueueSongBinding.inflate(parent.context.inflater) ItemQueueSongBinding.inflate(parent.context.inflater)
) )
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context) ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
else -> error("Invalid ViewHolder item type $viewType")
else -> error("Invalid ViewHolder item type $viewType.")
} }
} }
@ -83,8 +81,7 @@ class QueueAdapter(
is Song -> (holder as QueueSongViewHolder).bind(item) is Song -> (holder as QueueSongViewHolder).bind(item)
is Header -> (holder as HeaderViewHolder).bind(item) is Header -> (holder as HeaderViewHolder).bind(item)
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item) is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
else -> logE("Bad data given to QueueAdapter")
else -> logE("Bad data given to QueueAdapter.")
} }
} }
@ -95,7 +92,6 @@ class QueueAdapter(
fun submitList(newData: MutableList<BaseModel>) { fun submitList(newData: MutableList<BaseModel>) {
if (data != newData) { if (data != newData) {
data = newData data = newData
listDiffer.submitList(newData) listDiffer.submitList(newData)
} }
} }

View file

@ -27,6 +27,7 @@ import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.getDimenSafe import org.oxycblt.auxio.util.getDimenSafe
import org.oxycblt.auxio.util.logD
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -89,9 +90,10 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
val holder = viewHolder as QueueAdapter.QueueSongViewHolder val holder = viewHolder as QueueAdapter.QueueSongViewHolder
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) { if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
logD("Lifting queue item")
val bg = holder.bodyView.background as MaterialShapeDrawable val bg = holder.bodyView.background as MaterialShapeDrawable
val elevation = recyclerView.context.getDimenSafe(R.dimen.elevation_small) val elevation = recyclerView.context.getDimenSafe(R.dimen.elevation_small)
holder.itemView.animate() holder.itemView.animate()
.translationZ(elevation) .translationZ(elevation)
.setDuration(100) .setDuration(100)
@ -127,8 +129,9 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
val holder = viewHolder as QueueAdapter.QueueSongViewHolder val holder = viewHolder as QueueAdapter.QueueSongViewHolder
if (holder.itemView.translationZ != 0.0f) { if (holder.itemView.translationZ != 0.0f) {
val bg = holder.bodyView.background as MaterialShapeDrawable logD("Dropping queue item")
val bg = holder.bodyView.background as MaterialShapeDrawable
holder.itemView.animate() holder.itemView.animate()
.translationZ(0.0f) .translationZ(0.0f)
.setDuration(100) .setDuration(100)

View file

@ -28,6 +28,7 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.logD
/** /**
* A [Fragment] that shows the queue and enables editing as well. * A [Fragment] that shows the queue and enables editing as well.
@ -77,9 +78,11 @@ class QueueFragment : Fragment() {
} }
playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling -> playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling ->
// Try to prevent the queue adapter from going spastic during reshuffle events
// by just scrolling back to the top.
if (isShuffling != lastShuffle) { if (isShuffling != lastShuffle) {
logD("Reshuffle event, scrolling to top")
lastShuffle = isShuffling lastShuffle = isShuffling
binding.queueRecycler.scrollToPosition(0) binding.queueRecycler.scrollToPosition(0)
} }
} }

View file

@ -48,10 +48,10 @@ class PlaybackStateDatabase(context: Context) :
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = nuke(db) override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = nuke(db)
private fun nuke(db: SQLiteDatabase) { private fun nuke(db: SQLiteDatabase) {
logD("Nuking database")
db.apply { db.apply {
execSQL("DROP TABLE IF EXISTS $TABLE_NAME_STATE") execSQL("DROP TABLE IF EXISTS $TABLE_NAME_STATE")
execSQL("DROP TABLE IF EXISTS $TABLE_NAME_QUEUE") execSQL("DROP TABLE IF EXISTS $TABLE_NAME_QUEUE")
onCreate(this) onCreate(this)
} }
} }
@ -103,34 +103,6 @@ class PlaybackStateDatabase(context: Context) :
// --- INTERFACE FUNCTIONS --- // --- INTERFACE FUNCTIONS ---
/**
* Clear the previously written [SavedState] and write a new one.
*/
fun writeState(state: SavedState) {
assertBackgroundThread()
writableDatabase.transaction {
delete(TABLE_NAME_STATE, null, null)
this@PlaybackStateDatabase.logD("Wiped state db.")
val stateData = ContentValues(10).apply {
put(StateColumns.COLUMN_ID, 0)
put(StateColumns.COLUMN_SONG_HASH, state.song?.id)
put(StateColumns.COLUMN_POSITION, state.position)
put(StateColumns.COLUMN_PARENT_HASH, state.parent?.id)
put(StateColumns.COLUMN_QUEUE_INDEX, state.queueIndex)
put(StateColumns.COLUMN_PLAYBACK_MODE, state.playbackMode.toInt())
put(StateColumns.COLUMN_IS_SHUFFLING, state.isShuffling)
put(StateColumns.COLUMN_LOOP_MODE, state.loopMode.toInt())
}
insert(TABLE_NAME_STATE, null, stateData)
}
logD("Wrote state to database.")
}
/** /**
* Read the stored [SavedState] from the database, if there is one. * Read the stored [SavedState] from the database, if there is one.
* @param musicStore Required to transform database songs/parents into actual instances * @param musicStore Required to transform database songs/parents into actual instances
@ -178,11 +150,69 @@ class PlaybackStateDatabase(context: Context) :
isShuffling = cursor.getInt(shuffleIndex) == 1, isShuffling = cursor.getInt(shuffleIndex) == 1,
loopMode = LoopMode.fromInt(cursor.getInt(loopModeIndex)) ?: LoopMode.NONE, loopMode = LoopMode.fromInt(cursor.getInt(loopModeIndex)) ?: LoopMode.NONE,
) )
logD("Successfully read playback state: $state")
} }
return state return state
} }
/**
* Clear the previously written [SavedState] and write a new one.
*/
fun writeState(state: SavedState) {
assertBackgroundThread()
writableDatabase.transaction {
delete(TABLE_NAME_STATE, null, null)
this@PlaybackStateDatabase.logD("Wiped state db")
val stateData = ContentValues(10).apply {
put(StateColumns.COLUMN_ID, 0)
put(StateColumns.COLUMN_SONG_HASH, state.song?.id)
put(StateColumns.COLUMN_POSITION, state.position)
put(StateColumns.COLUMN_PARENT_HASH, state.parent?.id)
put(StateColumns.COLUMN_QUEUE_INDEX, state.queueIndex)
put(StateColumns.COLUMN_PLAYBACK_MODE, state.playbackMode.toInt())
put(StateColumns.COLUMN_IS_SHUFFLING, state.isShuffling)
put(StateColumns.COLUMN_LOOP_MODE, state.loopMode.toInt())
}
insert(TABLE_NAME_STATE, null, stateData)
}
logD("Wrote state to database")
}
/**
* Read a list of queue items from this database.
* @param musicStore Required to transform database songs into actual song instances
*/
fun readQueue(musicStore: MusicStore): MutableList<Song> {
assertBackgroundThread()
val queue = mutableListOf<Song>()
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
if (cursor.count == 0) return@queryAll
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_HASH)
val albumIndex = cursor.getColumnIndexOrThrow(QueueColumns.ALBUM_HASH)
while (cursor.moveToNext()) {
musicStore.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))
?.let { song ->
queue.add(song)
}
}
}
logD("Successfully read queue of ${queue.size} songs")
return queue
}
/** /**
* Write a queue to the database. * Write a queue to the database.
*/ */
@ -190,12 +220,11 @@ class PlaybackStateDatabase(context: Context) :
assertBackgroundThread() assertBackgroundThread()
val database = writableDatabase val database = writableDatabase
database.transaction { database.transaction {
delete(TABLE_NAME_QUEUE, null, null) delete(TABLE_NAME_QUEUE, null, null)
} }
logD("Wiped queue db.") logD("Wiped queue db")
writeQueueBatch(queue, queue.size) writeQueueBatch(queue, queue.size)
} }
@ -232,32 +261,6 @@ class PlaybackStateDatabase(context: Context) :
} }
} }
/**
* Read a list of queue items from this database.
* @param musicStore Required to transform database songs into actual song instances
*/
fun readQueue(musicStore: MusicStore): MutableList<Song> {
assertBackgroundThread()
val queue = mutableListOf<Song>()
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
if (cursor.count == 0) return@queryAll
val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_HASH)
val albumIndex = cursor.getColumnIndexOrThrow(QueueColumns.ALBUM_HASH)
while (cursor.moveToNext()) {
musicStore.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))
?.let { song ->
queue.add(song)
}
}
}
return queue
}
data class SavedState( data class SavedState(
val song: Song?, val song: Song?,
val position: Long, val position: Long,

View file

@ -224,7 +224,6 @@ class PlaybackStateManager private constructor() {
private fun updatePlayback(song: Song, shouldPlay: Boolean = true) { private fun updatePlayback(song: Song, shouldPlay: Boolean = true) {
mSong = song mSong = song
mPosition = 0 mPosition = 0
setPlaying(shouldPlay) setPlaying(shouldPlay)
} }
@ -271,18 +270,14 @@ class PlaybackStateManager private constructor() {
* Remove a queue item at [index]. Will ignore invalid indexes. * Remove a queue item at [index]. Will ignore invalid indexes.
*/ */
fun removeQueueItem(index: Int): Boolean { fun removeQueueItem(index: Int): Boolean {
logD("Removing item ${mQueue[index].name}.")
if (index > mQueue.size || index < 0) { if (index > mQueue.size || index < 0) {
logE("Index is out of bounds, did not remove queue item.") logE("Index is out of bounds, did not remove queue item")
return false return false
} }
logD("Removing item ${mQueue[index].name}")
mQueue.removeAt(index) mQueue.removeAt(index)
pushQueueUpdate() pushQueueUpdate()
return true return true
} }
@ -292,15 +287,12 @@ class PlaybackStateManager private constructor() {
fun moveQueueItems(from: Int, to: Int): Boolean { fun moveQueueItems(from: Int, to: Int): Boolean {
if (from > mQueue.size || from < 0 || to > mQueue.size || to < 0) { if (from > mQueue.size || from < 0 || to > mQueue.size || to < 0) {
logE("Indices were out of bounds, did not move queue item") logE("Indices were out of bounds, did not move queue item")
return false return false
} }
val item = mQueue.removeAt(from) logD("Moving item $from to position $to")
mQueue.add(to, item) mQueue.add(to, mQueue.removeAt(from))
pushQueueUpdate() pushQueueUpdate()
return true return true
} }
@ -501,7 +493,7 @@ class PlaybackStateManager private constructor() {
* @param context [Context] required * @param context [Context] required
*/ */
suspend fun saveStateToDatabase(context: Context) { suspend fun saveStateToDatabase(context: Context) {
logD("Saving state to DB.") logD("Saving state to DB")
// Pack the entire state and save it to the database. // Pack the entire state and save it to the database.
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -519,7 +511,7 @@ class PlaybackStateManager private constructor() {
database.writeQueue(mQueue) database.writeQueue(mQueue)
this@PlaybackStateManager.logD( this@PlaybackStateManager.logD(
"Save finished in ${System.currentTimeMillis() - start}ms" "State save completed successfully in ${System.currentTimeMillis() - start}ms"
) )
} }
} }
@ -529,10 +521,9 @@ class PlaybackStateManager private constructor() {
* @param context [Context] required. * @param context [Context] required.
*/ */
suspend fun restoreFromDatabase(context: Context) { suspend fun restoreFromDatabase(context: Context) {
logD("Getting state from DB.") logD("Getting state from DB")
val musicStore = MusicStore.maybeGetInstance() ?: return val musicStore = MusicStore.maybeGetInstance() ?: return
val start: Long val start: Long
val playbackState: PlaybackStateDatabase.SavedState? val playbackState: PlaybackStateDatabase.SavedState?
val queue: MutableList<Song> val queue: MutableList<Song>
@ -549,15 +540,13 @@ class PlaybackStateManager private constructor() {
// Get off the IO coroutine since it will cause LiveData updates to throw an exception // Get off the IO coroutine since it will cause LiveData updates to throw an exception
if (playbackState != null) { if (playbackState != null) {
logD("Found playback state $playbackState")
unpackFromPlaybackState(playbackState) unpackFromPlaybackState(playbackState)
unpackQueue(queue) unpackQueue(queue)
doParentSanityCheck() doParentSanityCheck()
doIndexSanityCheck() doIndexSanityCheck()
} }
logD("Restore finished in ${System.currentTimeMillis() - start}ms") logD("State load completed successfully in ${System.currentTimeMillis() - start}ms")
markRestored() markRestored()
} }
@ -592,7 +581,7 @@ class PlaybackStateManager private constructor() {
private fun doParentSanityCheck() { private fun doParentSanityCheck() {
// Check if the parent was lost while in the DB. // Check if the parent was lost while in the DB.
if (mSong != null && mParent == null && mPlaybackMode != PlaybackMode.ALL_SONGS) { if (mSong != null && mParent == null && mPlaybackMode != PlaybackMode.ALL_SONGS) {
logD("Parent lost, attempting restore.") logD("Parent lost, attempting restore")
mParent = when (mPlaybackMode) { mParent = when (mPlaybackMode) {
PlaybackMode.IN_ALBUM -> mQueue.firstOrNull()?.album PlaybackMode.IN_ALBUM -> mQueue.firstOrNull()?.album

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import kotlin.math.pow import kotlin.math.pow
/** /**
@ -85,6 +86,7 @@ class AudioReactor(
* Request the android system for audio focus * Request the android system for audio focus
*/ */
fun requestFocus() { fun requestFocus() {
logD("Requesting audio focus")
AudioManagerCompat.requestAudioFocus(audioManager, request) AudioManagerCompat.requestAudioFocus(audioManager, request)
} }
@ -94,7 +96,7 @@ class AudioReactor(
*/ */
fun applyReplayGain(metadata: Metadata?) { fun applyReplayGain(metadata: Metadata?) {
if (metadata == null) { if (metadata == null) {
logD("No metadata.") logW("No metadata could be extracted from this track")
volume = 1f volume = 1f
return return
} }
@ -102,7 +104,7 @@ class AudioReactor(
// ReplayGain is configurable, so determine what to do based off of the mode. // ReplayGain is configurable, so determine what to do based off of the mode.
val useAlbumGain: (Gain) -> Boolean = when (settingsManager.replayGainMode) { val useAlbumGain: (Gain) -> Boolean = when (settingsManager.replayGainMode) {
ReplayGainMode.OFF -> { ReplayGainMode.OFF -> {
logD("ReplayGain is off.") logD("ReplayGain is off")
volume = 1f volume = 1f
return return
} }
@ -132,10 +134,10 @@ class AudioReactor(
val adjust = if (gain != null) { val adjust = if (gain != null) {
if (useAlbumGain(gain)) { if (useAlbumGain(gain)) {
logD("Using album gain.") logD("Using album gain")
gain.album gain.album
} else { } else {
logD("Using track gain.") logD("Using track gain")
gain.track gain.track
} }
} else { } else {

View file

@ -5,6 +5,7 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import org.oxycblt.auxio.util.logD
/** /**
* Some apps like to party like it's 2011 and just blindly query for the ACTION_MEDIA_BUTTON * Some apps like to party like it's 2011 and just blindly query for the ACTION_MEDIA_BUTTON
@ -20,6 +21,7 @@ import androidx.core.content.ContextCompat
class MediaButtonReceiver : BroadcastReceiver() { class MediaButtonReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_MEDIA_BUTTON) { if (intent.action == Intent.ACTION_MEDIA_BUTTON) {
logD("Received external media button intent")
intent.component = ComponentName(context, PlaybackService::class.java) intent.component = ComponentName(context, PlaybackService::class.java)
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }

View file

@ -180,7 +180,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
settingsManager.addCallback(this) settingsManager.addCallback(this)
logD("Service created.") logD("Service created")
} }
override fun onDestroy() { override fun onDestroy() {
@ -207,7 +207,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
serviceJob.cancel() serviceJob.cancel()
} }
logD("Service destroyed.") logD("Service destroyed")
} }
// --- PLAYER EVENT LISTENER OVERRIDES --- // --- PLAYER EVENT LISTENER OVERRIDES ---
@ -260,22 +260,21 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
override fun onSongUpdate(song: Song?) { override fun onSongUpdate(song: Song?) {
if (song != null) { if (song != null) {
logD("Setting player to ${song.name}")
player.setMediaItem(MediaItem.fromUri(song.uri)) player.setMediaItem(MediaItem.fromUri(song.uri))
player.prepare() player.prepare()
notification.setMetadata(song, ::startForegroundOrNotify) notification.setMetadata(song, ::startForegroundOrNotify)
return return
} }
// Clear if there's nothing to play. // Clear if there's nothing to play.
logD("Nothing playing, stopping playback")
player.stop() player.stop()
stopForegroundAndNotification() stopForegroundAndNotification()
} }
override fun onParentUpdate(parent: MusicParent?) { override fun onParentUpdate(parent: MusicParent?) {
notification.setParent(parent) notification.setParent(parent)
startForegroundOrNotify() startForegroundOrNotify()
} }
@ -295,7 +294,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
override fun onLoopUpdate(loopMode: LoopMode) { override fun onLoopUpdate(loopMode: LoopMode) {
if (!settingsManager.useAltNotifAction) { if (!settingsManager.useAltNotifAction) {
notification.setLoop(loopMode) notification.setLoop(loopMode)
startForegroundOrNotify() startForegroundOrNotify()
} }
} }
@ -303,7 +301,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
override fun onShuffleUpdate(isShuffling: Boolean) { override fun onShuffleUpdate(isShuffling: Boolean) {
if (settingsManager.useAltNotifAction) { if (settingsManager.useAltNotifAction) {
notification.setShuffle(isShuffling) notification.setShuffle(isShuffling)
startForegroundOrNotify() startForegroundOrNotify()
} }
} }
@ -334,7 +331,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
override fun onShowCoverUpdate(showCovers: Boolean) { override fun onShowCoverUpdate(showCovers: Boolean) {
playbackManager.song?.let { song -> playbackManager.song?.let { song ->
connector.onSongUpdate(song) connector.onSongUpdate(song)
notification.setMetadata(song, ::startForegroundOrNotify) notification.setMetadata(song, ::startForegroundOrNotify)
} }
} }
@ -449,6 +445,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
/** /**
* A [BroadcastReceiver] for receiving general playback events from the system. * A [BroadcastReceiver] for receiving general playback events from the system.
* TODO: Don't fire when the service initially starts?
*/ */
private inner class PlaybackReceiver : BroadcastReceiver() { private inner class PlaybackReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
@ -501,7 +498,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
*/ */
private fun resumeFromPlug() { private fun resumeFromPlug() {
if (playbackManager.song != null && settingsManager.doPlugMgt) { if (playbackManager.song != null && settingsManager.doPlugMgt) {
logD("Device connected, resuming...") logD("Device connected, resuming")
playbackManager.setPlaying(true) playbackManager.setPlaying(true)
} }
} }
@ -511,7 +508,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
*/ */
private fun pauseFromPlug() { private fun pauseFromPlug() {
if (playbackManager.song != null && settingsManager.doPlugMgt) { if (playbackManager.song != null && settingsManager.doPlugMgt) {
logD("Device disconnected, pausing...") logD("Device disconnected, pausing")
playbackManager.setPlaying(false) playbackManager.setPlaying(false)
} }
} }

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.logD
/** /**
* Nightmarish class that coordinates communication between [MediaSessionCompat], [Player], * Nightmarish class that coordinates communication between [MediaSessionCompat], [Player],
@ -158,6 +159,8 @@ class PlaybackSessionConnector(
// --- MISC --- // --- MISC ---
private fun invalidateSessionState() { private fun invalidateSessionState() {
logD("Updating media session state")
// Position updates arrive faster when you upload STATE_PAUSED for some insane reason. // Position updates arrive faster when you upload STATE_PAUSED for some insane reason.
val state = PlaybackStateCompat.Builder() val state = PlaybackStateCompat.Builder()
.setActions(ACTIONS) .setActions(ACTIONS)

View file

@ -52,7 +52,6 @@ class SearchAdapter(
is Album -> AlbumViewHolder.ITEM_TYPE is Album -> AlbumViewHolder.ITEM_TYPE
is Song -> SongViewHolder.ITEM_TYPE is Song -> SongViewHolder.ITEM_TYPE
is Header -> HeaderViewHolder.ITEM_TYPE is Header -> HeaderViewHolder.ITEM_TYPE
else -> -1 else -> -1
} }
} }
@ -77,7 +76,7 @@ class SearchAdapter(
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
else -> error("Invalid ViewHolder item type.") else -> error("Invalid ViewHolder item type")
} }
} }

View file

@ -114,7 +114,6 @@ class SearchFragment : Fragment() {
if (!launchedKeyboard) { if (!launchedKeyboard) {
// Auto-open the keyboard when this view is shown // Auto-open the keyboard when this view is shown
requestFocus() requestFocus()
postDelayed(200) { postDelayed(200) {
imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
} }
@ -162,7 +161,7 @@ class SearchFragment : Fragment() {
imm.hide() imm.hide()
} }
logD("Fragment created.") logD("Fragment created")
return binding.root return binding.root
} }

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.logD
import java.text.Normalizer import java.text.Normalizer
/** /**
@ -70,11 +71,14 @@ class SearchViewModel : ViewModel() {
mLastQuery = query mLastQuery = query
if (query.isEmpty() || musicStore == null) { if (query.isEmpty() || musicStore == null) {
logD("No music/query, ignoring search")
mSearchResults.value = listOf() mSearchResults.value = listOf()
return return
} }
// Searching can be quite expensive, so hop on a co-routine logD("Performing search for $query")
// Searching can be quite expensive, so get on a co-routine
viewModelScope.launch { viewModelScope.launch {
val sort = Sort.ByName(true) val sort = Sort.ByName(true)
val results = mutableListOf<BaseModel>() val results = mutableListOf<BaseModel>()
@ -127,6 +131,8 @@ class SearchViewModel : ViewModel() {
else -> null else -> null
} }
logD("Updating filter mode to $mFilterMode")
settingsManager.searchFilterMode = mFilterMode settingsManager.searchFilterMode = mFilterMode
search(mLastQuery) search(mLastQuery)

View file

@ -74,7 +74,7 @@ class AboutFragment : Fragment() {
) )
} }
logD("Dialog created.") logD("Dialog created")
return binding.root return binding.root
} }
@ -83,6 +83,8 @@ class AboutFragment : Fragment() {
* Go through the process of opening a [link] in a browser. * Go through the process of opening a [link] in a browser.
*/ */
private fun openLinkInBrowser(link: String) { private fun openLinkInBrowser(link: String) {
logD("Opening $link")
val browserIntent = Intent(Intent.ACTION_VIEW, link.toUri()).setFlags( val browserIntent = Intent(Intent.ACTION_VIEW, link.toUri()).setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK Intent.FLAG_ACTIVITY_NEW_TASK
) )

View file

@ -22,8 +22,7 @@ import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import org.oxycblt.auxio.accent.Accent import org.oxycblt.auxio.accent.Accent
// A couple of utils for migrating from old settings values to the new // A couple of utils for migrating from old settings values to the new formats
// formats used in 1.3.2 & 1.4.0
fun handleAccentCompat(prefs: SharedPreferences): Accent { fun handleAccentCompat(prefs: SharedPreferences): Accent {
if (prefs.contains(OldKeys.KEY_ACCENT2)) { if (prefs.contains(OldKeys.KEY_ACCENT2)) {

View file

@ -31,7 +31,7 @@ import androidx.preference.children
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.Coil import coil.Coil
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.accent.AccentDialog import org.oxycblt.auxio.accent.AccentCustomizeDialog
import org.oxycblt.auxio.excluded.ExcludedDialog import org.oxycblt.auxio.excluded.ExcludedDialog
import org.oxycblt.auxio.home.tabs.TabCustomizeDialog import org.oxycblt.auxio.home.tabs.TabCustomizeDialog
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
@ -68,7 +68,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
} }
} }
logD("Fragment created.") logD("Fragment created")
} }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -119,7 +119,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
SettingsManager.KEY_ACCENT -> { SettingsManager.KEY_ACCENT -> {
onPreferenceClickListener = Preference.OnPreferenceClickListener { onPreferenceClickListener = Preference.OnPreferenceClickListener {
AccentDialog().show(childFragmentManager, AccentDialog.TAG) AccentCustomizeDialog().show(childFragmentManager, AccentCustomizeDialog.TAG)
true true
} }
@ -182,7 +182,6 @@ class SettingsListFragment : PreferenceFragmentCompat() {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> R.drawable.ic_auto AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> R.drawable.ic_auto
AppCompatDelegate.MODE_NIGHT_NO -> R.drawable.ic_day AppCompatDelegate.MODE_NIGHT_NO -> R.drawable.ic_day
AppCompatDelegate.MODE_NIGHT_YES -> R.drawable.ic_night AppCompatDelegate.MODE_NIGHT_YES -> R.drawable.ic_night
else -> R.drawable.ic_auto else -> R.drawable.ic_auto
} }
} }

View file

@ -331,7 +331,7 @@ class SettingsManager private constructor(context: Context) :
return instance return instance
} }
error("SettingsManager must be initialized with init() before getting its instance.") error("SettingsManager must be initialized with init() before getting its instance")
} }
} }
} }

View file

@ -29,7 +29,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
/** /**
@ -51,7 +51,6 @@ open class EdgeAppBarLayout @JvmOverloads constructor(
if (child != null) { if (child != null) {
val coordinator = parent as CoordinatorLayout val coordinator = parent as CoordinatorLayout
(layoutParams as CoordinatorLayout.LayoutParams).behavior?.onNestedPreScroll( (layoutParams as CoordinatorLayout.LayoutParams).behavior?.onNestedPreScroll(
coordinator, this, coordinator, 0, 0, tConsumed, 0 coordinator, this, coordinator, 0, 0, tConsumed, 0
) )
@ -66,15 +65,12 @@ open class EdgeAppBarLayout @JvmOverloads constructor(
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets { override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
super.onApplyWindowInsets(insets) super.onApplyWindowInsets(insets)
updatePadding(top = insets.systemBarInsetsCompat.top) updatePadding(top = insets.systemBarInsetsCompat.top)
return insets return insets
} }
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
super.onDetachedFromWindow() super.onDetachedFromWindow()
viewTreeObserver.removeOnPreDrawListener(onPreDraw) viewTreeObserver.removeOnPreDrawListener(onPreDraw)
} }
@ -94,9 +90,10 @@ open class EdgeAppBarLayout @JvmOverloads constructor(
if (liftOnScrollTargetViewId != ResourcesCompat.ID_NULL) { if (liftOnScrollTargetViewId != ResourcesCompat.ID_NULL) {
scrollingChild = (parent as ViewGroup).findViewById(liftOnScrollTargetViewId) scrollingChild = (parent as ViewGroup).findViewById(liftOnScrollTargetViewId)
} else { } else {
logE("liftOnScrollTargetViewId was not specified. ignoring scroll events.") logW("liftOnScrollTargetViewId was not specified. ignoring scroll events")
} }
} }
return scrollingChild return scrollingChild
} }
} }

View file

@ -73,7 +73,7 @@ class MemberBinder<T : ViewDataBinding>(
val lifecycle = fragment.viewLifecycleOwner.lifecycle val lifecycle = fragment.viewLifecycleOwner.lifecycle
check(lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { check(lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
"Fragment views are destroyed." "Fragment views are destroyed"
} }
// Otherwise create the binding and return that. // Otherwise create the binding and return that.

View file

@ -39,7 +39,6 @@ import androidx.annotation.PluralsRes
import androidx.annotation.Px import androidx.annotation.Px
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.MainActivity import org.oxycblt.auxio.MainActivity
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -190,16 +189,9 @@ fun Context.pxOfDp(@Dimension dp: Float): Int {
} }
private fun <T> Context.handleResourceFailure(e: Exception, what: String, default: T): T { private fun <T> Context.handleResourceFailure(e: Exception, what: String, default: T): T {
logE("$what load failed.") logE("$what load failed")
e.logTraceOrThrow()
if (BuildConfig.DEBUG) { return default
// I'd rather be aware of a sudden crash when debugging.
throw e
} else {
// Not so much when the app is in production.
logE(e.stackTraceToString())
return default
}
} }
/** /**

View file

@ -34,7 +34,7 @@ fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
*/ */
fun assertBackgroundThread() { fun assertBackgroundThread() {
check(Looper.myLooper() != Looper.getMainLooper()) { check(Looper.myLooper() != Looper.getMainLooper()) {
"This operation must be ran on a background thread." "This operation must be ran on a background thread"
} }
} }

View file

@ -41,6 +41,13 @@ fun Any.logD(msg: String) {
} }
} }
/**
* Shortcut method for logging [msg] as a warning to the console. Handles anonymous objects
*/
fun Any.logW(msg: String) {
Log.w(getName(), msg)
}
/** /**
* Shortcut method for logging [msg] as an error to the console. Handles anonymous objects * Shortcut method for logging [msg] as an error to the console. Handles anonymous objects
*/ */
@ -48,6 +55,18 @@ fun Any.logE(msg: String) {
Log.e(getName(), msg) Log.e(getName(), msg)
} }
/**
* Logs an error in production while still throwing it in debug mode. This is useful for
* non-showstopper bugs that I would still prefer to be caught in debug mode.
*/
fun Throwable.logTraceOrThrow() {
if (BuildConfig.DEBUG) {
throw this
} else {
logE(stackTraceToString())
}
}
/** /**
* Get a non-nullable name, used so that logs will always show up by Auxio * Get a non-nullable name, used so that logs will always show up by Auxio
* @return The name of the object, otherwise "Anonymous Object" * @return The name of the object, otherwise "Anonymous Object"

View file

@ -69,6 +69,7 @@ fun RecyclerView.canScroll(): Boolean = computeVerticalScrollRange() > height
*/ */
fun View.disableDropShadowCompat() { fun View.disableDropShadowCompat() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
logD("Disabling drop shadows")
val transparent = context.getColorSafe(android.R.color.transparent) val transparent = context.getColorSafe(android.R.color.transparent)
outlineAmbientShadowColor = transparent outlineAmbientShadowColor = transparent
outlineSpotShadowColor = transparent outlineSpotShadowColor = transparent

View file

@ -23,6 +23,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.logD
/** /**
* A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the * A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the
@ -53,6 +54,8 @@ class WidgetController(private val context: Context) :
* Release this instance, removing the callbacks and resetting all widgets * Release this instance, removing the callbacks and resetting all widgets
*/ */
fun release() { fun release() {
logD("Releasing instance")
widget.reset(context) widget.reset(context)
playbackManager.removeCallback(this) playbackManager.removeCallback(this)
settingsManager.removeCallback(this) settingsManager.removeCallback(this)

View file

@ -40,6 +40,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.getDimenSizeSafe import org.oxycblt.auxio.util.getDimenSizeSafe
import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.isLandscape
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import kotlin.math.min import kotlin.math.min
/** /**
@ -87,6 +88,10 @@ class WidgetProvider : AppWidgetProvider() {
} }
} }
/**
* Custom function for loading bitmaps to the widget in a way that works with the
* widget ImageView instances.
*/
private fun loadWidgetBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) { private fun loadWidgetBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
val coverRequest = ImageRequest.Builder(context) val coverRequest = ImageRequest.Builder(context)
.data(song.album) .data(song.album)
@ -152,6 +157,8 @@ class WidgetProvider : AppWidgetProvider() {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
logD("Requesting new view from PlaybackService")
// We can't resize the widget until we can generate the views, so request an update // We can't resize the widget until we can generate the views, so request an update
// from PlaybackService. // from PlaybackService.
requestUpdate(context) requestUpdate(context)
@ -234,7 +241,7 @@ class WidgetProvider : AppWidgetProvider() {
continue continue
} else { } else {
// Default to the smallest view if no layout fits // Default to the smallest view if no layout fits
logD("No widget layout found") logW("No good widget layout found")
val minimum = requireNotNull( val minimum = requireNotNull(
views.minByOrNull { it.key.width * it.key.height }?.value views.minByOrNull { it.key.width * it.key.height }?.value

View file

@ -52,7 +52,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_small" android:layout_marginStart="@dimen/spacing_small"
android:ellipsize="end" android:ellipsize="end"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}" app:songInfo="@{song}"
app:layout_constraintBottom_toBottomOf="@+id/playback_cover" app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
app:layout_constraintEnd_toEndOf="@+id/playback_song" app:layout_constraintEnd_toEndOf="@+id/playback_song"
app:layout_constraintStart_toEndOf="@+id/playback_cover" app:layout_constraintStart_toEndOf="@+id/playback_cover"

View file

@ -50,7 +50,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_small" android:layout_marginStart="@dimen/spacing_small"
android:ellipsize="end" android:ellipsize="end"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}" app:songInfo="@{song}"
app:layout_constraintBottom_toBottomOf="@+id/playback_cover" app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
app:layout_constraintEnd_toEndOf="@+id/playback_song" app:layout_constraintEnd_toEndOf="@+id/playback_song"
app:layout_constraintStart_toEndOf="@+id/playback_cover" app:layout_constraintStart_toEndOf="@+id/playback_cover"

View file

@ -12,7 +12,7 @@
android:paddingTop="@dimen/spacing_medium" android:paddingTop="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium" android:paddingEnd="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_small" android:paddingBottom="@dimen/spacing_small"
app:layoutManager="org.oxycblt.auxio.accent.AutoGridLayoutManager" app:layoutManager="org.oxycblt.auxio.accent.AccentGridLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/accent_cancel" app:layout_constraintBottom_toTopOf="@+id/accent_cancel"
app:layout_constraintTop_toBottomOf="@+id/accent_header" app:layout_constraintTop_toBottomOf="@+id/accent_header"
tools:itemCount="18" tools:itemCount="18"

View file

@ -41,7 +41,7 @@
style="@style/Widget.Auxio.TextView.Item.Secondary" style="@style/Widget.Auxio.TextView.Item.Secondary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@{@string/fmt_two(album.resolvedArtistName, @plurals/fmt_song_count(album.songs.size, album.songs.size))}" app:albumInfo="@{album}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_cover" app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -41,7 +41,7 @@
style="@style/Widget.Auxio.TextView.Item.Secondary" style="@style/Widget.Auxio.TextView.Item.Secondary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:artistCounts="@{artist}" app:artistInfo="@{artist}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/artist_image" app:layout_constraintStart_toEndOf="@+id/artist_image"

View file

@ -41,7 +41,7 @@
style="@style/Widget.Auxio.TextView.Item.Secondary" style="@style/Widget.Auxio.TextView.Item.Secondary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@{@plurals/fmt_song_count(genre.songs.size(), genre.songs.size())}" app:genreInfo="@{genre}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/genre_image" app:layout_constraintStart_toEndOf="@+id/genre_image"

View file

@ -44,7 +44,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_medium" android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}" app:songInfo="@{song}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/song_duration" app:layout_constraintEnd_toStartOf="@+id/song_duration"
app:layout_constraintStart_toEndOf="@+id/album_cover" app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -69,7 +69,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_medium" android:layout_marginEnd="@dimen/spacing_medium"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}" app:songInfo="@{song}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/song_drag_handle" app:layout_constraintEnd_toStartOf="@+id/song_drag_handle"
app:layout_constraintStart_toEndOf="@+id/album_cover" app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -42,7 +42,7 @@
style="@style/Widget.Auxio.TextView.Item.Secondary" style="@style/Widget.Auxio.TextView.Item.Secondary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}" app:songInfo="@{song}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_cover" app:layout_constraintStart_toEndOf="@+id/album_cover"

View file

@ -51,7 +51,7 @@
android:layout_marginStart="@dimen/spacing_small" android:layout_marginStart="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_small" android:layout_marginEnd="@dimen/spacing_small"
android:ellipsize="end" android:ellipsize="end"
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}" app:songInfo="@{song}"
app:layout_constraintBottom_toBottomOf="@+id/playback_cover" app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause" app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
app:layout_constraintStart_toEndOf="@+id/playback_cover" app:layout_constraintStart_toEndOf="@+id/playback_cover"