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:
parent
e1dbe6c40c
commit
3aaa2ab0e0
69 changed files with 436 additions and 339 deletions
|
@ -29,7 +29,6 @@ import androidx.appcompat.app.AppCompatDelegate
|
|||
import androidx.core.view.updatePadding
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.oxycblt.auxio.accent.Accent
|
||||
import org.oxycblt.auxio.databinding.ActivityMainBinding
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.playback.system.PlaybackService
|
||||
|
@ -56,7 +55,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
applyEdgeToEdgeWindow(binding)
|
||||
|
||||
logD("Activity created.")
|
||||
logD("Activity created")
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -94,26 +93,29 @@ class MainActivity : AppCompatActivity() {
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Android 12, let dynamic colors be our accent and only enable the black theme option
|
||||
if (isNight && settingsManager.useBlackTheme) {
|
||||
logD("Applying black theme [dynamic colors]")
|
||||
setTheme(R.style.Theme_Auxio_Black)
|
||||
}
|
||||
} else {
|
||||
// Below android 12, load the accent and enable theme customization
|
||||
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
|
||||
// be modified at runtime.
|
||||
if (isNight && settingsManager.useBlackTheme) {
|
||||
setTheme(newAccent.blackTheme)
|
||||
logD("Applying black theme [with accent $accent]")
|
||||
setTheme(accent.blackTheme)
|
||||
} else {
|
||||
setTheme(newAccent.theme)
|
||||
logD("Applying normal theme [with accent $accent]")
|
||||
setTheme(accent.theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyEdgeToEdgeWindow(binding: ViewBinding) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
logD("Doing R+ edge-to-edge.")
|
||||
logD("Doing R+ edge-to-edge")
|
||||
|
||||
window?.setDecorFitsSystemWindows(false)
|
||||
|
||||
|
@ -136,7 +138,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
} else {
|
||||
// Do old edge-to-edge otherwise.
|
||||
logD("Doing legacy edge-to-edge.")
|
||||
logD("Doing legacy edge-to-edge")
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
binding.root.apply {
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.oxycblt.auxio.music.MusicStore
|
|||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
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
|
||||
|
@ -110,7 +111,7 @@ class MainFragment : Fragment() {
|
|||
|
||||
// Error, show the error to the user
|
||||
is MusicStore.Response.Err -> {
|
||||
logD("Received Error")
|
||||
logW("Received Error")
|
||||
|
||||
val errorRes = when (response.kind) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
* 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 theme The 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 blackTheme: Int get() = ACCENT_BLACK_THEMES[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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,12 +77,10 @@ class AccentAdapter(
|
|||
val context = binding.accent.context
|
||||
|
||||
binding.accent.isEnabled = !isSelected
|
||||
|
||||
binding.accent.imageTintList = if (isSelected) {
|
||||
// Switch out the currently selected ViewHolder with this one.
|
||||
selectedViewHolder?.setSelected(false)
|
||||
selectedViewHolder = this
|
||||
|
||||
context.getAttrColorSafe(R.attr.colorSurface).stateList
|
||||
} else {
|
||||
context.getColorSafe(android.R.color.transparent).stateList
|
||||
|
|
|
@ -34,9 +34,9 @@ import org.oxycblt.auxio.util.logD
|
|||
* Dialog responsible for showing the list of accents to select.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AccentDialog : LifecycleDialog() {
|
||||
class AccentCustomizeDialog : LifecycleDialog() {
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
private var pendingAccent = Accent.get()
|
||||
private var pendingAccent = settingsManager.accent
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -53,18 +53,18 @@ class AccentDialog : LifecycleDialog() {
|
|||
|
||||
binding.accentRecycler.apply {
|
||||
adapter = AccentAdapter(pendingAccent) { accent ->
|
||||
logD("Switching selected accent to $accent")
|
||||
pendingAccent = accent
|
||||
}
|
||||
}
|
||||
|
||||
logD("Dialog created.")
|
||||
logD("Dialog created")
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putInt(KEY_PENDING_ACCENT, pendingAccent.index)
|
||||
}
|
||||
|
||||
|
@ -72,9 +72,9 @@ class AccentDialog : LifecycleDialog() {
|
|||
builder.setTitle(R.string.set_accent)
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (pendingAccent != Accent.get()) {
|
||||
if (pendingAccent != settingsManager.accent) {
|
||||
logD("Applying new accent")
|
||||
settingsManager.accent = pendingAccent
|
||||
|
||||
requireActivity().recreate()
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ import kotlin.math.max
|
|||
* of the RecyclerView.
|
||||
* Adapted from this StackOverflow answer: https://stackoverflow.com/a/30256880/14143986
|
||||
*/
|
||||
class AutoGridLayoutManager(
|
||||
class AccentGridLayoutManager(
|
||||
context: Context,
|
||||
attrs: AttributeSet,
|
||||
defStyleAttr: Int,
|
|
@ -27,6 +27,7 @@ import okio.source
|
|||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import android.util.Size as AndroidSize
|
||||
|
@ -55,6 +56,7 @@ abstract class AuxioFetcher : Fetcher {
|
|||
fetchMediaStoreCovers(context, album)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logW("Unable to extract album art due to an error")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +82,6 @@ abstract class AuxioFetcher : Fetcher {
|
|||
// 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.
|
||||
val result = fetchAospMetadataCovers(context, album)
|
||||
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
|
@ -88,7 +89,6 @@ abstract class AuxioFetcher : Fetcher {
|
|||
// Our next fallback is to rely on ExoPlayer's largely half-baked and undocumented
|
||||
// metadata system.
|
||||
val exoResult = fetchExoplayerCover(context, album)
|
||||
|
||||
if (exoResult != null) {
|
||||
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
|
||||
// and we can't do any filesystem traversing due to scoped storage.
|
||||
val mediaStoreResult = fetchMediaStoreCovers(context, album)
|
||||
|
||||
if (mediaStoreResult != null) {
|
||||
return mediaStoreResult
|
||||
}
|
||||
|
@ -192,7 +191,7 @@ abstract class AuxioFetcher : Fetcher {
|
|||
} else if (stream != null) {
|
||||
// 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.
|
||||
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)
|
||||
}
|
||||
|
@ -223,9 +222,10 @@ abstract class AuxioFetcher : Fetcher {
|
|||
val increment = AndroidSize(mosaicSize.width / 2, mosaicSize.height / 2)
|
||||
|
||||
val mosaicBitmap = Bitmap.createBitmap(
|
||||
mosaicSize.width, mosaicSize.height, Bitmap.Config.ARGB_8888
|
||||
mosaicSize.width,
|
||||
mosaicSize.height,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
|
||||
val canvas = Canvas(mosaicBitmap)
|
||||
|
||||
var x = 0
|
||||
|
|
|
@ -79,7 +79,6 @@ class ArtistImageFetcher private constructor(
|
|||
override suspend fun fetch(): FetchResult? {
|
||||
val albums = Sort.ByName(true)
|
||||
.sortAlbums(artist.albums)
|
||||
|
||||
val results = albums.mapAtMost(4) { album ->
|
||||
fetchArt(context, album)
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import org.oxycblt.auxio.ui.ActionMenu
|
|||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.util.canScroll
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
|
||||
/**
|
||||
|
@ -111,10 +112,11 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
// fragment should be launched otherwise.
|
||||
is Song -> {
|
||||
if (detailModel.curAlbum.value!!.id == item.album.id) {
|
||||
logD("Navigating to a song in this album")
|
||||
scrollToItem(item.id, detailAdapter)
|
||||
|
||||
detailModel.finishNavToItem()
|
||||
} else {
|
||||
logD("Navigating to another album")
|
||||
findNavController().navigate(
|
||||
AlbumDetailFragmentDirections.actionShowAlbum(item.album.id)
|
||||
)
|
||||
|
@ -125,9 +127,11 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
// detail fragment.
|
||||
is Album -> {
|
||||
if (detailModel.curAlbum.value!!.id == item.id) {
|
||||
logD("Navigating to the top of this album")
|
||||
binding.detailRecycler.scrollToPosition(0)
|
||||
detailModel.finishNavToItem()
|
||||
} else {
|
||||
logD("Navigating to another album")
|
||||
findNavController().navigate(
|
||||
AlbumDetailFragmentDirections.actionShowAlbum(item.id)
|
||||
)
|
||||
|
@ -136,13 +140,14 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
|
||||
// Always launch a new ArtistDetailFragment.
|
||||
is Artist -> {
|
||||
logD("Navigating to another artist")
|
||||
findNavController().navigate(
|
||||
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
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.oxycblt.auxio.playback.state.PlaybackMode
|
|||
import org.oxycblt.auxio.ui.ActionMenu
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
/**
|
||||
* The [DetailFragment] for an artist.
|
||||
|
@ -98,25 +99,33 @@ class ArtistDetailFragment : DetailFragment() {
|
|||
when (item) {
|
||||
is Artist -> {
|
||||
if (item.id == detailModel.curArtist.value?.id) {
|
||||
logD("Navigating to the top of this artist")
|
||||
binding.detailRecycler.scrollToPosition(0)
|
||||
detailModel.finishNavToItem()
|
||||
} else {
|
||||
logD("Navigating to another artist")
|
||||
findNavController().navigate(
|
||||
ArtistDetailFragmentDirections.actionShowArtist(item.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is Album -> findNavController().navigate(
|
||||
ArtistDetailFragmentDirections.actionShowAlbum(item.id)
|
||||
)
|
||||
|
||||
is Song -> findNavController().navigate(
|
||||
ArtistDetailFragmentDirections.actionShowAlbum(item.album.id)
|
||||
)
|
||||
|
||||
else -> {
|
||||
is Album -> {
|
||||
logD("Navigating to another album")
|
||||
findNavController().navigate(
|
||||
ArtistDetailFragmentDirections.actionShowAlbum(item.id)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.google.android.material.appbar.AppBarLayout
|
||||
import org.oxycblt.auxio.R
|
||||
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
|
||||
|
@ -39,9 +42,8 @@ class DetailAppBarLayout @JvmOverloads constructor(
|
|||
(layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context)
|
||||
}
|
||||
|
||||
private fun findTitleView(): AppCompatTextView {
|
||||
private fun findTitleView(): AppCompatTextView? {
|
||||
val titleView = mTitleView
|
||||
|
||||
if (titleView != null) {
|
||||
return titleView
|
||||
}
|
||||
|
@ -49,13 +51,18 @@ class DetailAppBarLayout @JvmOverloads constructor(
|
|||
val toolbar = findViewById<Toolbar>(R.id.detail_toolbar)
|
||||
|
||||
// Reflect to get the actual title view to do transformations on
|
||||
val newTitleView = Toolbar::class.java.getDeclaredField("mTitleTextView").run {
|
||||
isAccessible = true
|
||||
get(toolbar) as AppCompatTextView
|
||||
val newTitleView = try {
|
||||
Toolbar::class.java.getDeclaredField("mTitleTextView").run {
|
||||
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
|
||||
|
||||
mTitleView = newTitleView
|
||||
return newTitleView
|
||||
}
|
||||
|
@ -95,11 +102,11 @@ class DetailAppBarLayout @JvmOverloads constructor(
|
|||
to = 0f
|
||||
}
|
||||
|
||||
if (titleView.alpha == to) return
|
||||
if (titleView?.alpha == to) return
|
||||
|
||||
mTitleAnimator = ValueAnimator.ofFloat(from, to).apply {
|
||||
addUpdateListener {
|
||||
titleView.alpha = it.animatedValue as Float
|
||||
titleView?.alpha = it.animatedValue as Float
|
||||
}
|
||||
|
||||
duration = resources.getInteger(R.integer.app_bar_elevation_anim_duration).toLong()
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.oxycblt.auxio.music.MusicParent
|
|||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.memberBinding
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* A Base [Fragment] implementing the base features shared across all detail fragments.
|
||||
|
@ -43,13 +44,11 @@ abstract class DetailFragment : Fragment() {
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
detailModel.setNavigating(false)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
// Cancel all pending menus when this fragment stops to prevent bugs/crashes
|
||||
detailModel.finishShowMenu(null)
|
||||
}
|
||||
|
@ -94,7 +93,6 @@ abstract class DetailFragment : Fragment() {
|
|||
binding.detailRecycler.apply {
|
||||
adapter = detailAdapter
|
||||
setHasFixedSize(true)
|
||||
|
||||
applySpans(gridLookup)
|
||||
}
|
||||
}
|
||||
|
@ -105,6 +103,8 @@ abstract class DetailFragment : Fragment() {
|
|||
* @param showItem Which menu items to keep
|
||||
*/
|
||||
protected fun showMenu(config: DetailViewModel.MenuConfig, showItem: ((Int) -> Boolean)? = null) {
|
||||
logD("Launching menu [$config]")
|
||||
|
||||
PopupMenu(config.anchor.context, config.anchor).apply {
|
||||
inflate(R.menu.menu_detail_sort)
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.MusicStore
|
|||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* ViewModel that stores data for the [DetailFragment]s. This includes:
|
||||
|
@ -77,12 +78,10 @@ class DetailViewModel : ViewModel() {
|
|||
private set
|
||||
|
||||
private var currentMenuContext: DisplayMode? = null
|
||||
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
fun setGenre(id: Long) {
|
||||
if (mCurGenre.value?.id == id) return
|
||||
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
mCurGenre.value = musicStore.genres.find { it.id == id }
|
||||
refreshGenreData()
|
||||
|
@ -90,7 +89,6 @@ class DetailViewModel : ViewModel() {
|
|||
|
||||
fun setArtist(id: Long) {
|
||||
if (mCurArtist.value?.id == id) return
|
||||
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
mCurArtist.value = musicStore.artists.find { it.id == id }
|
||||
refreshArtistData()
|
||||
|
@ -98,7 +96,6 @@ class DetailViewModel : ViewModel() {
|
|||
|
||||
fun setAlbum(id: Long) {
|
||||
if (mCurAlbum.value?.id == id) return
|
||||
|
||||
val musicStore = MusicStore.requireInstance()
|
||||
mCurAlbum.value = musicStore.albums.find { it.id == id }
|
||||
refreshAlbumData()
|
||||
|
@ -112,6 +109,7 @@ class DetailViewModel : ViewModel() {
|
|||
mShowMenu.value = null
|
||||
|
||||
if (newMode != null) {
|
||||
logD("Applying new sort mode")
|
||||
when (currentMenuContext) {
|
||||
DisplayMode.SHOW_ALBUMS -> {
|
||||
settingsManager.detailAlbumSort = newMode
|
||||
|
@ -154,7 +152,9 @@ class DetailViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
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(
|
||||
ActionHeader(
|
||||
|
@ -175,7 +175,8 @@ class DetailViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private fun refreshArtistData() {
|
||||
val artist = curArtist.value!!
|
||||
logD("Refreshing artist data")
|
||||
val artist = requireNotNull(curArtist.value)
|
||||
val data = mutableListOf<BaseModel>(artist)
|
||||
|
||||
data.add(
|
||||
|
@ -206,7 +207,9 @@ class DetailViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
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(
|
||||
ActionHeader(
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.oxycblt.auxio.playback.state.PlaybackMode
|
|||
import org.oxycblt.auxio.ui.ActionMenu
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
/**
|
||||
* The [DetailFragment] for a genre.
|
||||
|
@ -79,20 +80,29 @@ class GenreDetailFragment : DetailFragment() {
|
|||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||
when (item) {
|
||||
// All items will launch new detail fragments.
|
||||
is 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 Artist -> {
|
||||
logD("Navigating to another artist")
|
||||
findNavController().navigate(
|
||||
GenreDetailFragmentDirections.actionShowArtist(item.id)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -58,7 +58,6 @@ class AlbumDetailAdapter(
|
|||
is Album -> ALBUM_DETAIL_ITEM_TYPE
|
||||
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
|
||||
is Song -> ALBUM_SONG_ITEM_TYPE
|
||||
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +85,6 @@ class AlbumDetailAdapter(
|
|||
is Album -> (holder as AlbumDetailViewHolder).bind(item)
|
||||
is Song -> (holder as AlbumSongViewHolder).bind(item)
|
||||
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
||||
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +125,6 @@ class AlbumDetailAdapter(
|
|||
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
||||
recycler.getChildViewHolder(child)?.let {
|
||||
currentHolder = it as Highlightable
|
||||
|
||||
currentHolder?.setHighlighted(true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,12 +33,12 @@ import org.oxycblt.auxio.music.Artist
|
|||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.bindArtistInfo
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.BaseViewHolder
|
||||
import org.oxycblt.auxio.ui.DiffCallback
|
||||
import org.oxycblt.auxio.ui.HeaderViewHolder
|
||||
import org.oxycblt.auxio.util.getPluralSafe
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/**
|
||||
|
@ -64,7 +64,6 @@ class ArtistDetailAdapter(
|
|||
is Song -> ARTIST_SONG_ITEM_TYPE
|
||||
is Header -> HeaderViewHolder.ITEM_TYPE
|
||||
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
|
||||
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
@ -174,7 +173,6 @@ class ArtistDetailAdapter(
|
|||
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
||||
recycler.getChildViewHolder(child)?.let {
|
||||
currentSongHolder = it as Highlightable
|
||||
|
||||
currentSongHolder?.setHighlighted(true)
|
||||
}
|
||||
}
|
||||
|
@ -205,11 +203,7 @@ class ArtistDetailAdapter(
|
|||
.entries.maxByOrNull { it.value.size }
|
||||
?.key ?: context.getString(R.string.def_genre)
|
||||
|
||||
binding.detailInfo.text = context.getString(
|
||||
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.detailInfo.bindArtistInfo(data)
|
||||
|
||||
binding.detailPlayButton.setOnClickListener {
|
||||
playbackModel.playArtist(data, false)
|
||||
|
|
|
@ -30,11 +30,11 @@ import org.oxycblt.auxio.music.ActionHeader
|
|||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.bindGenreInfo
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.ActionHeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.BaseViewHolder
|
||||
import org.oxycblt.auxio.ui.DiffCallback
|
||||
import org.oxycblt.auxio.util.getPluralSafe
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/**
|
||||
|
@ -54,7 +54,6 @@ class GenreDetailAdapter(
|
|||
is Genre -> GENRE_DETAIL_ITEM_TYPE
|
||||
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
|
||||
is Song -> GENRE_SONG_ITEM_TYPE
|
||||
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +120,6 @@ class GenreDetailAdapter(
|
|||
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
||||
recycler.getChildViewHolder(child)?.let {
|
||||
currentHolder = it as Highlightable
|
||||
|
||||
currentHolder?.setHighlighted(true)
|
||||
}
|
||||
}
|
||||
|
@ -143,11 +141,7 @@ class GenreDetailAdapter(
|
|||
}
|
||||
|
||||
binding.detailName.text = data.resolvedName
|
||||
|
||||
binding.detailSubhead.apply {
|
||||
text = context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size)
|
||||
}
|
||||
|
||||
binding.detailSubhead.bindGenreInfo(data)
|
||||
binding.detailInfo.text = data.totalDuration
|
||||
|
||||
binding.detailPlayButton.setOnClickListener {
|
||||
|
|
|
@ -55,7 +55,6 @@ class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
|
|||
|
||||
writableDatabase.transaction {
|
||||
delete(TABLE_NAME, null, null)
|
||||
|
||||
logD("Deleted paths db")
|
||||
|
||||
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()
|
||||
|
||||
val paths = mutableListOf<String>()
|
||||
|
||||
readableDatabase.queryAll(TABLE_NAME) { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
paths.add(cursor.getString(0))
|
||||
}
|
||||
}
|
||||
|
||||
logD("Successfully read ${paths.size} paths from db")
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
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_NAME = "auxio_blacklist_database.db"
|
||||
|
||||
|
|
|
@ -77,13 +77,16 @@ class ExcludedDialog : LifecycleDialog() {
|
|||
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener {
|
||||
logD("Opening launcher")
|
||||
launcher.launch(null)
|
||||
}
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
|
||||
if (excludedModel.isModified) {
|
||||
logD("Committing changes")
|
||||
saveAndRestart()
|
||||
} else {
|
||||
logD("Dropping changes")
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
@ -93,11 +96,10 @@ class ExcludedDialog : LifecycleDialog() {
|
|||
|
||||
excludedModel.paths.observe(viewLifecycleOwner) { paths ->
|
||||
adapter.submitList(paths)
|
||||
|
||||
binding.excludedEmpty.isVisible = paths.isEmpty()
|
||||
}
|
||||
|
||||
logD("Dialog created.")
|
||||
logD("Dialog created")
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
@ -114,6 +116,7 @@ class ExcludedDialog : LifecycleDialog() {
|
|||
private fun addDocTreePath(uri: Uri?) {
|
||||
// A null URI means that the user left the file picker without picking a directory
|
||||
if (uri == null) {
|
||||
logD("No URI given (user closed the dialog)")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -142,6 +145,7 @@ class ExcludedDialog : LifecycleDialog() {
|
|||
return getRootPath() + "/" + typeAndPath.last()
|
||||
}
|
||||
|
||||
logD("Unsupported volume ${typeAndPath[0]}")
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -156,7 +160,6 @@ class ExcludedDialog : LifecycleDialog() {
|
|||
/**
|
||||
* Get *just* the root path, nothing else is really needed.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getRootPath(): String {
|
||||
return Environment.getExternalStorageDirectory().absolutePath
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val start = System.currentTimeMillis()
|
||||
excludedDatabase.writePaths(mPaths.value!!)
|
||||
dbPaths = mPaths.value!!
|
||||
|
||||
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() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val start = System.currentTimeMillis()
|
||||
dbPaths = excludedDatabase.readPaths()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
mPaths.value = dbPaths.toMutableList()
|
||||
}
|
||||
this@ExcludedViewModel.logD(
|
||||
"Path load completed successfully in ${System.currentTimeMillis() - start}ms"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.util.AttributeSet
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import org.oxycblt.auxio.util.getDimenSizeSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import com.google.android.material.R as MaterialR
|
||||
|
||||
/**
|
||||
|
@ -20,7 +21,10 @@ class AdaptiveFloatingActionButton @JvmOverloads constructor(
|
|||
init {
|
||||
size = SIZE_NORMAL
|
||||
|
||||
// Use a large FAB on large screens, as it makes it easier to touch.
|
||||
if (resources.configuration.smallestScreenWidthDp >= 640) {
|
||||
logD("Using large FAB configuration")
|
||||
|
||||
val largeFabSize = context.getDimenSizeSafe(
|
||||
MaterialR.dimen.m3_large_fab_size
|
||||
)
|
||||
|
@ -29,7 +33,6 @@ class AdaptiveFloatingActionButton @JvmOverloads constructor(
|
|||
MaterialR.dimen.m3_large_fab_max_image_size
|
||||
)
|
||||
|
||||
// Use a large FAB on large screens, as it makes it easier to touch.
|
||||
customSize = largeFabSize
|
||||
setMaxImageSize(largeImageSize)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.oxycblt.auxio.home
|
|||
import android.content.Context
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
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.
|
||||
|
@ -20,15 +21,22 @@ class AdaptiveTabStrategy(
|
|||
val tabMode = homeModel.tabs[position]
|
||||
|
||||
when {
|
||||
width < 370 ->
|
||||
width < 370 -> {
|
||||
logD("Using icon-only configuration")
|
||||
tab.setIcon(tabMode.icon)
|
||||
.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)
|
||||
.setText(tabMode.string)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
|||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
import org.oxycblt.auxio.util.logTraceOrThrow
|
||||
|
||||
/**
|
||||
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail
|
||||
|
@ -77,16 +78,19 @@ class HomeFragment : Fragment() {
|
|||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.action_search -> {
|
||||
logD("Navigating to search")
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowSearch())
|
||||
}
|
||||
|
||||
R.id.action_settings -> {
|
||||
logD("Navigating to settings")
|
||||
parentFragment?.parentFragment?.findNavController()?.navigate(
|
||||
MainFragmentDirections.actionShowSettings()
|
||||
)
|
||||
}
|
||||
|
||||
R.id.action_about -> {
|
||||
logD("Navigating to about")
|
||||
parentFragment?.parentFragment?.findNavController()?.navigate(
|
||||
MainFragmentDirections.actionShowAbout()
|
||||
)
|
||||
|
@ -96,20 +100,16 @@ class HomeFragment : Fragment() {
|
|||
|
||||
R.id.option_sort_asc -> {
|
||||
item.isChecked = !item.isChecked
|
||||
|
||||
val new = homeModel.getSortForDisplay(homeModel.curTab.value!!)
|
||||
.ascending(item.isChecked)
|
||||
|
||||
homeModel.updateCurrentSort(new)
|
||||
}
|
||||
|
||||
// Sorting option was selected, mark it as selected and update the mode
|
||||
else -> {
|
||||
item.isChecked = true
|
||||
|
||||
val new = homeModel.getSortForDisplay(homeModel.curTab.value!!)
|
||||
.assignId(item.itemId)
|
||||
|
||||
homeModel.updateCurrentSort(requireNotNull(new))
|
||||
}
|
||||
}
|
||||
|
@ -141,8 +141,8 @@ class HomeFragment : Fragment() {
|
|||
set(recycler, slop * 3) // 3x seems to be the best fit here
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to reduce ViewPager sensitivity")
|
||||
logE(e.stackTraceToString())
|
||||
logE("Unable to reduce ViewPager sensitivity (likely an internal code change)")
|
||||
e.logTraceOrThrow()
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
// 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.
|
||||
else -> binding.homeFab.hide()
|
||||
}
|
||||
|
@ -207,7 +207,7 @@ class HomeFragment : Fragment() {
|
|||
homeModel.curTab.observe(viewLifecycleOwner) { 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.
|
||||
when (tab) {
|
||||
DisplayMode.SHOW_SONGS -> updateSortMenu(sortItem, tab)
|
||||
|
@ -229,8 +229,9 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||
// The AppBarLayout gets confused and collapses when we navigate too fast, wait for it
|
||||
// to draw before we continue.
|
||||
// The AppBarLayout gets confused when we navigate too fast, wait for it to draw
|
||||
// before we navigate.
|
||||
// This is only here just in case a collapsing toolbar is re-added.
|
||||
binding.homeAppbar.post {
|
||||
when (item) {
|
||||
is Song -> findNavController().navigate(
|
||||
|
@ -255,7 +256,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
logD("Fragment Created.")
|
||||
logD("Fragment Created")
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state.
|
||||
|
@ -78,7 +79,6 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
|||
|
||||
viewModelScope.launch {
|
||||
val musicStore = MusicStore.awaitInstance()
|
||||
|
||||
mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs)
|
||||
mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums)
|
||||
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.
|
||||
*/
|
||||
fun updateCurrentTab(pos: Int) {
|
||||
logD("Updating current tab to ${tabs[pos]}")
|
||||
mCurTab.value = tabs[pos]
|
||||
}
|
||||
|
||||
|
@ -110,6 +111,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
|||
* Update the currently displayed item's [Sort].
|
||||
*/
|
||||
fun updateCurrentSort(sort: Sort) {
|
||||
logD("Updating ${mCurTab.value} sort to $sort")
|
||||
when (mCurTab.value) {
|
||||
DisplayMode.SHOW_SONGS -> {
|
||||
settingsManager.libSongSort = sort
|
||||
|
|
|
@ -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.
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.DialogTabsBinding
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
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
|
||||
|
@ -49,7 +50,6 @@ class TabCustomizeDialog : LifecycleDialog() {
|
|||
if (savedInstanceState != null) {
|
||||
// Restore any pending tab configurations
|
||||
val tabs = Tab.fromSequence(savedInstanceState.getInt(KEY_TABS))
|
||||
|
||||
if (tabs != null) {
|
||||
pendingTabs = tabs
|
||||
}
|
||||
|
@ -66,10 +66,9 @@ class TabCustomizeDialog : LifecycleDialog() {
|
|||
// of how ViewHolders are bound], but instead simply look for the mode in
|
||||
// the list of pending tabs and update that instead.
|
||||
val index = pendingTabs.indexOfFirst { it.mode == tab.mode }
|
||||
|
||||
if (index != -1) {
|
||||
val curTab = pendingTabs[index]
|
||||
|
||||
logD("Updating tab $curTab to $tab")
|
||||
pendingTabs[index] = when (curTab) {
|
||||
is Tab.Visible -> Tab.Invisible(curTab.mode)
|
||||
is Tab.Invisible -> Tab.Visible(curTab.mode)
|
||||
|
@ -93,7 +92,6 @@ class TabCustomizeDialog : LifecycleDialog() {
|
|||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putInt(KEY_TABS, Tab.toSequence(pendingTabs))
|
||||
}
|
||||
|
||||
|
@ -101,6 +99,7 @@ class TabCustomizeDialog : LifecycleDialog() {
|
|||
builder.setTitle(R.string.set_lib_tabs)
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
logD("Committing tab changes")
|
||||
settingsManager.libTabs = pendingTabs
|
||||
}
|
||||
|
||||
|
|
|
@ -124,8 +124,12 @@ data class Song(
|
|||
val internalGroupingArtistName: String get() = internalMediaStoreAlbumArtistName
|
||||
?: 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. **/
|
||||
val internalMissingGenre: Boolean get() = mGenre == null
|
||||
val internalIsMissingGenre: Boolean get() = mGenre == null
|
||||
|
||||
/** Internal method. Do not use. */
|
||||
fun internalLinkAlbum(album: Album) {
|
||||
|
@ -180,6 +184,9 @@ data class Album(
|
|||
val resolvedArtistName: String get() =
|
||||
artist.resolvedName
|
||||
|
||||
/** Internal field. Do not use. */
|
||||
val internalIsMissingArtist: Boolean = mArtist != null
|
||||
|
||||
/** Internal method. Do not use. */
|
||||
fun internalLinkArtist(artist: Artist) {
|
||||
mArtist = artist
|
||||
|
|
|
@ -7,8 +7,8 @@ import android.provider.MediaStore
|
|||
import androidx.core.database.getStringOrNull
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.excluded.ExcludedDatabase
|
||||
import org.oxycblt.auxio.util.logD
|
||||
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
|
||||
|
@ -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
|
||||
* 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
|
||||
* 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
|
||||
* own Google Play Music, and of course every Google Play Music user knew how great that turned
|
||||
* out!
|
||||
|
@ -88,14 +88,14 @@ class MusicLoader {
|
|||
val artists = buildArtists(context, albums)
|
||||
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) {
|
||||
try {
|
||||
song.album.artist
|
||||
song.genre
|
||||
} catch (e: Exception) {
|
||||
if (song.internalIsMissingAlbum ||
|
||||
song.internalIsMissingArtist ||
|
||||
song.internalIsMissingGenre
|
||||
) {
|
||||
logE("Found malformed song: ${song.name}")
|
||||
throw e
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,6 +204,8 @@ class MusicLoader {
|
|||
it.internalMediaStoreAlbumArtistName to it.track to it.duration
|
||||
}.toMutableList()
|
||||
|
||||
logD("Successfully loaded ${songs.size} songs")
|
||||
|
||||
return songs
|
||||
}
|
||||
|
||||
|
@ -247,6 +249,8 @@ class MusicLoader {
|
|||
)
|
||||
}
|
||||
|
||||
logD("Successfully built ${albums.size} albums")
|
||||
|
||||
return albums
|
||||
}
|
||||
|
||||
|
@ -264,14 +268,15 @@ class MusicLoader {
|
|||
|
||||
// Album deduplication does not eliminate every case of fragmented artists, do
|
||||
// we deduplicate in the artist creation step as well.
|
||||
// Note that we actually don't do this in groupBy. This is generally because we
|
||||
// only want to default to a lowercase artist name when we have no other choice.
|
||||
// Note that we actually don't do this in groupBy. This is generally because using
|
||||
// a template song may not result in the best possible artist name in all cases.
|
||||
val previousArtistIndex = artists.indexOfFirst { artist ->
|
||||
artist.name.lowercase() == artistName.lowercase()
|
||||
}
|
||||
|
||||
if (previousArtistIndex > -1) {
|
||||
val previousArtist = artists[previousArtistIndex]
|
||||
logD("Merging duplicate artist into pre-existing artist ${previousArtist.name}")
|
||||
artists[previousArtistIndex] = Artist(
|
||||
previousArtist.name,
|
||||
previousArtist.resolvedName,
|
||||
|
@ -288,6 +293,8 @@ class MusicLoader {
|
|||
}
|
||||
}
|
||||
|
||||
logD("Successfully built ${artists.size} artists")
|
||||
|
||||
return artists
|
||||
}
|
||||
|
||||
|
@ -327,7 +334,7 @@ class MusicLoader {
|
|||
}
|
||||
}
|
||||
|
||||
val songsWithoutGenres = songs.filter { it.internalMissingGenre }
|
||||
val songsWithoutGenres = songs.filter { it.internalIsMissingGenre }
|
||||
|
||||
if (songsWithoutGenres.isNotEmpty()) {
|
||||
// Songs that don't have a genre will be thrown into an unknown genre.
|
||||
|
@ -340,6 +347,8 @@ class MusicLoader {
|
|||
genres.add(unknownGenre)
|
||||
}
|
||||
|
||||
logD("Successfully loaded ${genres.size} genres")
|
||||
|
||||
return genres
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ class MusicStore private constructor() {
|
|||
* Load/Sort the entire music library. Should always be ran on a coroutine.
|
||||
*/
|
||||
private fun load(context: Context): Response {
|
||||
logD("Starting initial music load...")
|
||||
logD("Starting initial music load")
|
||||
|
||||
val notGranted = ContextCompat.checkSelfPermission(
|
||||
context, Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
|
@ -76,11 +76,10 @@ class MusicStore private constructor() {
|
|||
mArtists = library.artists
|
||||
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) {
|
||||
logE("Something went horribly wrong.")
|
||||
logE("Something went horribly wrong")
|
||||
logE(e.stackTraceToString())
|
||||
|
||||
return Response.Err(ErrorKind.FAILED)
|
||||
}
|
||||
|
||||
|
@ -117,6 +116,7 @@ class MusicStore private constructor() {
|
|||
/**
|
||||
* A response that [MusicStore] returns when loading music.
|
||||
* And before you ask, yes, I do like rust.
|
||||
* TODO: Replace this with the kotlin builtin
|
||||
*/
|
||||
sealed class Response {
|
||||
class Ok(val musicStore: MusicStore) : Response()
|
||||
|
@ -201,7 +201,7 @@ class MusicStore private constructor() {
|
|||
*/
|
||||
fun requireInstance(): MusicStore {
|
||||
return requireNotNull(maybeGetInstance()) {
|
||||
"Required MusicStore instance was not available."
|
||||
"Required MusicStore instance was not available"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ import androidx.core.text.isDigitsOnly
|
|||
import androidx.databinding.BindingAdapter
|
||||
import org.oxycblt.auxio.R
|
||||
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
|
||||
|
@ -98,6 +100,7 @@ fun String.getGenreNameCompat(): String? {
|
|||
*/
|
||||
fun Long.toDuration(isElapsed: Boolean): String {
|
||||
if (!isElapsed && this == 0L) {
|
||||
logD("Non-elapsed duration is zero, using --:--")
|
||||
return "--:--"
|
||||
}
|
||||
|
||||
|
@ -121,14 +124,53 @@ fun Int.toDate(context: Context): String {
|
|||
|
||||
// --- BINDING ADAPTERS ---
|
||||
|
||||
/**
|
||||
* Bind the album + song counts for an artist
|
||||
*/
|
||||
@BindingAdapter("artistCounts")
|
||||
fun TextView.bindArtistCounts(artist: Artist) {
|
||||
@BindingAdapter("songInfo")
|
||||
fun TextView.bindSongInfo(song: Song?) {
|
||||
if (song == null) {
|
||||
logW("Song was null, not applying info")
|
||||
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(
|
||||
R.string.fmt_counts,
|
||||
context.getPluralSafe(R.plurals.fmt_album_count, artist.albums.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)
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
class MusicViewModel : ViewModel() {
|
||||
private val mLoaderResponse = MutableLiveData<MusicStore.Response?>(null)
|
||||
|
@ -37,6 +38,7 @@ class MusicViewModel : ViewModel() {
|
|||
*/
|
||||
fun loadMusic(context: Context) {
|
||||
if (mLoaderResponse.value != null || isBusy) {
|
||||
logD("Loader is busy/already completed, not reloading")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -45,15 +47,14 @@ class MusicViewModel : ViewModel() {
|
|||
|
||||
viewModelScope.launch {
|
||||
val result = MusicStore.initInstance(context)
|
||||
|
||||
isBusy = false
|
||||
mLoaderResponse.value = result
|
||||
isBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadMusic(context: Context) {
|
||||
logD("Reloading music library")
|
||||
mLoaderResponse.value = null
|
||||
|
||||
loadMusic(context)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,6 @@ class PlaybackFragment : Fragment() {
|
|||
// Make marquee of song title work
|
||||
binding.playbackSong.isSelected = true
|
||||
binding.playbackSeekBar.onConfirmListener = playbackModel::setPosition
|
||||
|
||||
binding.playbackPlayPause.post {
|
||||
binding.playbackPlayPause.stateListAnimator = null
|
||||
}
|
||||
|
@ -101,11 +100,11 @@ class PlaybackFragment : Fragment() {
|
|||
|
||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
||||
if (song != null) {
|
||||
logD("Updating song display to ${song.name}.")
|
||||
logD("Updating song display to ${song.name}")
|
||||
binding.song = song
|
||||
binding.playbackSeekBar.setDuration(song.seconds)
|
||||
} else {
|
||||
logD("No song is being played, leaving.")
|
||||
logD("No song is being played, leaving")
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
|
@ -152,7 +151,7 @@ class PlaybackFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
logD("Fragment Created.")
|
||||
logD("Fragment Created")
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.oxycblt.auxio.util.disableDropShadowCompat
|
|||
import org.oxycblt.auxio.util.getAttrColorSafe
|
||||
import org.oxycblt.auxio.util.getDimenSafe
|
||||
import org.oxycblt.auxio.util.getDrawableSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.pxOfDp
|
||||
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
|
||||
import org.oxycblt.auxio.util.stateList
|
||||
|
@ -225,6 +226,8 @@ class PlaybackLayout @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
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
|
||||
// while we are in one.
|
||||
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
|
||||
// to reflect the presence of the panel [at least in it's collapsed state]
|
||||
playbackContainerView.dispatchApplyWindowInsets(insets)
|
||||
|
||||
lastInsets = insets
|
||||
applyContentWindowInsets()
|
||||
|
||||
return insets
|
||||
}
|
||||
|
||||
|
@ -370,7 +371,6 @@ class PlaybackLayout @JvmOverloads constructor(
|
|||
*/
|
||||
private fun applyContentWindowInsets() {
|
||||
val insets = lastInsets
|
||||
|
||||
if (insets != null) {
|
||||
contentView.dispatchApplyWindowInsets(adjustInsets(insets))
|
||||
}
|
||||
|
@ -386,8 +386,9 @@ class PlaybackLayout @JvmOverloads constructor(
|
|||
val bars = insets.systemBarInsetsCompat
|
||||
val consumedByPanel = computePanelTopPosition(panelOffset) - measuredHeight
|
||||
val adjustedBottomInset = (consumedByPanel + bars.bottom).coerceAtLeast(0)
|
||||
|
||||
return insets.replaceSystemBarInsetsCompat(bars.left, bars.top, bars.right, adjustedBottomInset)
|
||||
return insets.replaceSystemBarInsetsCompat(
|
||||
bars.left, bars.top, bars.right, adjustedBottomInset
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable = Bundle().apply {
|
||||
|
@ -586,6 +587,8 @@ class PlaybackLayout @JvmOverloads constructor(
|
|||
(computePanelTopPosition(0f) - topPosition).toFloat() / panelRange
|
||||
|
||||
private fun smoothSlideTo(offset: Float) {
|
||||
logD("Smooth sliding to $offset")
|
||||
|
||||
val okay = dragHelper.smoothSlideViewTo(
|
||||
playbackContainerView, playbackContainerView.left, computePanelTopPosition(offset)
|
||||
)
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.oxycblt.auxio.databinding.ViewSeekBarBinding
|
|||
import org.oxycblt.auxio.music.toDuration
|
||||
import org.oxycblt.auxio.util.getAttrColorSafe
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.logD
|
||||
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
|
||||
// to seconds.
|
||||
// In either of these cases, the seekbar is more or less useless. Disable it.
|
||||
logD("Duration is 0, entering disabled state")
|
||||
binding.seekBar.apply {
|
||||
valueTo = 1f
|
||||
isEnabled = false
|
||||
|
|
|
@ -111,7 +111,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
*/
|
||||
fun playAlbum(album: Album, shuffled: Boolean) {
|
||||
if (album.songs.isEmpty()) {
|
||||
logE("Album is empty, Not playing.")
|
||||
logE("Album is empty, Not playing")
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
*/
|
||||
fun playArtist(artist: Artist, shuffled: Boolean) {
|
||||
if (artist.songs.isEmpty()) {
|
||||
logE("Artist is empty, Not playing.")
|
||||
logE("Artist is empty, Not playing")
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
*/
|
||||
fun playGenre(genre: Genre, shuffled: Boolean) {
|
||||
if (genre.songs.isEmpty()) {
|
||||
logE("Genre is empty, Not playing.")
|
||||
logE("Genre is empty, Not playing")
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
if (playbackManager.isRestored && MusicStore.loaded()) {
|
||||
playWithUriInternal(uri, context)
|
||||
} else {
|
||||
logD("Cant play this URI right now, waiting...")
|
||||
logD("Cant play this URI right now, waiting")
|
||||
|
||||
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.
|
||||
*/
|
||||
fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) {
|
||||
val adjusted = adapterIndex + (playbackManager.queue.size - mNextUp.value!!.size)
|
||||
logD("$adjusted")
|
||||
|
||||
if (adjusted in playbackManager.queue.indices) {
|
||||
val index = adapterIndex + (playbackManager.queue.size - mNextUp.value!!.size)
|
||||
if (index in playbackManager.queue.indices) {
|
||||
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 {
|
||||
val delta = (playbackManager.queue.size - mNextUp.value!!.size)
|
||||
|
||||
val from = adapterFrom + delta
|
||||
val to = adapterTo + delta
|
||||
|
||||
if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) {
|
||||
apply()
|
||||
playbackManager.moveQueueItems(from, to)
|
||||
|
@ -332,7 +328,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
* [PlaybackStateManager] instance.
|
||||
*/
|
||||
private fun restorePlaybackState() {
|
||||
logD("Attempting to restore playback state.")
|
||||
logD("Attempting to restore playback state")
|
||||
|
||||
onSongUpdate(playbackManager.song)
|
||||
onPositionUpdate(playbackManager.position)
|
||||
|
|
|
@ -70,11 +70,9 @@ class QueueAdapter(
|
|||
QUEUE_SONG_ITEM_TYPE -> QueueSongViewHolder(
|
||||
ItemQueueSongBinding.inflate(parent.context.inflater)
|
||||
)
|
||||
|
||||
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.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 Header -> (holder as HeaderViewHolder).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>) {
|
||||
if (data != newData) {
|
||||
data = newData
|
||||
|
||||
listDiffer.submitList(newData)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.util.getDimenSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
@ -89,9 +90,10 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
|||
val holder = viewHolder as QueueAdapter.QueueSongViewHolder
|
||||
|
||||
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||
logD("Lifting queue item")
|
||||
|
||||
val bg = holder.bodyView.background as MaterialShapeDrawable
|
||||
val elevation = recyclerView.context.getDimenSafe(R.dimen.elevation_small)
|
||||
|
||||
holder.itemView.animate()
|
||||
.translationZ(elevation)
|
||||
.setDuration(100)
|
||||
|
@ -127,8 +129,9 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
|||
val holder = viewHolder as QueueAdapter.QueueSongViewHolder
|
||||
|
||||
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()
|
||||
.translationZ(0.0f)
|
||||
.setDuration(100)
|
||||
|
|
|
@ -28,6 +28,7 @@ import androidx.navigation.fragment.findNavController
|
|||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* A [Fragment] that shows the queue and enables editing as well.
|
||||
|
@ -77,9 +78,11 @@ class QueueFragment : Fragment() {
|
|||
}
|
||||
|
||||
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) {
|
||||
logD("Reshuffle event, scrolling to top")
|
||||
lastShuffle = isShuffling
|
||||
|
||||
binding.queueRecycler.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,10 +48,10 @@ class PlaybackStateDatabase(context: Context) :
|
|||
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = nuke(db)
|
||||
|
||||
private fun nuke(db: SQLiteDatabase) {
|
||||
logD("Nuking database")
|
||||
db.apply {
|
||||
execSQL("DROP TABLE IF EXISTS $TABLE_NAME_STATE")
|
||||
execSQL("DROP TABLE IF EXISTS $TABLE_NAME_QUEUE")
|
||||
|
||||
onCreate(this)
|
||||
}
|
||||
}
|
||||
|
@ -103,34 +103,6 @@ class PlaybackStateDatabase(context: Context) :
|
|||
|
||||
// --- 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.
|
||||
* @param musicStore Required to transform database songs/parents into actual instances
|
||||
|
@ -178,11 +150,69 @@ class PlaybackStateDatabase(context: Context) :
|
|||
isShuffling = cursor.getInt(shuffleIndex) == 1,
|
||||
loopMode = LoopMode.fromInt(cursor.getInt(loopModeIndex)) ?: LoopMode.NONE,
|
||||
)
|
||||
|
||||
logD("Successfully read playback state: $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.
|
||||
*/
|
||||
|
@ -190,12 +220,11 @@ class PlaybackStateDatabase(context: Context) :
|
|||
assertBackgroundThread()
|
||||
|
||||
val database = writableDatabase
|
||||
|
||||
database.transaction {
|
||||
delete(TABLE_NAME_QUEUE, null, null)
|
||||
}
|
||||
|
||||
logD("Wiped queue db.")
|
||||
logD("Wiped queue db")
|
||||
|
||||
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(
|
||||
val song: Song?,
|
||||
val position: Long,
|
||||
|
|
|
@ -224,7 +224,6 @@ class PlaybackStateManager private constructor() {
|
|||
private fun updatePlayback(song: Song, shouldPlay: Boolean = true) {
|
||||
mSong = song
|
||||
mPosition = 0
|
||||
|
||||
setPlaying(shouldPlay)
|
||||
}
|
||||
|
||||
|
@ -271,18 +270,14 @@ class PlaybackStateManager private constructor() {
|
|||
* Remove a queue item at [index]. Will ignore invalid indexes.
|
||||
*/
|
||||
fun removeQueueItem(index: Int): Boolean {
|
||||
logD("Removing item ${mQueue[index].name}.")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
logD("Removing item ${mQueue[index].name}")
|
||||
mQueue.removeAt(index)
|
||||
|
||||
pushQueueUpdate()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -292,15 +287,12 @@ class PlaybackStateManager private constructor() {
|
|||
fun moveQueueItems(from: Int, to: Int): Boolean {
|
||||
if (from > mQueue.size || from < 0 || to > mQueue.size || to < 0) {
|
||||
logE("Indices were out of bounds, did not move queue item")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
val item = mQueue.removeAt(from)
|
||||
mQueue.add(to, item)
|
||||
|
||||
logD("Moving item $from to position $to")
|
||||
mQueue.add(to, mQueue.removeAt(from))
|
||||
pushQueueUpdate()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -501,7 +493,7 @@ class PlaybackStateManager private constructor() {
|
|||
* @param context [Context] required
|
||||
*/
|
||||
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.
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -519,7 +511,7 @@ class PlaybackStateManager private constructor() {
|
|||
database.writeQueue(mQueue)
|
||||
|
||||
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.
|
||||
*/
|
||||
suspend fun restoreFromDatabase(context: Context) {
|
||||
logD("Getting state from DB.")
|
||||
logD("Getting state from DB")
|
||||
|
||||
val musicStore = MusicStore.maybeGetInstance() ?: return
|
||||
|
||||
val start: Long
|
||||
val playbackState: PlaybackStateDatabase.SavedState?
|
||||
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
|
||||
|
||||
if (playbackState != null) {
|
||||
logD("Found playback state $playbackState")
|
||||
|
||||
unpackFromPlaybackState(playbackState)
|
||||
unpackQueue(queue)
|
||||
doParentSanityCheck()
|
||||
doIndexSanityCheck()
|
||||
}
|
||||
|
||||
logD("Restore finished in ${System.currentTimeMillis() - start}ms")
|
||||
logD("State load completed successfully in ${System.currentTimeMillis() - start}ms")
|
||||
|
||||
markRestored()
|
||||
}
|
||||
|
@ -592,7 +581,7 @@ class PlaybackStateManager private constructor() {
|
|||
private fun doParentSanityCheck() {
|
||||
// Check if the parent was lost while in the DB.
|
||||
if (mSong != null && mParent == null && mPlaybackMode != PlaybackMode.ALL_SONGS) {
|
||||
logD("Parent lost, attempting restore.")
|
||||
logD("Parent lost, attempting restore")
|
||||
|
||||
mParent = when (mPlaybackMode) {
|
||||
PlaybackMode.IN_ALBUM -> mQueue.firstOrNull()?.album
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
|||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
|
@ -85,6 +86,7 @@ class AudioReactor(
|
|||
* Request the android system for audio focus
|
||||
*/
|
||||
fun requestFocus() {
|
||||
logD("Requesting audio focus")
|
||||
AudioManagerCompat.requestAudioFocus(audioManager, request)
|
||||
}
|
||||
|
||||
|
@ -94,7 +96,7 @@ class AudioReactor(
|
|||
*/
|
||||
fun applyReplayGain(metadata: Metadata?) {
|
||||
if (metadata == null) {
|
||||
logD("No metadata.")
|
||||
logW("No metadata could be extracted from this track")
|
||||
volume = 1f
|
||||
return
|
||||
}
|
||||
|
@ -102,7 +104,7 @@ class AudioReactor(
|
|||
// ReplayGain is configurable, so determine what to do based off of the mode.
|
||||
val useAlbumGain: (Gain) -> Boolean = when (settingsManager.replayGainMode) {
|
||||
ReplayGainMode.OFF -> {
|
||||
logD("ReplayGain is off.")
|
||||
logD("ReplayGain is off")
|
||||
volume = 1f
|
||||
return
|
||||
}
|
||||
|
@ -132,10 +134,10 @@ class AudioReactor(
|
|||
|
||||
val adjust = if (gain != null) {
|
||||
if (useAlbumGain(gain)) {
|
||||
logD("Using album gain.")
|
||||
logD("Using album gain")
|
||||
gain.album
|
||||
} else {
|
||||
logD("Using track gain.")
|
||||
logD("Using track gain")
|
||||
gain.track
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.ComponentName
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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
|
||||
|
@ -20,6 +21,7 @@ import androidx.core.content.ContextCompat
|
|||
class MediaButtonReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_MEDIA_BUTTON) {
|
||||
logD("Received external media button intent")
|
||||
intent.component = ComponentName(context, PlaybackService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
|
|
@ -180,7 +180,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
|
||||
settingsManager.addCallback(this)
|
||||
|
||||
logD("Service created.")
|
||||
logD("Service created")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -207,7 +207,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
serviceJob.cancel()
|
||||
}
|
||||
|
||||
logD("Service destroyed.")
|
||||
logD("Service destroyed")
|
||||
}
|
||||
|
||||
// --- PLAYER EVENT LISTENER OVERRIDES ---
|
||||
|
@ -260,22 +260,21 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
|
||||
override fun onSongUpdate(song: Song?) {
|
||||
if (song != null) {
|
||||
logD("Setting player to ${song.name}")
|
||||
player.setMediaItem(MediaItem.fromUri(song.uri))
|
||||
player.prepare()
|
||||
|
||||
notification.setMetadata(song, ::startForegroundOrNotify)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Clear if there's nothing to play.
|
||||
logD("Nothing playing, stopping playback")
|
||||
player.stop()
|
||||
stopForegroundAndNotification()
|
||||
}
|
||||
|
||||
override fun onParentUpdate(parent: MusicParent?) {
|
||||
notification.setParent(parent)
|
||||
|
||||
startForegroundOrNotify()
|
||||
}
|
||||
|
||||
|
@ -295,7 +294,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
override fun onLoopUpdate(loopMode: LoopMode) {
|
||||
if (!settingsManager.useAltNotifAction) {
|
||||
notification.setLoop(loopMode)
|
||||
|
||||
startForegroundOrNotify()
|
||||
}
|
||||
}
|
||||
|
@ -303,7 +301,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
override fun onShuffleUpdate(isShuffling: Boolean) {
|
||||
if (settingsManager.useAltNotifAction) {
|
||||
notification.setShuffle(isShuffling)
|
||||
|
||||
startForegroundOrNotify()
|
||||
}
|
||||
}
|
||||
|
@ -334,7 +331,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
override fun onShowCoverUpdate(showCovers: Boolean) {
|
||||
playbackManager.song?.let { song ->
|
||||
connector.onSongUpdate(song)
|
||||
|
||||
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.
|
||||
* TODO: Don't fire when the service initially starts?
|
||||
*/
|
||||
private inner class PlaybackReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
@ -501,7 +498,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
*/
|
||||
private fun resumeFromPlug() {
|
||||
if (playbackManager.song != null && settingsManager.doPlugMgt) {
|
||||
logD("Device connected, resuming...")
|
||||
logD("Device connected, resuming")
|
||||
playbackManager.setPlaying(true)
|
||||
}
|
||||
}
|
||||
|
@ -511,7 +508,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
|||
*/
|
||||
private fun pauseFromPlug() {
|
||||
if (playbackManager.song != null && settingsManager.doPlugMgt) {
|
||||
logD("Device disconnected, pausing...")
|
||||
logD("Device disconnected, pausing")
|
||||
playbackManager.setPlaying(false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.oxycblt.auxio.coil.loadBitmap
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.LoopMode
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* Nightmarish class that coordinates communication between [MediaSessionCompat], [Player],
|
||||
|
@ -158,6 +159,8 @@ class PlaybackSessionConnector(
|
|||
// --- MISC ---
|
||||
|
||||
private fun invalidateSessionState() {
|
||||
logD("Updating media session state")
|
||||
|
||||
// Position updates arrive faster when you upload STATE_PAUSED for some insane reason.
|
||||
val state = PlaybackStateCompat.Builder()
|
||||
.setActions(ACTIONS)
|
||||
|
|
|
@ -52,7 +52,6 @@ class SearchAdapter(
|
|||
is Album -> AlbumViewHolder.ITEM_TYPE
|
||||
is Song -> SongViewHolder.ITEM_TYPE
|
||||
is Header -> HeaderViewHolder.ITEM_TYPE
|
||||
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +76,7 @@ class SearchAdapter(
|
|||
|
||||
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
||||
|
||||
else -> error("Invalid ViewHolder item type.")
|
||||
else -> error("Invalid ViewHolder item type")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -114,7 +114,6 @@ class SearchFragment : Fragment() {
|
|||
if (!launchedKeyboard) {
|
||||
// Auto-open the keyboard when this view is shown
|
||||
requestFocus()
|
||||
|
||||
postDelayed(200) {
|
||||
imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
@ -162,7 +161,7 @@ class SearchFragment : Fragment() {
|
|||
imm.hide()
|
||||
}
|
||||
|
||||
logD("Fragment created.")
|
||||
logD("Fragment created")
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.MusicStore
|
|||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import java.text.Normalizer
|
||||
|
||||
/**
|
||||
|
@ -70,11 +71,14 @@ class SearchViewModel : ViewModel() {
|
|||
mLastQuery = query
|
||||
|
||||
if (query.isEmpty() || musicStore == null) {
|
||||
logD("No music/query, ignoring search")
|
||||
mSearchResults.value = listOf()
|
||||
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 {
|
||||
val sort = Sort.ByName(true)
|
||||
val results = mutableListOf<BaseModel>()
|
||||
|
@ -127,6 +131,8 @@ class SearchViewModel : ViewModel() {
|
|||
else -> null
|
||||
}
|
||||
|
||||
logD("Updating filter mode to $mFilterMode")
|
||||
|
||||
settingsManager.searchFilterMode = mFilterMode
|
||||
|
||||
search(mLastQuery)
|
||||
|
|
|
@ -74,7 +74,7 @@ class AboutFragment : Fragment() {
|
|||
)
|
||||
}
|
||||
|
||||
logD("Dialog created.")
|
||||
logD("Dialog created")
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
@ -83,6 +83,8 @@ class AboutFragment : Fragment() {
|
|||
* Go through the process of opening a [link] in a browser.
|
||||
*/
|
||||
private fun openLinkInBrowser(link: String) {
|
||||
logD("Opening $link")
|
||||
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, link.toUri()).setFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
)
|
||||
|
|
|
@ -22,8 +22,7 @@ import android.content.SharedPreferences
|
|||
import androidx.core.content.edit
|
||||
import org.oxycblt.auxio.accent.Accent
|
||||
|
||||
// A couple of utils for migrating from old settings values to the new
|
||||
// formats used in 1.3.2 & 1.4.0
|
||||
// A couple of utils for migrating from old settings values to the new formats
|
||||
|
||||
fun handleAccentCompat(prefs: SharedPreferences): Accent {
|
||||
if (prefs.contains(OldKeys.KEY_ACCENT2)) {
|
||||
|
|
|
@ -31,7 +31,7 @@ import androidx.preference.children
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.Coil
|
||||
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.home.tabs.TabCustomizeDialog
|
||||
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?) {
|
||||
|
@ -119,7 +119,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
|||
|
||||
SettingsManager.KEY_ACCENT -> {
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
AccentDialog().show(childFragmentManager, AccentDialog.TAG)
|
||||
AccentCustomizeDialog().show(childFragmentManager, AccentCustomizeDialog.TAG)
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -182,7 +182,6 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
|||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> R.drawable.ic_auto
|
||||
AppCompatDelegate.MODE_NIGHT_NO -> R.drawable.ic_day
|
||||
AppCompatDelegate.MODE_NIGHT_YES -> R.drawable.ic_night
|
||||
|
||||
else -> R.drawable.ic_auto
|
||||
}
|
||||
}
|
||||
|
|
|
@ -331,7 +331,7 @@ class SettingsManager private constructor(context: Context) :
|
|||
return instance
|
||||
}
|
||||
|
||||
error("SettingsManager must be initialized with init() before getting its instance.")
|
||||
error("SettingsManager must be initialized with init() before getting its instance")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.updatePadding
|
||||
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
|
||||
|
||||
/**
|
||||
|
@ -51,7 +51,6 @@ open class EdgeAppBarLayout @JvmOverloads constructor(
|
|||
|
||||
if (child != null) {
|
||||
val coordinator = parent as CoordinatorLayout
|
||||
|
||||
(layoutParams as CoordinatorLayout.LayoutParams).behavior?.onNestedPreScroll(
|
||||
coordinator, this, coordinator, 0, 0, tConsumed, 0
|
||||
)
|
||||
|
@ -66,15 +65,12 @@ open class EdgeAppBarLayout @JvmOverloads constructor(
|
|||
|
||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||
super.onApplyWindowInsets(insets)
|
||||
|
||||
updatePadding(top = insets.systemBarInsetsCompat.top)
|
||||
|
||||
return insets
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
|
||||
viewTreeObserver.removeOnPreDrawListener(onPreDraw)
|
||||
}
|
||||
|
||||
|
@ -94,9 +90,10 @@ open class EdgeAppBarLayout @JvmOverloads constructor(
|
|||
if (liftOnScrollTargetViewId != ResourcesCompat.ID_NULL) {
|
||||
scrollingChild = (parent as ViewGroup).findViewById(liftOnScrollTargetViewId)
|
||||
} else {
|
||||
logE("liftOnScrollTargetViewId was not specified. ignoring scroll events.")
|
||||
logW("liftOnScrollTargetViewId was not specified. ignoring scroll events")
|
||||
}
|
||||
}
|
||||
|
||||
return scrollingChild
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ class MemberBinder<T : ViewDataBinding>(
|
|||
val lifecycle = fragment.viewLifecycleOwner.lifecycle
|
||||
|
||||
check(lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
|
||||
"Fragment views are destroyed."
|
||||
"Fragment views are destroyed"
|
||||
}
|
||||
|
||||
// Otherwise create the binding and return that.
|
||||
|
|
|
@ -39,7 +39,6 @@ import androidx.annotation.PluralsRes
|
|||
import androidx.annotation.Px
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.MainActivity
|
||||
import kotlin.reflect.KClass
|
||||
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 {
|
||||
logE("$what load failed.")
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
// 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
|
||||
}
|
||||
logE("$what load failed")
|
||||
e.logTraceOrThrow()
|
||||
return default
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -34,7 +34,7 @@ fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
|
|||
*/
|
||||
fun assertBackgroundThread() {
|
||||
check(Looper.myLooper() != Looper.getMainLooper()) {
|
||||
"This operation must be ran on a background thread."
|
||||
"This operation must be ran on a background thread"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
@ -48,6 +55,18 @@ fun Any.logE(msg: String) {
|
|||
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
|
||||
* @return The name of the object, otherwise "Anonymous Object"
|
||||
|
|
|
@ -69,6 +69,7 @@ fun RecyclerView.canScroll(): Boolean = computeVerticalScrollRange() > height
|
|||
*/
|
||||
fun View.disableDropShadowCompat() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
logD("Disabling drop shadows")
|
||||
val transparent = context.getColorSafe(android.R.color.transparent)
|
||||
outlineAmbientShadowColor = transparent
|
||||
outlineSpotShadowColor = transparent
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.playback.state.LoopMode
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
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
|
||||
|
@ -53,6 +54,8 @@ class WidgetController(private val context: Context) :
|
|||
* Release this instance, removing the callbacks and resetting all widgets
|
||||
*/
|
||||
fun release() {
|
||||
logD("Releasing instance")
|
||||
|
||||
widget.reset(context)
|
||||
playbackManager.removeCallback(this)
|
||||
settingsManager.removeCallback(this)
|
||||
|
|
|
@ -40,6 +40,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
|||
import org.oxycblt.auxio.util.getDimenSizeSafe
|
||||
import org.oxycblt.auxio.util.isLandscape
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
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) {
|
||||
val coverRequest = ImageRequest.Builder(context)
|
||||
.data(song.album)
|
||||
|
@ -152,6 +157,8 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
|
||||
|
||||
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
|
||||
// from PlaybackService.
|
||||
requestUpdate(context)
|
||||
|
@ -234,7 +241,7 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
continue
|
||||
} else {
|
||||
// Default to the smallest view if no layout fits
|
||||
logD("No widget layout found")
|
||||
logW("No good widget layout found")
|
||||
|
||||
val minimum = requireNotNull(
|
||||
views.minByOrNull { it.key.width * it.key.height }?.value
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_small"
|
||||
android:ellipsize="end"
|
||||
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}"
|
||||
app:songInfo="@{song}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
|
||||
app:layout_constraintEnd_toEndOf="@+id/playback_song"
|
||||
app:layout_constraintStart_toEndOf="@+id/playback_cover"
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_small"
|
||||
android:ellipsize="end"
|
||||
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}"
|
||||
app:songInfo="@{song}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
|
||||
app:layout_constraintEnd_toEndOf="@+id/playback_song"
|
||||
app:layout_constraintStart_toEndOf="@+id/playback_cover"
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
android:paddingTop="@dimen/spacing_medium"
|
||||
android:paddingEnd="@dimen/spacing_medium"
|
||||
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_constraintTop_toBottomOf="@+id/accent_header"
|
||||
tools:itemCount="18"
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
||||
android:layout_width="0dp"
|
||||
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_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:artistCounts="@{artist}"
|
||||
app:artistInfo="@{artist}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/artist_image"
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
||||
android:layout_width="0dp"
|
||||
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_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/genre_image"
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
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_constraintEnd_toStartOf="@+id/song_duration"
|
||||
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
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_constraintEnd_toStartOf="@+id/song_drag_handle"
|
||||
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
style="@style/Widget.Auxio.TextView.Item.Secondary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}"
|
||||
app:songInfo="@{song}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
android:layout_marginStart="@dimen/spacing_small"
|
||||
android:layout_marginEnd="@dimen/spacing_small"
|
||||
android:ellipsize="end"
|
||||
android:text="@{@string/fmt_two(song.resolvedArtistName, song.resolvedAlbumName)}"
|
||||
app:songInfo="@{song}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/playback_cover"
|
||||
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
|
||||
app:layout_constraintStart_toEndOf="@+id/playback_cover"
|
||||
|
|
Loading…
Reference in a new issue