accent: move back to ui

Move the accent module back into the ui module, where it's more consistent.
This commit is contained in:
Alexander Capehart 2022-12-25 19:31:27 -07:00
parent ac137d4cc8
commit 9d283fc6e4
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
110 changed files with 1159 additions and 1168 deletions

View file

@ -74,14 +74,10 @@ class AuxioApp : Application(), ImageLoaderFactory {
.build() .build()
companion object { companion object {
/** /** The ID of the "Shuffle All" shortcut. */
* The ID of the "Shuffle All" shortcut.
*/
const val SHORTCUT_SHUFFLE_ID = "shortcut_shuffle" const val SHORTCUT_SHUFFLE_ID = "shortcut_shuffle"
/** /** The [Intent] name for the "Shuffle All" shortcut. */
* The [Intent] name for the "Shuffle All" shortcut.
*/
const val INTENT_KEY_SHORTCUT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE_ALL" const val INTENT_KEY_SHORTCUT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE_ALL"
} }
} }

View file

@ -18,8 +18,8 @@
package org.oxycblt.auxio package org.oxycblt.auxio
/** /**
* A table containing all of the magic integer codes that the codebase has currently reserved. * A table containing all of the magic integer codes that the codebase has currently reserved. May
* May be non-contiguous. * be non-contiguous.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
object IntegerTable { object IntegerTable {

View file

@ -113,10 +113,9 @@ class MainActivity : AppCompatActivity() {
} }
/** /**
* Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action] * Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action] that can be used
* that can be used in the playback system. * in the playback system.
* @param intent The (new) [Intent] given to this [MainActivity], or null if there * @param intent The (new) [Intent] given to this [MainActivity], or null if there is no intent.
* is no intent.
* @return true If the analogous [InternalPlayer.Action] to the given [Intent] was started, * @return true If the analogous [InternalPlayer.Action] to the given [Intent] was started,
* false otherwise. * false otherwise.
*/ */

View file

@ -40,9 +40,9 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior
import org.oxycblt.auxio.shared.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**
@ -348,8 +348,8 @@ class MainFragment :
} }
/** /**
* A [OnBackPressedCallback] that overrides the back button to first navigate out of * A [OnBackPressedCallback] that overrides the back button to first navigate out of internal
* internal app components, such as the Bottom Sheets or Explore Navigation. * app components, such as the Bottom Sheets or Explore Navigation.
*/ */
private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) { private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@ -379,13 +379,13 @@ class MainFragment :
} }
/** /**
* Force this instance to update whether it's enabled or not. If there are no app * Force this instance to update whether it's enabled or not. If there are no app components
* components that the back button should close first, the instance is disabled and * that the back button should close first, the instance is disabled and back navigation is
* back navigation is delegated to the system. * delegated to the system.
* *
* Normally, this callback would have just called the [MainActivity.onBackPressed] * Normally, this callback would have just called the [MainActivity.onBackPressed] if there
* if there were no components to close, but that prevents adaptive back navigation * were no components to close, but that prevents adaptive back navigation from working on
* from working on Android 14+, so we must do it this way. * Android 14+, so we must do it this way.
*/ */
fun invalidateEnabled() { fun invalidateEnabled() {
val binding = requireBinding() val binding = requireBinding()

View file

@ -174,7 +174,6 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists) navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists)
} }
private fun updateAlbum(album: Album?) { private fun updateAlbum(album: Album?) {
if (album == null) { if (album == null) {
// Album we were showing no longer exists. // Album we were showing no longer exists.

View file

@ -54,8 +54,7 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapte
// Information about what artist to display is initially within the navigation arguments // Information about what artist to display is initially within the navigation arguments
// as a UID, as that is the only safe way to parcel an artist. // as a UID, as that is the only safe way to parcel an artist.
private val args: ArtistDetailFragmentArgs by navArgs() private val args: ArtistDetailFragmentArgs by navArgs()
private val detailAdapter = private val detailAdapter = ArtistDetailAdapter(this)
ArtistDetailAdapter(this)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View file

@ -1,3 +1,20 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.detail package org.oxycblt.auxio.detail
import androidx.annotation.StringRes import androidx.annotation.StringRes

View file

@ -31,7 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import java.lang.reflect.Field import java.lang.reflect.Field
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.shared.AuxioAppBarLayout import org.oxycblt.auxio.ui.AuxioAppBarLayout
import org.oxycblt.auxio.util.getInteger import org.oxycblt.auxio.util.getInteger
import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedField
@ -75,7 +75,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// The Toolbar's title view is actually hidden. To avoid having to create our own // The Toolbar's title view is actually hidden. To avoid having to create our own
// title view, we just reflect into Toolbar and grab the hidden field. // title view, we just reflect into Toolbar and grab the hidden field.
val newTitleView = (TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply { val newTitleView =
(TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply {
// We can never properly initialize the title view's state before draw time, // We can never properly initialize the title view's state before draw time,
// so we just set it's alpha to 0f to produce a less jarring initialization // so we just set it's alpha to 0f to produce a less jarring initialization
// animation.. // animation..
@ -161,8 +162,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// Title should be visible if we are no longer showing the top item // Title should be visible if we are no longer showing the top item
// (i.e the header) // (i.e the header)
appBarLayout.setTitleVisibility( appBarLayout.setTitleVisibility(
(recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() > 0 (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() > 0)
)
} }
} }

View file

@ -44,9 +44,9 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**
* [AndroidViewModel] that manages the Song, Album, Artist, and Genre detail views. * [AndroidViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of
* Keeps track of the current item they are showing, sub-data to display, and configuration. * the current item they are showing, sub-data to display, and configuration. Since this ViewModel
* Since this ViewModel requires a context, it must be instantiated [AndroidViewModel]'s Factory. * requires a context, it must be instantiated [AndroidViewModel]'s Factory.
* @param application [Application] context required to initialize certain information. * @param application [Application] context required to initialize certain information.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@ -61,8 +61,8 @@ class DetailViewModel(application: Application) :
private val _currentSong = MutableStateFlow<DetailSong?>(null) private val _currentSong = MutableStateFlow<DetailSong?>(null)
/** /**
* The current [DetailSong] to display. Null if there is nothing to show. * The current [DetailSong] to display. Null if there is nothing to show. TODO: De-couple Song
* TODO: De-couple Song and Properties? * and Properties?
*/ */
val currentSong: StateFlow<DetailSong?> val currentSong: StateFlow<DetailSong?>
get() = _currentSong get() = _currentSong
@ -392,8 +392,8 @@ class DetailViewModel(application: Application) :
/** /**
* A simpler mapping of [Album.Type] used for grouping and sorting songs. * A simpler mapping of [Album.Type] used for grouping and sorting songs.
* @param headerTitleRes The title string resource to use for a header created * @param headerTitleRes The title string resource to use for a header created out of an
* out of an instance of this enum. * instance of this enum.
*/ */
private enum class AlbumGrouping(@StringRes val headerTitleRes: Int) { private enum class AlbumGrouping(@StringRes val headerTitleRes: Int) {
ALBUMS(R.string.lbl_albums), ALBUMS(R.string.lbl_albums),

View file

@ -55,8 +55,7 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapter
// Information about what genre to display is initially within the navigation arguments // Information about what genre to display is initially within the navigation arguments
// as a UID, as that is the only safe way to parcel an genre. // as a UID, as that is the only safe way to parcel an genre.
private val args: GenreDetailFragmentArgs by navArgs() private val args: GenreDetailFragmentArgs by navArgs()
private val detailAdapter = private val detailAdapter = GenreDetailAdapter(this)
GenreDetailAdapter(this)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View file

@ -25,8 +25,8 @@ import com.google.android.material.textfield.TextInputEditText
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
/** /**
* A [TextInputEditText] that deliberately restricts all input except for selection. This will * A [TextInputEditText] that deliberately restricts all input except for selection. This will work
* work just like a normal block of selectable/copyable text, but with nicer aesthetics. * just like a normal block of selectable/copyable text, but with nicer aesthetics.
* *
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles * Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
* *

View file

@ -27,7 +27,7 @@ import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSongDetailBinding import org.oxycblt.auxio.databinding.DialogSongDetailBinding
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.shared.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately

View file

@ -27,8 +27,8 @@ import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
import org.oxycblt.auxio.detail.DiscHeader import org.oxycblt.auxio.detail.DiscHeader
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SimpleItemCallback import org.oxycblt.auxio.list.recycler.SimpleItemCallback
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album

View file

@ -26,8 +26,8 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SimpleItemCallback import org.oxycblt.auxio.list.recycler.SimpleItemCallback
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album

View file

@ -26,9 +26,9 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.detail.SortHeader import org.oxycblt.auxio.detail.SortHeader
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.recycler.* import org.oxycblt.auxio.list.recycler.*
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater

View file

@ -58,8 +58,8 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.system.Indexer import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.shared.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**
@ -100,8 +100,7 @@ class HomeFragment :
override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
override fun getSelectionToolbar(binding: FragmentHomeBinding) = override fun getSelectionToolbar(binding: FragmentHomeBinding) = binding.homeSelectionToolbar
binding.homeSelectionToolbar
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
@ -239,7 +238,8 @@ class HomeFragment :
} }
private fun setupPager(binding: FragmentHomeBinding) { private fun setupPager(binding: FragmentHomeBinding) {
binding.homePager.adapter = HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner) binding.homePager.adapter =
HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner)
val toolbarParams = binding.homeSelectionToolbar.layoutParams as AppBarLayout.LayoutParams val toolbarParams = binding.homeSelectionToolbar.layoutParams as AppBarLayout.LayoutParams
if (homeModel.currentTabModes.size == 1) { if (homeModel.currentTabModes.size == 1) {
@ -256,13 +256,17 @@ class HomeFragment :
} }
// Set up the mapping between the ViewPager and TabLayout. // Set up the mapping between the ViewPager and TabLayout.
TabLayoutMediator(binding.homeTabs, binding.homePager, TabLayoutMediator(
AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes)).attach() binding.homeTabs,
binding.homePager,
AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes))
.attach()
} }
private fun updateCurrentTab(tabMode: MusicMode) { private fun updateCurrentTab(tabMode: MusicMode) {
// Update the sort options to align with those allowed by the tab // Update the sort options to align with those allowed by the tab
val isVisible: (Int) -> Boolean = when (tabMode) { val isVisible: (Int) -> Boolean =
when (tabMode) {
// Disallow sorting by count for songs // Disallow sorting by count for songs
MusicMode.SONGS -> { id -> id != R.id.option_sort_count } MusicMode.SONGS -> { id -> id != R.id.option_sort_count }
// Disallow sorting by album for albums // Disallow sorting by album for albums
@ -289,8 +293,9 @@ class HomeFragment :
for (option in sortMenu) { for (option in sortMenu) {
// Check the ascending option and corresponding sort option to align with // Check the ascending option and corresponding sort option to align with
// the current sort of the tab. // the current sort of the tab.
option.isChecked = option.itemId == toHighlight.mode.itemId option.isChecked =
|| (option.itemId == R.id.option_sort_asc && toHighlight.isAscending) option.itemId == toHighlight.mode.itemId ||
(option.itemId == R.id.option_sort_asc && toHighlight.isAscending)
// Disable options that are not allowed by the isVisible lambda // Disable options that are not allowed by the isVisible lambda
option.isVisible = isVisible(option.itemId) option.isVisible = isVisible(option.itemId)
@ -454,8 +459,8 @@ class HomeFragment :
} }
/** /**
* Get the ID of the RecyclerView contained by [ViewPager2] tab represented with * Get the ID of the RecyclerView contained by [ViewPager2] tab represented with the given
* the given [MusicMode]. * [MusicMode].
* @param tabMode The [MusicMode] of the tab. * @param tabMode The [MusicMode] of the tab.
* @return The ID of the RecyclerView contained by the given tab. * @return The ID of the RecyclerView contained by the given tab.
*/ */
@ -478,8 +483,7 @@ class HomeFragment :
private val tabs: List<MusicMode>, private val tabs: List<MusicMode>,
fragmentManager: FragmentManager, fragmentManager: FragmentManager,
lifecycleOwner: LifecycleOwner lifecycleOwner: LifecycleOwner
) : ) : FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle) {
FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle ) {
override fun getItemCount() = tabs.size override fun getItemCount() = tabs.size

View file

@ -44,60 +44,50 @@ class HomeViewModel(application: Application) :
private val settings = Settings(application, this) private val settings = Settings(application, this)
private val _songsList = MutableStateFlow(listOf<Song>()) private val _songsList = MutableStateFlow(listOf<Song>())
/** /** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
* A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view.
*/
val songLists: StateFlow<List<Song>> val songLists: StateFlow<List<Song>>
get() = _songsList get() = _songsList
private val _albumsLists = MutableStateFlow(listOf<Album>()) private val _albumsLists = MutableStateFlow(listOf<Album>())
/** /** A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view. */
* A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view.
*/
val albumsList: StateFlow<List<Album>> val albumsList: StateFlow<List<Album>>
get() = _albumsLists get() = _albumsLists
private val _artistsList = MutableStateFlow(listOf<Artist>()) private val _artistsList = MutableStateFlow(listOf<Artist>())
/** /**
* A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. * A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. Note that
* Note that if "Hide collaborators" is on, this list will not include [Artist]s * if "Hide collaborators" is on, this list will not include [Artist]s where
* where [Artist.isCollaborator] is true. * [Artist.isCollaborator] is true.
*/ */
val artistsList: MutableStateFlow<List<Artist>> val artistsList: MutableStateFlow<List<Artist>>
get() = _artistsList get() = _artistsList
private val _genresList = MutableStateFlow(listOf<Genre>()) private val _genresList = MutableStateFlow(listOf<Genre>())
/** /** A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view. */
* A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view.
*/
val genresList: StateFlow<List<Genre>> val genresList: StateFlow<List<Genre>>
get() = _genresList get() = _genresList
/** /**
* A list of [MusicMode] corresponding to the current [Tab] configuration, excluding * A list of [MusicMode] corresponding to the current [Tab] configuration, excluding invisible
* invisible [Tab]s. * [Tab]s.
*/ */
var currentTabModes: List<MusicMode> = makeTabModes() var currentTabModes: List<MusicMode> = makeTabModes()
private set private set
private val _currentTabMode = MutableStateFlow(currentTabModes[0]) private val _currentTabMode = MutableStateFlow(currentTabModes[0])
/** /** The [MusicMode] of the currently shown [Tab]. */
* The [MusicMode] of the currently shown [Tab].
*/
val currentTabMode: StateFlow<MusicMode> = _currentTabMode val currentTabMode: StateFlow<MusicMode> = _currentTabMode
private val _shouldRecreate = MutableStateFlow(false) private val _shouldRecreate = MutableStateFlow(false)
/** /**
* A marker to re-create all library tabs, usually initiated by a settings change. * A marker to re-create all library tabs, usually initiated by a settings change. When this
* When this flag is true, all tabs (and their respective ViewPager2 fragments) will be * flag is true, all tabs (and their respective ViewPager2 fragments) will be re-created from
* re-created from scratch. * scratch.
*/ */
val shouldRecreate: StateFlow<Boolean> = _shouldRecreate val shouldRecreate: StateFlow<Boolean> = _shouldRecreate
private val _isFastScrolling = MutableStateFlow(false) private val _isFastScrolling = MutableStateFlow(false)
/** /** A marker for whether the user is fast-scrolling in the home view or not. */
* A marker for whether the user is fast-scrolling in the home view or not.
*/
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
init { init {
@ -136,7 +126,6 @@ class HomeViewModel(application: Application) :
currentTabModes = makeTabModes() currentTabModes = makeTabModes()
_shouldRecreate.value = true _shouldRecreate.value = true
} }
context.getString(R.string.set_key_hide_collaborators) -> { context.getString(R.string.set_key_hide_collaborators) -> {
// Changes in the hide collaborator setting will change the artist contents // Changes in the hide collaborator setting will change the artist contents
// of the library, consider it a library update. // of the library, consider it a library update.
@ -213,9 +202,8 @@ class HomeViewModel(application: Application) :
/** /**
* Create a list of [MusicMode]s representing a simpler version of the [Tab] configuration. * Create a list of [MusicMode]s representing a simpler version of the [Tab] configuration.
* @return A list of the [MusicMode]s for each visible [Tab] in the configuration, * @return A list of the [MusicMode]s for each visible [Tab] in the configuration, ordered in
* ordered in the same way as the configuration. * the same way as the configuration.
*/ */
private fun makeTabModes() = private fun makeTabModes() = settings.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
settings.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
} }

View file

@ -19,7 +19,6 @@ package org.oxycblt.auxio.home.fastscroll
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.PointF
import android.graphics.Rect import android.graphics.Rect
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity import android.view.Gravity
@ -72,22 +71,18 @@ class FastScrollRecyclerView
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
AuxioRecyclerView(context, attrs, defStyleAttr) { AuxioRecyclerView(context, attrs, defStyleAttr) {
/** /** An interface to provide text to use in the popup when fast-scrolling. */
* An interface to provide text to use in the popup when fast-scrolling.
*/
interface PopupProvider { interface PopupProvider {
/** /**
* Get text to use in the popup at the specified position. * Get text to use in the popup at the specified position.
* @param pos The position in the list. * @param pos The position in the list.
* @return A [String] to use in the popup. Null if there is no applicable text for * @return A [String] to use in the popup. Null if there is no applicable text for the popup
* the popup at [pos]. * at [pos].
*/ */
fun getPopup(pos: Int): String? fun getPopup(pos: Int): String?
} }
/** /** A listener for fast scroller interactions. */
* A listener for fast scroller interactions.
*/
interface Listener { interface Listener {
/** /**
* Called when the fast scrolling state changes. * Called when the fast scrolling state changes.

View file

@ -46,7 +46,10 @@ import org.oxycblt.auxio.util.collectImmediately
* A [ListFragment] that shows a list of [Album]s. * A [ListFragment] that shows a list of [Album]s.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AlbumListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecyclerView.Listener, FastScrollRecyclerView.PopupProvider { class AlbumListFragment :
ListFragment<FragmentHomeListBinding>(),
FastScrollRecyclerView.Listener,
FastScrollRecyclerView.PopupProvider {
private val homeModel: HomeViewModel by activityViewModels() private val homeModel: HomeViewModel by activityViewModels()
private val albumAdapter = AlbumAdapter(this) private val albumAdapter = AlbumAdapter(this)
// Save memory by re-using the same formatter and string builder when creating popup text // Save memory by re-using the same formatter and string builder when creating popup text

View file

@ -44,7 +44,10 @@ import org.oxycblt.auxio.util.nonZeroOrNull
* A [ListFragment] that shows a list of [Artist]s. * A [ListFragment] that shows a list of [Artist]s.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class ArtistListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener { class ArtistListFragment :
ListFragment<FragmentHomeListBinding>(),
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels() private val homeModel: HomeViewModel by activityViewModels()
private val homeAdapter = ArtistAdapter(this) private val homeAdapter = ArtistAdapter(this)

View file

@ -43,7 +43,10 @@ import org.oxycblt.auxio.util.collectImmediately
* A [ListFragment] that shows a list of [Genre]s. * A [ListFragment] that shows a list of [Genre]s.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class GenreListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener{ class GenreListFragment :
ListFragment<FragmentHomeListBinding>(),
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels() private val homeModel: HomeViewModel by activityViewModels()
private val homeAdapter = GenreAdapter(this) private val homeAdapter = GenreAdapter(this)

View file

@ -47,7 +47,10 @@ import org.oxycblt.auxio.util.collectImmediately
* A [ListFragment] that shows a list of [Song]s. * A [ListFragment] that shows a list of [Song]s.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class SongListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener { class SongListFragment :
ListFragment<FragmentHomeListBinding>(),
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels() private val homeModel: HomeViewModel by activityViewModels()
private val homeAdapter = SongAdapter(this) private val homeAdapter = SongAdapter(this)
// Save memory by re-using the same formatter and string builder when creating popup text // Save memory by re-using the same formatter and string builder when creating popup text

View file

@ -21,7 +21,6 @@ import android.content.Context
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD

View file

@ -33,8 +33,7 @@ sealed class Tab(open val mode: MusicMode) {
data class Visible(override val mode: MusicMode) : Tab(mode) data class Visible(override val mode: MusicMode) : Tab(mode)
/** /**
* A visible tab. This will be visible in the tab configuration view, but not in the * A visible tab. This will be visible in the tab configuration view, but not in the home view.
* home view.
* @param mode The type of list in the home view this instance corresponds to. * @param mode The type of list in the home view this instance corresponds to.
*/ */
data class Invisible(override val mode: MusicMode) : Tab(mode) data class Invisible(override val mode: MusicMode) : Tab(mode)
@ -58,9 +57,8 @@ sealed class Tab(open val mode: MusicMode) {
private const val SEQUENCE_LEN = 4 private const val SEQUENCE_LEN = 4
/** /**
* The default tab sequence, in integer form. * The default tab sequence, in integer form. This represents a set of four visible tabs
* This represents a set of four visible tabs ordered as "Song", "Album", "Artist", and * ordered as "Song", "Album", "Artist", and "Genre".
* "Genre".
*/ */
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100 const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100

View file

@ -78,8 +78,8 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
/** A listener for interactions specific to tab configuration. */ /** A listener for interactions specific to tab configuration. */
interface Listener { interface Listener {
/** /**
* Called when a tab is clicked, requesting that the visibility should be inverted * Called when a tab is clicked, requesting that the visibility should be inverted (i.e
* (i.e Visible -> Invisible and vice versa). * Visible -> Invisible and vice versa).
* @param tabMode The [MusicMode] of the tab clicked. * @param tabMode The [MusicMode] of the tab clicked.
*/ */
fun onToggleVisibility(tabMode: MusicMode) fun onToggleVisibility(tabMode: MusicMode)

View file

@ -27,7 +27,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.databinding.DialogTabsBinding
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD

View file

@ -31,25 +31,26 @@ import org.oxycblt.auxio.music.Song
* A utility to provide bitmaps in a race-less manner. * A utility to provide bitmaps in a race-less manner.
* *
* When it comes to components that load images manually as [Bitmap] instances, queued * When it comes to components that load images manually as [Bitmap] instances, queued
* [ImageRequest]s may cause a race condition that results in the incorrect image being * [ImageRequest]s may cause a race condition that results in the incorrect image being drawn. This
* drawn. This utility resolves this by keeping track of the current request, and disposing * utility resolves this by keeping track of the current request, and disposing it as soon as a new
* it as soon as a new request is queued or if another, competing request is newer. * request is queued or if another, competing request is newer.
* *
* @param context [Context] required to load images. * @param context [Context] required to load images.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class BitmapProvider(private val context: Context) { class BitmapProvider(private val context: Context) {
/** An extension of [Disposable] with an additional [Target] to deliver the final [Bitmap] to. */ /**
* An extension of [Disposable] with an additional [Target] to deliver the final [Bitmap] to.
*/
private data class Request(val disposable: Disposable, val callback: Target) private data class Request(val disposable: Disposable, val callback: Target)
/** The target that will receive the requested [Bitmap]. */ /** The target that will receive the requested [Bitmap]. */
interface Target { interface Target {
/** /**
* Configure the [ImageRequest.Builder] to enable [Target]-specific configuration. * Configure the [ImageRequest.Builder] to enable [Target]-specific configuration.
* @param builder The [ImageRequest.Builder] that will be used to request the * @param builder The [ImageRequest.Builder] that will be used to request the desired
* desired [Bitmap]. * [Bitmap].
* @return The same [ImageRequest.Builder] in order to easily chain configuration * @return The same [ImageRequest.Builder] in order to easily chain configuration methods.
* methods.
*/ */
fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder
@ -80,7 +81,8 @@ class BitmapProvider(private val context: Context) {
currentRequest = null currentRequest = null
val imageRequest = val imageRequest =
target.onConfigRequest( target
.onConfigRequest(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(song) .data(song)
// Use ORIGINAL sizing, as we are not loading into any View-like component. // Use ORIGINAL sizing, as we are not loading into any View-like component.
@ -110,9 +112,7 @@ class BitmapProvider(private val context: Context) {
currentRequest = Request(context.imageLoader.enqueue(imageRequest.build()), target) currentRequest = Request(context.imageLoader.enqueue(imageRequest.build()), target)
} }
/** /** Release this instance, cancelling any currently running operations. */
* Release this instance, cancelling any currently running operations.
*/
@Synchronized @Synchronized
fun release() { fun release() {
++currentHandle ++currentHandle

View file

@ -39,8 +39,8 @@ import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.getInteger import org.oxycblt.auxio.util.getInteger
/** /**
* A super-charged [StyledImageView]. This class enables the following features in addition * A super-charged [StyledImageView]. This class enables the following features in addition to
* to [StyledImageView]: * [StyledImageView]:
* - A selection indicator * - A selection indicator
* - An activation (playback) indicator * - An activation (playback) indicator
* - Support for ONE custom view * - Support for ONE custom view
@ -174,9 +174,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/** /**
* Whether this view should be indicated to have ongoing playback or not. See * Whether this view should be indicated to have ongoing playback or not. See
* PlaybackIndicatorView for more information on what occurs here. * PlaybackIndicatorView for more information on what occurs here. Note: It's expected for this
* Note: It's expected for this view to already be marked as playing with setSelected * view to already be marked as playing with setSelected (not the same thing) before this is set
* (not the same thing) before this is set to true. * to true.
*/ */
var isPlaying: Boolean var isPlaying: Boolean
get() = playbackIndicatorView.isPlaying get() = playbackIndicatorView.isPlaying
@ -214,13 +214,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (isActivated) { if (isActivated) {
// View is "activated" (i.e marked as selected), so show the selection indicator. // View is "activated" (i.e marked as selected), so show the selection indicator.
targetAlpha = 1f targetAlpha = 1f
targetDuration = targetDuration = context.getInteger(R.integer.anim_fade_enter_duration).toLong()
context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else { } else {
// View is not "activated", hide the selection indicator. // View is not "activated", hide the selection indicator.
targetAlpha = 0f targetAlpha = 0f
targetDuration = targetDuration = context.getInteger(R.integer.anim_fade_exit_duration).toLong()
context.getInteger(R.integer.anim_fade_exit_duration).toLong()
} }
if (selectionIndicatorView.alpha == targetAlpha) { if (selectionIndicatorView.alpha == targetAlpha) {

View file

@ -33,8 +33,8 @@ import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat import org.oxycblt.auxio.util.getDrawableCompat
/** /**
* A view that displays an activation (i.e playback) indicator, with an accented styling and * A view that displays an activation (i.e playback) indicator, with an accented styling and an
* an animated equalizer icon. * animated equalizer icon.
* *
* This is only meant for use with [ImageGroup]. Due to limitations with [AnimationDrawable] * This is only meant for use with [ImageGroup]. Due to limitations with [AnimationDrawable]
* instances within custom views, this cannot be merged with [ImageGroup]. * instances within custom views, this cannot be merged with [ImageGroup].
@ -55,8 +55,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private val settings = Settings(context) private val settings = Settings(context)
/** /**
* The corner radius of this view. This allows the outer ImageGroup to apply it's * The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius
* corner radius to this view without any attribute hacks. * to this view without any attribute hacks.
*/ */
var cornerRadius = 0f var cornerRadius = 0f
set(value) { set(value) {
@ -71,8 +71,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
/** /**
* Whether this view should be indicated to have ongoing playback or not. If true, * Whether this view should be indicated to have ongoing playback or not. If true, the animated
* the animated playing icon will be shown. If false, the static paused icon will be shown. * playing icon will be shown. If false, the static paused icon will be shown.
*/ */
var isPlaying: Boolean var isPlaying: Boolean
get() = drawable == playingIndicatorDrawable get() = drawable == playingIndicatorDrawable

View file

@ -48,8 +48,8 @@ import org.oxycblt.auxio.util.getDrawableCompat
* *
* - Tonal background * - Tonal background
* - Rounded corners based on user preferences * - Rounded corners based on user preferences
* - Built-in support for binding image data or using a static icon with the same * - Built-in support for binding image data or using a static icon with the same styling as
* styling as placeholder drawables. * placeholder drawables.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@ -116,8 +116,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
* Internally bind a [Music]'s image to this view. * Internally bind a [Music]'s image to this view.
* @param music The music to find. * @param music The music to find.
* @param errorRes The error drawable resource to use if the music cannot be loaded. * @param errorRes The error drawable resource to use if the music cannot be loaded.
* @param descRes The content description string resource to use. The resource must have * @param descRes The content description string resource to use. The resource must have one
* one field for the name of the [Music]. * field for the name of the [Music].
*/ */
private fun bindImpl(music: Music, @DrawableRes errorRes: Int, @StringRes descRes: Int) { private fun bindImpl(music: Music, @DrawableRes errorRes: Int, @StringRes descRes: Int) {
// Dispose of any previous image request and load a new image. // Dispose of any previous image request and load a new image.
@ -132,8 +132,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
/** /**
* A [Drawable] wrapper that re-styles the drawable to better align with the style * A [Drawable] wrapper that re-styles the drawable to better align with the style of
* of [StyledImageView]. * [StyledImageView].
* @param context [Context] required for initialization. * @param context [Context] required for initialization.
* @param inner The [Drawable] to wrap. * @param inner The [Drawable] to wrap.
*/ */

View file

@ -52,8 +52,8 @@ class MusicKeyer : Keyer<Music> {
} }
/** /**
* Generic [Fetcher] for [Album] covers. Works with both [Album] and [Song]. * Generic [Fetcher] for [Album] covers. Works with both [Album] and [Song]. Use [SongFactory] or
* Use [SongFactory] or [AlbumFactory] for instantiation. * [AlbumFactory] for instantiation.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AlbumCoverFetcher class AlbumCoverFetcher
@ -129,8 +129,8 @@ private constructor(
* Map at most N [T] items a collection into a collection of [R], ignoring [T] that cannot be * Map at most N [T] items a collection into a collection of [R], ignoring [T] that cannot be
* transformed into [R]. * transformed into [R].
* @param n The maximum amount of items to map. * @param n The maximum amount of items to map.
* @param transform The function that transforms data [T] from the original list into * @param transform The function that transforms data [T] from the original list into data [R] in
* data [R] in the new list. Can return null if the [T] cannot be transformed into an [R]. * the new list. Can return null if the [T] cannot be transformed into an [R].
* @return A new list of at most N non-null [R] items. * @return A new list of at most N non-null [R] items.
*/ */
private inline fun <T : Any, R : Any> Collection<T>.mapAtMostNotNull( private inline fun <T : Any, R : Any> Collection<T>.mapAtMostNotNull(

View file

@ -1,3 +1,20 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.image.extractor package org.oxycblt.auxio.image.extractor
import android.content.Context import android.content.Context
@ -7,6 +24,8 @@ import com.google.android.exoplayer2.MediaMetadata
import com.google.android.exoplayer2.MetadataRetriever import com.google.android.exoplayer2.MetadataRetriever
import com.google.android.exoplayer2.metadata.flac.PictureFrame import com.google.android.exoplayer2.metadata.flac.PictureFrame
import com.google.android.exoplayer2.metadata.id3.ApicFrame import com.google.android.exoplayer2.metadata.id3.ApicFrame
import java.io.ByteArrayInputStream
import java.io.InputStream
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.image.CoverMode import org.oxycblt.auxio.image.CoverMode
@ -14,8 +33,6 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import java.io.ByteArrayInputStream
import java.io.InputStream
/** /**
* Internal utilities for loading album covers. * Internal utilities for loading album covers.
@ -26,8 +43,8 @@ object Covers {
* Fetch an album cover, respecting the current cover configuration. * Fetch an album cover, respecting the current cover configuration.
* @param context [Context] required to load the image. * @param context [Context] required to load the image.
* @param album [Album] to load the cover from. * @param album [Album] to load the cover from.
* @return An [InputStream] of image data if the cover loading was successful, null if the * @return An [InputStream] of image data if the cover loading was successful, null if the cover
* cover loading failed or should not occur. * loading failed or should not occur.
*/ */
suspend fun fetch(context: Context, album: Album): InputStream? { suspend fun fetch(context: Context, album: Album): InputStream? {
val settings = Settings(context) val settings = Settings(context)
@ -45,8 +62,8 @@ object Covers {
} }
/** /**
* Load an [Album] cover directly from one of it's Song files. This attempts * Load an [Album] cover directly from one of it's Song files. This attempts the following in
* the following in order: * order:
* - [MediaMetadataRetriever], as it has the best support and speed. * - [MediaMetadataRetriever], as it has the best support and speed.
* - ExoPlayer's [MetadataRetriever], as some devices (notably Samsung) can have broken * - ExoPlayer's [MetadataRetriever], as some devices (notably Samsung) can have broken
* [MediaMetadataRetriever] implementations. * [MediaMetadataRetriever] implementations.

View file

@ -1,3 +1,20 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.image.extractor package org.oxycblt.auxio.image.extractor
import android.content.Context import android.content.Context
@ -14,9 +31,9 @@ import coil.fetch.SourceResult
import coil.size.Dimension import coil.size.Dimension
import coil.size.Size import coil.size.Size
import coil.size.pxOrElse import coil.size.pxOrElse
import java.io.InputStream
import okio.buffer import okio.buffer
import okio.source import okio.source
import java.io.InputStream
/** /**
* Utilities for constructing Artist and Genre images. * Utilities for constructing Artist and Genre images.
@ -24,8 +41,8 @@ import java.io.InputStream
*/ */
object Images { object Images {
/** /**
* Create a mosaic image from the given image [InputStream]s. * Create a mosaic image from the given image [InputStream]s. Derived from phonograph:
* Derived from phonograph: https://github.com/kabouzeid/Phonograph * https://github.com/kabouzeid/Phonograph
* @param context [Context] required to generate the mosaic. * @param context [Context] required to generate the mosaic.
* @param streams [InputStream]s of image data to create the mosaic out of. * @param streams [InputStream]s of image data to create the mosaic out of.
* @param size [Size] of the Mosaic to generate. * @param size [Size] of the Mosaic to generate.

View file

@ -1,10 +1,25 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.list package org.oxycblt.auxio.list
import androidx.annotation.StringRes import androidx.annotation.StringRes
/** /** A marker for something that is a RecyclerView item. Has no functionality on it's own. */
* A marker for something that is a RecyclerView item. Has no functionality on it's own.
*/
interface Item interface Item
/** /**

View file

@ -27,8 +27,8 @@ import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.selection.SelectionFragment import org.oxycblt.auxio.list.selection.SelectionFragment
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.shared.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
@ -47,8 +47,8 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Selecta
} }
/** /**
* Called when [onClick] is called, but does not result in the item being selected. This * Called when [onClick] is called, but does not result in the item being selected. This more or
* more or less corresponds to an [onClick] implementation in a non-[ListFragment]. * less corresponds to an [onClick] implementation in a non-[ListFragment].
* @param music The [Music] item that was clicked. * @param music The [Music] item that was clicked.
*/ */
abstract fun onRealClick(music: Music) abstract fun onRealClick(music: Music)
@ -70,8 +70,8 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Selecta
} }
/** /**
* Opens a menu in the context of a [Song]. This menu will be managed by the Fragment and * Opens a menu in the context of a [Song]. This menu will be managed by the Fragment and closed
* closed when the view is destroyed. If a menu is already opened, this call is ignored. * when the view is destroyed. If a menu is already opened, this call is ignored.
* @param anchor The [View] to anchor the menu to. * @param anchor The [View] to anchor the menu to.
* @param menuRes The resource of the menu to load. * @param menuRes The resource of the menu to load.
* @param song The [Song] to create the menu for. * @param song The [Song] to create the menu for.
@ -223,8 +223,8 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Selecta
} }
/** /**
* Open a menu. This menu will be managed by the Fragment and closed when the view is * Open a menu. This menu will be managed by the Fragment and closed when the view is destroyed.
* destroyed. If a menu is already opened, this call is ignored. * If a menu is already opened, this call is ignored.
* @param anchor The [View] to anchor the menu to. * @param anchor The [View] to anchor the menu to.
* @param menuRes The resource of the menu to load. * @param menuRes The resource of the menu to load.
* @param block A block that is ran within [PopupMenu] that allows further configuration. * @param block A block that is ran within [PopupMenu] that allows further configuration.

View file

@ -1,14 +1,29 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.list package org.oxycblt.auxio.list
import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
/** /**
* A basic listener for list interactions. * A basic listener for list interactions. TODO: Supply a ViewHolder on clicks (allows editable
* TODO: Supply a ViewHolder on clicks (allows editable lists to be standardized into a listener.) * lists to be standardized into a listener.)
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
interface ClickableListListener { interface ClickableListListener {

View file

@ -18,7 +18,6 @@
package org.oxycblt.auxio.list.recycler package org.oxycblt.auxio.list.recycler
import android.content.Context import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet import android.util.AttributeSet
import android.view.WindowInsets import android.view.WindowInsets
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
@ -56,8 +55,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets { override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
// Update the RecyclerView's padding such that the bottom insets are applied // Update the RecyclerView's padding such that the bottom insets are applied
// while still preserving bottom padding. // while still preserving bottom padding.
updatePadding( updatePadding(bottom = initialPaddingBottom + insets.systemBarInsetsCompat.bottom)
bottom = initialPaddingBottom + insets.systemBarInsetsCompat.bottom)
return insets return insets
} }

View file

@ -100,17 +100,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
lmm.findLastCompletelyVisibleItemPosition() == (lmm.itemCount - 1) lmm.findLastCompletelyVisibleItemPosition() == (lmm.itemCount - 1)
} }
/** /** A [RecyclerView.ViewHolder] that implements dialog-specific fixes. */
* A [RecyclerView.ViewHolder] that implements dialog-specific fixes.
*/
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) { abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
init { init {
// ViewHolders are not automatically full-width in dialogs, manually resize // ViewHolders are not automatically full-width in dialogs, manually resize
// them to be as such. // them to be as such.
root.layoutParams = root.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
} }
} }
} }

View file

@ -21,7 +21,6 @@ import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
/** /**
* A [RecyclerView.Adapter] that supports indicating the playback status of a particular item. * A [RecyclerView.Adapter] that supports indicating the playback status of a particular item.
@ -36,8 +35,7 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
private var isPlaying = false private var isPlaying = false
/** /**
* The current list of the adapter. This is used to update items if the indicator * The current list of the adapter. This is used to update items if the indicator state changes.
* state changes.
*/ */
abstract val currentList: List<Item> abstract val currentList: List<Item>
@ -106,15 +104,13 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
} }
} }
/** /** A [RecyclerView.ViewHolder] that can display a playing indicator. */
* A [RecyclerView.ViewHolder] that can display a playing indicator.
*/
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) { abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
/** /**
* Update the playing indicator within this [RecyclerView.ViewHolder]. * Update the playing indicator within this [RecyclerView.ViewHolder].
* @param isActive True if this item is playing, false otherwise. * @param isActive True if this item is playing, false otherwise.
* @param isPlaying True if playback is ongoing, false if paused. If this * @param isPlaying True if playback is ongoing, false if paused. If this is true,
* is true, [isActive] will also be true. * [isActive] will also be true.
*/ */
abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean)
} }

View file

@ -68,9 +68,7 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
} }
} }
/** /** A [PlayingIndicatorAdapter.ViewHolder] that can display a selection indicator. */
* A [PlayingIndicatorAdapter.ViewHolder] that can display a selection indicator.
*/
abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) { abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) {
/** /**
* Update the selection indicator within this [PlayingIndicatorAdapter.ViewHolder]. * Update the selection indicator within this [PlayingIndicatorAdapter.ViewHolder].

View file

@ -21,9 +21,8 @@ import androidx.recyclerview.widget.DiffUtil
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
/** /**
* A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method. * A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method. Use this
* Use this whenever creating [DiffUtil.ItemCallback] implementations with an [Item] * whenever creating [DiffUtil.ItemCallback] implementations with an [Item] subclass.
* subclass.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() { abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {

View file

@ -23,8 +23,8 @@ import androidx.recyclerview.widget.RecyclerView
/** /**
* A list differ that operates synchronously. This can help resolve some shortcomings with * A list differ that operates synchronously. This can help resolve some shortcomings with
* AsyncListDiffer, at the cost of performance. * AsyncListDiffer, at the cost of performance. Derived from Material Files:
* Derived from Material Files: https://github.com/zhanghai/MaterialFiles * https://github.com/zhanghai/MaterialFiles
* @author Hai Zhang, Alexander Capehart (OxygenCobalt) * @author Hai Zhang, Alexander Capehart (OxygenCobalt)
*/ */
class SyncListDiffer<T>( class SyncListDiffer<T>(
@ -111,8 +111,8 @@ class SyncListDiffer<T>(
} }
/** /**
* Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only * Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only use it
* use it if the changes are trivial. * if the changes are trivial.
* @param newList The list to update to. * @param newList The list to update to.
*/ */
fun submitList(newList: List<T>) { fun submitList(newList: List<T>) {
@ -125,8 +125,8 @@ class SyncListDiffer<T>(
} }
/** /**
* Replace this list with a new list. This is good for large diffs that are too slow to * Replace this list with a new list. This is good for large diffs that are too slow to update
* update synchronously, but too chaotic to update asynchronously. * synchronously, but too chaotic to update asynchronously.
* @param newList The list to update to. * @param newList The list to update to.
*/ */
fun replaceList(newList: List<T>) { fun replaceList(newList: List<T>) {

View file

@ -24,8 +24,8 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemHeaderBinding import org.oxycblt.auxio.databinding.ItemHeaderBinding
import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre

View file

@ -18,16 +18,14 @@
package org.oxycblt.auxio.list.selection package org.oxycblt.auxio.list.selection
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
/** /**
@ -40,10 +38,10 @@ abstract class SelectionFragment<VB : ViewBinding> :
protected val playbackModel: PlaybackViewModel by androidActivityViewModels() protected val playbackModel: PlaybackViewModel by androidActivityViewModels()
/** /**
* Get the [SelectionToolbarOverlay] of the concrete Fragment to be automatically managed * Get the [SelectionToolbarOverlay] of the concrete Fragment to be automatically managed by
* by [SelectionFragment]. * [SelectionFragment].
* @return The [SelectionToolbarOverlay] of the concrete [SelectionFragment]'s [VB], or * @return The [SelectionToolbarOverlay] of the concrete [SelectionFragment]'s [VB], or null if
* null if there is not one. * there is not one.
*/ */
open fun getSelectionToolbar(binding: VB): SelectionToolbarOverlay? = null open fun getSelectionToolbar(binding: VB): SelectionToolbarOverlay? = null

View file

@ -115,13 +115,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (selectionVisible) { if (selectionVisible) {
targetInnerAlpha = 0f targetInnerAlpha = 0f
targetSelectionAlpha = 1f targetSelectionAlpha = 1f
targetDuration = targetDuration = context.getInteger(R.integer.anim_fade_enter_duration).toLong()
context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else { } else {
targetInnerAlpha = 1f targetInnerAlpha = 1f
targetSelectionAlpha = 0f targetSelectionAlpha = 0f
targetDuration = targetDuration = context.getInteger(R.integer.anim_fade_exit_duration).toLong()
context.getInteger(R.integer.anim_fade_exit_duration).toLong()
} }
if (innerToolbar.alpha == targetInnerAlpha && if (innerToolbar.alpha == targetInnerAlpha &&
@ -154,8 +152,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/** /**
* Update the alpha of the inner and selection [MaterialToolbar]s. * Update the alpha of the inner and selection [MaterialToolbar]s.
* @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the * @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the inverse
* inverse opacity of the selection [MaterialToolbar]. * opacity of the selection [MaterialToolbar].
*/ */
private fun setToolbarsAlpha(innerAlpha: Float) { private fun setToolbarsAlpha(innerAlpha: Float) {
innerToolbar.apply { innerToolbar.apply {

View file

@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.util.logD
/** /**
* A [ViewModel] that manages the current selection. * A [ViewModel] that manages the current selection.
@ -31,10 +30,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback {
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val _selected = MutableStateFlow(listOf<Music>()) private val _selected = MutableStateFlow(listOf<Music>())
/** /** the currently selected items. These are ordered in earliest selected and latest selected. */
* the currently selected items. These are ordered in earliest selected
* and latest selected.
*/
val selected: StateFlow<List<Music>> val selected: StateFlow<List<Music>>
get() = _selected get() = _selected
@ -49,7 +45,8 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback {
// Sanitize the selection to remove items that no longer exist and thus // Sanitize the selection to remove items that no longer exist and thus
// won't appear in any list. // won't appear in any list.
_selected.value = _selected.value.mapNotNull { _selected.value =
_selected.value.mapNotNull {
when (it) { when (it) {
is Song -> library.sanitize(it) is Song -> library.sanitize(it)
is Album -> library.sanitize(it) is Album -> library.sanitize(it)
@ -65,8 +62,8 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback {
} }
/** /**
* Select a new [Music] item. If this item is already within the selected items, the item will be * Select a new [Music] item. If this item is already within the selected items, the item will
* removed. Otherwise, it will be added. * be removed. Otherwise, it will be added.
* @param music The [Music] item to select. * @param music The [Music] item to select.
*/ */
fun select(music: Music) { fun select(music: Music) {
@ -81,6 +78,5 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback {
* Consume the current selection. This will clear any items that were selected prior. * Consume the current selection. This will clear any items that were selected prior.
* @return The list of selected items before it was cleared. * @return The list of selected items before it was cleared.
*/ */
fun consume() = fun consume() = _selected.value.also { _selected.value = listOf() }
_selected.value.also { _selected.value = listOf() }
} }

View file

@ -15,7 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// We use a special naming convention for internal fields, disable the lints that check for that.
@file:Suppress("PropertyName", "FunctionName") @file:Suppress("PropertyName", "FunctionName")
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
@ -25,6 +24,8 @@ import android.os.Parcelable
import java.security.MessageDigest import java.security.MessageDigest
import java.text.CollationKey import java.text.CollationKey
import java.text.Collator import java.text.Collator
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.UUID import java.util.UUID
import kotlin.math.max import kotlin.math.max
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
@ -39,14 +40,12 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.inRangeOrNull import org.oxycblt.auxio.util.inRangeOrNull
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
import java.text.ParseException
import java.text.SimpleDateFormat
// --- MUSIC MODELS --- // --- MUSIC MODELS ---
/** /**
* Abstract music data. This contains universal information about all concrete music implementations, * Abstract music data. This contains universal information about all concrete music
* such as identification information and names. * implementations, such as identification information and names.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
sealed class Music : Item { sealed class Music : Item {
@ -66,34 +65,33 @@ sealed class Music : Item {
* Returns a name suitable for use in the app UI. This should be favored over [rawName] in * Returns a name suitable for use in the app UI. This should be favored over [rawName] in
* nearly all cases. * nearly all cases.
* @param context [Context] required to obtain placeholder text or formatting information. * @param context [Context] required to obtain placeholder text or formatting information.
* @return A human-readable string representing the name of this music. In the case that * @return A human-readable string representing the name of this music. In the case that the
* the item does not have a name, an analogous "Unknown X" name is returned. * item does not have a name, an analogous "Unknown X" name is returned.
*/ */
abstract fun resolveName(context: Context): String abstract fun resolveName(context: Context): String
/** /**
* The raw sort name of this item as it was extracted from the file-system. This can be used * The raw sort name of this item as it was extracted from the file-system. This can be used not
* not only when sorting music, but also trying to locate music based on a fuzzy search by * only when sorting music, but also trying to locate music based on a fuzzy search by the user.
* the user. Will be null if the item has no known sort name. * Will be null if the item has no known sort name.
*/ */
abstract val rawSortName: String? abstract val rawSortName: String?
/** /**
* A [CollationKey] derived from [rawName] and [rawSortName] that can be used to sort items * A [CollationKey] derived from [rawName] and [rawSortName] that can be used to sort items in a
* in a semantically-correct manner. Will be null if the item has no name. * semantically-correct manner. Will be null if the item has no name.
* *
* The key will have the following attributes: * The key will have the following attributes:
* - If [rawSortName] is present, this key will be derived from it. Otherwise [rawName] * - If [rawSortName] is present, this key will be derived from it. Otherwise [rawName] is used.
* is used.
* - If the string begins with an article, such as "the", it will be stripped, as is usually * - If the string begins with an article, such as "the", it will be stripped, as is usually
* convention for sorting media. This is not internationalized. * convention for sorting media. This is not internationalized.
*/ */
abstract val collationKey: CollationKey? abstract val collationKey: CollationKey?
/** /**
* Finalize this item once the music library has been fully constructed. This is where * Finalize this item once the music library has been fully constructed. This is where any final
* any final ordering or sanity checking should occur. * ordering or sanity checking should occur. **This function is internal to the music package.
* **This function is internal to the music package. Do not use it elsewhere.** * Do not use it elsewhere.**
*/ */
abstract fun _finalize() abstract fun _finalize()
@ -128,20 +126,20 @@ sealed class Music : Item {
* A unique identifier for a piece of music. * A unique identifier for a piece of music.
* *
* [UID] enables a much cheaper and more reliable form of differentiating music, derived from * [UID] enables a much cheaper and more reliable form of differentiating music, derived from
* either a hash of meaningful metadata or the MusicBrainz ID spec. Using this enables * either a hash of meaningful metadata or the MusicBrainz ID spec. Using this enables several
* several improvements to music management in this app, including: * improvements to music management in this app, including:
* *
* - Proper differentiation of identical music. It's common for large, well-tagged libraries * - Proper differentiation of identical music. It's common for large, well-tagged libraries to
* to have functionally duplicate items that are differentiated with MusicBrainz IDs, and so * have functionally duplicate items that are differentiated with MusicBrainz IDs, and so [UID]
* [UID] allows us to properly differentiate between these in the app. * allows us to properly differentiate between these in the app.
* - Better music persistence between restarts. Whereas directly storing song names would be * - Better music persistence between restarts. Whereas directly storing song names would be
* prone to collisions, and storing MediaStore IDs would drift rapidly as the music library * prone to collisions, and storing MediaStore IDs would drift rapidly as the music library
* changes, [UID] enables a much stronger form of persistence given it's unique link to a * changes, [UID] enables a much stronger form of persistence given it's unique link to a
* specific files metadata configuration, which is unlikely to collide with another item * specific files metadata configuration, which is unlikely to collide with another item or
* or drift as the music library changes. * drift as the music library changes.
* *
* Note: Generally try to use [UID] as a black box that can only be read, written, and * Note: Generally try to use [UID] as a black box that can only be read, written, and compared.
* compared. It will not be fun if you try to manipulate it in any other manner. * It will not be fun if you try to manipulate it in any other manner.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@ -180,11 +178,11 @@ sealed class Music : Item {
companion object { companion object {
/** /**
* Creates an auxio-style [UID] with a [UUID] composed of a hash of the * Creates an auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
* non-subjective, unlikely-to-change metadata of the music. * unlikely-to-change metadata of the music.
* @param mode The analogous [MusicMode] of the item that created this [UID]. * @param mode The analogous [MusicMode] of the item that created this [UID].
* @param updates Block to update the [MessageDigest] hash with the metadata of * @param updates Block to update the [MessageDigest] hash with the metadata of the
* the item. Make sure the metadata hashed semantically aligns with the format * item. Make sure the metadata hashed semantically aligns with the format
* specification. * specification.
* @return A new auxio-style [UID]. * @return A new auxio-style [UID].
*/ */
@ -192,7 +190,8 @@ sealed class Music : Item {
// Auxio hashes consist of the MD5 hash of the non-subjective, consistent // Auxio hashes consist of the MD5 hash of the non-subjective, consistent
// tags in a music item. For easier use with MusicBrainz IDs, we transform // tags in a music item. For easier use with MusicBrainz IDs, we transform
// this into a UUID too. // this into a UUID too.
val uuid = MessageDigest.getInstance("MD5").run { val uuid =
MessageDigest.getInstance("MD5").run {
updates() updates()
digest().toUuid() digest().toUuid()
} }
@ -235,7 +234,8 @@ sealed class Music : Item {
return null return null
} }
val mode = MusicMode.fromIntCode(ids[0].toIntOrNull(16) ?: return null) ?: return null val mode =
MusicMode.fromIntCode(ids[0].toIntOrNull(16) ?: return null) ?: return null
val uuid = ids[1].toUuidOrNull() ?: return null val uuid = ids[1].toUuidOrNull() ?: return null
return UID(format, mode, uuid) return UID(format, mode, uuid)
@ -254,9 +254,7 @@ sealed class Music : Item {
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
sealed class MusicParent : Music() { sealed class MusicParent : Music() {
/** /** The [Song]s in this this group. */
* The [Song]s in this this group.
*/
abstract val songs: List<Song> abstract val songs: List<Song>
// Note: Append song contents to MusicParent equality so that Groups with // Note: Append song contents to MusicParent equality so that Groups with
@ -310,14 +308,14 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
val date = raw.date val date = raw.date
/** /**
* The URI to the audio file that this instance was created from. This can be used to * The URI to the audio file that this instance was created from. This can be used to access the
* access the audio file in a way that is scoped-storage-safe. * audio file in a way that is scoped-storage-safe.
*/ */
val uri = requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri() val uri = requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()
/** /**
* The [Path] to this audio file. This is only intended for display, [uri] should be * The [Path] to this audio file. This is only intended for display, [uri] should be favored
* favored instead for accessing the audio file. * instead for accessing the audio file.
*/ */
val path = val path =
Path( Path(
@ -341,8 +339,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
private var _album: Album? = null private var _album: Album? = null
/** /**
* The parent [Album]. If the metadata did not specify an album, it's parent directory is * The parent [Album]. If the metadata did not specify an album, it's parent directory is used
* used instead. * instead.
*/ */
val album: Album val album: Album
get() = unlikelyToBeNull(_album) get() = unlikelyToBeNull(_album)
@ -371,23 +369,23 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
private val _artists = mutableListOf<Artist>() private val _artists = mutableListOf<Artist>()
/** /**
* The parent [Artist]s of this [Song]. Is often one, but there can be multiple if more * The parent [Artist]s of this [Song]. Is often one, but there can be multiple if more than one
* than one [Artist] name was specified in the metadata. Unliked [Album], artists are * [Artist] name was specified in the metadata. Unliked [Album], artists are prioritized for
* prioritized for this field. * this field.
*/ */
val artists: List<Artist> val artists: List<Artist>
get() = _artists get() = _artists
/** /**
* Resolves one or more [Artist]s into a single piece of human-readable names. * Resolves one or more [Artist]s into a single piece of human-readable names.
* @param context [Context] required for [resolveName]. * @param context [Context] required for [resolveName]. TODO Internationalize the list
* TODO Internationalize the list formatter. * formatter.
*/ */
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) } fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
/** /**
* Checks if the [Artist] *display* of this [Song] and another [Song] are equal. This * Checks if the [Artist] *display* of this [Song] and another [Song] are equal. This will only
* will only compare surface-level names, and not [Music.UID]s. * compare surface-level names, and not [Music.UID]s.
* @param other The [Song] to compare to. * @param other The [Song] to compare to.
* @return True if the [Artist] displays are equal, false otherwise * @return True if the [Artist] displays are equal, false otherwise
*/ */
@ -405,8 +403,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
private val _genres = mutableListOf<Genre>() private val _genres = mutableListOf<Genre>()
/** /**
* The parent [Genre]s of this [Song]. Is often one, but there can be multiple if more * The parent [Genre]s of this [Song]. Is often one, but there can be multiple if more than one
* than one [Genre] name was specified in the metadata. * [Genre] name was specified in the metadata.
*/ */
val genres: List<Genre> val genres: List<Genre>
get() = _genres get() = _genres
@ -420,9 +418,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
// --- INTERNAL FIELDS --- // --- INTERNAL FIELDS ---
/** /**
* The [Album.Raw] instances collated by the [Song]. This can be used to group [Song]s into * The [Album.Raw] instances collated by the [Song]. This can be used to group [Song]s into an
* an [Album]. * [Album]. **This is only meant for use within the music package.**
* **This is only meant for use within the music package.**
*/ */
val _rawAlbum = val _rawAlbum =
Album.Raw( Album.Raw(
@ -435,19 +432,17 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) }) rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
/** /**
* The [Artist.Raw] instances collated by the [Song]. The artists of the song take * The [Artist.Raw] instances collated by the [Song]. The artists of the song take priority,
* priority, followed by the album artists. If there are no artists, this field will * followed by the album artists. If there are no artists, this field will be a single "unknown"
* be a single "unknown" [Artist.Raw]. This can be used to group up [Song]s into * [Artist.Raw]. This can be used to group up [Song]s into an [Artist]. **This is only meant for
* an [Artist]. * use within the music package.**
* **This is only meant for use within the music package.**
*/ */
val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(Artist.Raw()) } val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(Artist.Raw()) }
/** /**
* The [Genre.Raw] instances collated by the [Song]. This can be used to group up * The [Genre.Raw] instances collated by the [Song]. This can be used to group up [Song]s into a
* [Song]s into a [Genre]. ID3v2 Genre names are automatically converted to their * [Genre]. ID3v2 Genre names are automatically converted to their resolved names. **This is
* resolved names. * only meant for use within the music package.**
* **This is only meant for use within the music package.**
*/ */
val _rawGenres = val _rawGenres =
raw.genreNames raw.genreNames
@ -457,8 +452,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
/** /**
* Links this [Song] with a parent [Album]. * Links this [Song] with a parent [Album].
* @param album The parent [Album] to link to. * @param album The parent [Album] to link to. **This is only meant for use within the music
* **This is only meant for use within the music package.** * package.**
*/ */
fun _link(album: Album) { fun _link(album: Album) {
_album = album _album = album
@ -466,8 +461,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
/** /**
* Links this [Song] with a parent [Artist]. * Links this [Song] with a parent [Artist].
* @param artist The parent [Artist] to link to. * @param artist The parent [Artist] to link to. **This is only meant for use within the music
* **This is only meant for use within the music package.** * package.**
*/ */
fun _link(artist: Artist) { fun _link(artist: Artist) {
_artists.add(artist) _artists.add(artist)
@ -475,8 +470,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
/** /**
* Links this [Song] with a parent [Genre]. * Links this [Song] with a parent [Genre].
* @param genre The parent [Genre] to link to. * @param genre The parent [Genre] to link to. **This is only meant for use within the music
* **This is only meant for use within the music package.** * package.**
*/ */
fun _link(genre: Genre) { fun _link(genre: Genre) {
_genres.add(genre) _genres.add(genre)
@ -508,14 +503,14 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
} }
/** /**
* Raw information about a [Song] obtained from the filesystem/Extractor instances. * Raw information about a [Song] obtained from the filesystem/Extractor instances. **This is
* **This is only meant for use within the music package.** * only meant for use within the music package.**
*/ */
class Raw class Raw
constructor( constructor(
/** /**
* The ID of the [Song]'s audio file, obtained from MediaStore. Note that this * The ID of the [Song]'s audio file, obtained from MediaStore. Note that this ID is highly
* ID is highly unstable and should only be used for accessing the audio file. * unstable and should only be used for accessing the audio file.
*/ */
var mediaStoreId: Long? = null, var mediaStoreId: Long? = null,
/** @see Song.dateAdded */ /** @see Song.dateAdded */
@ -598,23 +593,20 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
override fun resolveName(context: Context) = rawName override fun resolveName(context: Context) = rawName
/** /**
* The earliest [Date] this album was released. * The earliest [Date] this album was released. Will be null if no valid date was present in the
* Will be null if no valid date was present in the metadata of any [Song]. * metadata of any [Song]. TODO: Date ranges?
* TODO: Date ranges?
*/ */
val date: Date? val date: Date?
/** /**
* The [Type] of this album, signifying the type of release it actually is. * The [Type] of this album, signifying the type of release it actually is. Defaults to
* Defaults to [Type.Album]. * [Type.Album].
*/ */
val type = raw.type ?: Type.Album(null) val type = raw.type ?: Type.Album(null)
/** /**
* The URI to a MediaStore-provided album cover. These images will be fast to load, but * The URI to a MediaStore-provided album cover. These images will be fast to load, but at the
* at the cost of image quality. * cost of image quality.
*/ */
val coverUri = raw.mediaStoreId.toCoverUri() val coverUri = raw.mediaStoreId.toCoverUri()
/** The duration of all songs in the album, in milliseconds. */ /** The duration of all songs in the album, in milliseconds. */
@ -655,9 +647,9 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
private val _artists = mutableListOf<Artist>() private val _artists = mutableListOf<Artist>()
/** /**
* The parent [Artist]s of this [Album]. Is often one, but there can be multiple if more * The parent [Artist]s of this [Album]. Is often one, but there can be multiple if more than
* than one [Artist] name was specified in the metadata of the [Song]'s. Unlike [Song], * one [Artist] name was specified in the metadata of the [Song]'s. Unlike [Song], album artists
* album artists are prioritized for this field. * are prioritized for this field.
*/ */
val artists: List<Artist> val artists: List<Artist>
get() = _artists get() = _artists
@ -669,8 +661,8 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) } fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
/** /**
* Checks if the [Artist] *display* of this [Album] and another [Album] are equal. This * Checks if the [Artist] *display* of this [Album] and another [Album] are equal. This will
* will only compare surface-level names, and not [Music.UID]s. * only compare surface-level names, and not [Music.UID]s.
* @param other The [Album] to compare to. * @param other The [Album] to compare to.
* @return True if the [Artist] displays are equal, false otherwise * @return True if the [Artist] displays are equal, false otherwise
*/ */
@ -690,17 +682,16 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
/** /**
* The [Artist.Raw] instances collated by the [Album]. The album artists of the song take * The [Artist.Raw] instances collated by the [Album]. The album artists of the song take
* priority, followed by the artists. If there are no artists, this field will * priority, followed by the artists. If there are no artists, this field will be a single
* be a single "unknown" [Artist.Raw]. This can be used to group up [Album]s into * "unknown" [Artist.Raw]. This can be used to group up [Album]s into an [Artist]. **This is
* an [Artist]. * only meant for use within the music package.**
* **This is only meant for use within the music package.**
*/ */
val _rawArtists = raw.rawArtists val _rawArtists = raw.rawArtists
/** /**
* Links this [Album] with a parent [Artist]. * Links this [Album] with a parent [Artist].
* @param artist The parent [Artist] to link to. * @param artist The parent [Artist] to link to. **This is only meant for use within the music
* **This is only meant for use within the music package.** * package.**
*/ */
fun _link(artist: Artist) { fun _link(artist: Artist) {
_artists.add(artist) _artists.add(artist)
@ -722,8 +713,8 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
/** /**
* The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc. * The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc.
* *
* This class is derived from the MusicBrainz Release Group Type specification. It can * This class is derived from the MusicBrainz Release Group Type specification. It can be found
* be found at: https://musicbrainz.org/doc/Release_Group/Type * at: https://musicbrainz.org/doc/Release_Group/Type
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
sealed class Type { sealed class Type {
@ -825,8 +816,8 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
} }
/** /**
* A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist] * A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist] or
* or a future release. * a future release.
*/ */
object Mixtape : Type() { object Mixtape : Type() {
override val refinement: Refinement? override val refinement: Refinement?
@ -836,18 +827,12 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
get() = R.string.lbl_mixtape get() = R.string.lbl_mixtape
} }
/** /** A specification of what kind of performance a particular release is. */
* A specification of what kind of performance a particular release is.
*/
enum class Refinement { enum class Refinement {
/** /** A release consisting of a live performance */
* A release consisting of a live performance
*/
LIVE, LIVE,
/** /** A release consisting of another [Artist]s remix of a prior performance. */
* A release consisting of another [Artist]s remix of a prior performance.
*/
REMIX REMIX
} }
@ -856,8 +841,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
* Parse a [Type] from a string formatted with the MusicBrainz Release Group Type * Parse a [Type] from a string formatted with the MusicBrainz Release Group Type
* specification. * specification.
* @param types A list of values consisting of valid release type values. * @param types A list of values consisting of valid release type values.
* @return A [Type] consisting of the given types, or null if the types * @return A [Type] consisting of the given types, or null if the types were not valid.
* were not valid.
*/ */
fun parse(types: List<String>): Type? { fun parse(types: List<String>): Type? {
val primary = types.getOrNull(0) ?: return null val primary = types.getOrNull(0) ?: return null
@ -877,9 +861,9 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
* Parse "secondary" types (i.e not [Album], [EP], or [Single]) from a string formatted * Parse "secondary" types (i.e not [Album], [EP], or [Single]) from a string formatted
* with the MusicBrainz Release Group Type specification. * with the MusicBrainz Release Group Type specification.
* @param index The index of the release type to parse. * @param index The index of the release type to parse.
* @param convertRefinement Code to convert a [Refinement] into a [Type] * @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding
* corresponding to the callee's context. This is used in order to handle secondary * to the callee's context. This is used in order to handle secondary times that are
* times that are actually [Refinement]s. * actually [Refinement]s.
* @return A [Type] corresponding to the secondary type found at that index. * @return A [Type] corresponding to the secondary type found at that index.
*/ */
private inline fun List<String>.parseSecondaryTypes( private inline fun List<String>.parseSecondaryTypes(
@ -898,12 +882,12 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
} }
/** /**
* Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond * Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond to
* to any child values. * any child values.
* @param type The release type value to parse. * @param type The release type value to parse.
* @param convertRefinement Code to convert a [Refinement] into a [Type] * @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding
* corresponding to the callee's context. This is used in order to handle secondary * to the callee's context. This is used in order to handle secondary times that are
* times that are actually [Refinement]s. * actually [Refinement]s.
*/ */
private inline fun parseSecondaryTypeImpl( private inline fun parseSecondaryTypeImpl(
type: String?, type: String?,
@ -922,14 +906,13 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
} }
/** /**
* Raw information about an [Album] obtained from the component [Song] instances. * Raw information about an [Album] obtained from the component [Song] instances. **This is only
* **This is only meant for use within the music package.** * meant for use within the music package.**
*/ */
class Raw( class Raw(
/** /**
* The ID of the [Album]'s grouping, obtained from MediaStore. Note that this * The ID of the [Album]'s grouping, obtained from MediaStore. Note that this ID is highly
* ID is highly unstable and should only be used for accessing the system-provided * unstable and should only be used for accessing the system-provided cover art.
* cover art.
*/ */
val mediaStoreId: Long, val mediaStoreId: Long,
/** @see Music.uid */ /** @see Music.uid */
@ -970,12 +953,12 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
} }
/** /**
* An abstract artist. These are actually a combination of the artist and album artist tags * An abstract artist. These are actually a combination of the artist and album artist tags from
* from within the library, derived from [Song]s and [Album]s respectively. * within the library, derived from [Song]s and [Album]s respectively.
* @param raw The [Artist.Raw] to derive the member data from. * @param raw The [Artist.Raw] to derive the member data from.
* @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist], * @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist], either
* either through artist or album artist tags. Providing [Song]s to the artist is optional. * through artist or album artist tags. Providing [Song]s to the artist is optional. These instances
* These instances will be linked to this [Artist]. * will be linked to this [Artist].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() { class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
@ -990,21 +973,21 @@ class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicP
override val songs: List<Song> override val songs: List<Song>
/** /**
* All of the [Album]s this artist is credited to. Note that any [Song] credited to this * All of the [Album]s this artist is credited to. Note that any [Song] credited to this artist
* artist will have it's [Album] considered to be "indirectly" linked to this [Artist], and * will have it's [Album] considered to be "indirectly" linked to this [Artist], and thus
* thus included in this list. * included in this list.
*/ */
val albums: List<Album> val albums: List<Album>
/** /**
* The duration of all [Song]s in the artist, in milliseconds. * The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no
* Will be null if there are no songs. * songs.
*/ */
val durationMs: Long? val durationMs: Long?
/** /**
* Whether this artist is considered a "collaborator", i.e it is not directly credited on * Whether this artist is considered a "collaborator", i.e it is not directly credited on any
* any [Album]. * [Album].
*/ */
val isCollaborator: Boolean val isCollaborator: Boolean
@ -1045,8 +1028,8 @@ class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicP
fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) } fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) }
/** /**
* Checks if the [Genre] *display* of this [Artist] and another [Artist] are equal. This * Checks if the [Genre] *display* of this [Artist] and another [Artist] are equal. This will
* will only compare surface-level names, and not [Music.UID]s. * only compare surface-level names, and not [Music.UID]s.
* @param other The [Artist] to compare to. * @param other The [Artist] to compare to.
* @return True if the [Genre] displays are equal, false otherwise * @return True if the [Genre] displays are equal, false otherwise
*/ */
@ -1066,12 +1049,12 @@ class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicP
/** /**
* Returns the original position of this [Artist]'s [Artist.Raw] within the given [Artist.Raw] * Returns the original position of this [Artist]'s [Artist.Raw] within the given [Artist.Raw]
* list. This can be used to create a consistent ordering within child [Artist] lists * list. This can be used to create a consistent ordering within child [Artist] lists based on
* based on the original tag order. * the original tag order.
* @param rawArtists The [Artist.Raw] instances to check. It is assumed that this [Artist]'s * @param rawArtists The [Artist.Raw] instances to check. It is assumed that this [Artist]'s
* [Artist.Raw] will be within the list. * [Artist.Raw] will be within the list.
* @return The index of the [Artist]'s [Artist.Raw] within the list. * @return The index of the [Artist]'s [Artist.Raw] within the list. **This is only meant for
* **This is only meant for use within the music package.** * use within the music package.**
*/ */
fun _getOriginalPositionIn(rawArtists: List<Raw>) = rawArtists.indexOf(raw) fun _getOriginalPositionIn(rawArtists: List<Raw>) = rawArtists.indexOf(raw)
@ -1138,19 +1121,13 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
override val collationKey = makeCollationKeyImpl() override val collationKey = makeCollationKeyImpl()
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre) override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
/** /** The albums indirectly linked to by the [Song]s of this [Genre]. */
* The albums indirectly linked to by the [Song]s of this [Genre].
*/
val albums: List<Album> val albums: List<Album>
/** /** The artists indirectly linked to by the [Artist]s of this [Genre]. */
* The artists indirectly linked to by the [Artist]s of this [Genre].
*/
val artists: List<Artist> val artists: List<Artist>
/** /** The total duration of the songs in this genre, in milliseconds. */
* The total duration of the songs in this genre, in milliseconds.
*/
val durationMs: Long val durationMs: Long
init { init {
@ -1177,12 +1154,12 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
/** /**
* Returns the original position of this [Genre]'s [Genre.Raw] within the given [Genre.Raw] * Returns the original position of this [Genre]'s [Genre.Raw] within the given [Genre.Raw]
* list. This can be used to create a consistent ordering within child [Genre] lists * list. This can be used to create a consistent ordering within child [Genre] lists based on
* based on the original tag order. * the original tag order.
* @param rawGenres The [Genre.Raw] instances to check. It is assumed that this [Genre]'s * @param rawGenres The [Genre.Raw] instances to check. It is assumed that this [Genre]'s
* [Genre.Raw] will be within the list. * [Genre.Raw] will be within the list.
* @return The index of the [Genre]'s [Genre.Raw] within the list. * @return The index of the [Genre]'s [Genre.Raw] within the list. **This is only meant for use
* **This is only meant for use within the music package.** * within the music package.**
*/ */
fun _getOriginalPositionIn(rawGenres: List<Raw>) = rawGenres.indexOf(raw) fun _getOriginalPositionIn(rawGenres: List<Raw>) = rawGenres.indexOf(raw)
@ -1191,13 +1168,11 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
} }
/** /**
* Raw information about a [Genre] obtained from the component [Song] instances. * Raw information about a [Genre] obtained from the component [Song] instances. **This is only
* **This is only meant for use within the music package.** * meant for use within the music package.**
*/ */
class Raw( class Raw(
/** /** @see Music.rawName */
* @see Music.rawName
*/
val name: String? = null val name: String? = null
) { ) {
// Only group by the lowercase genre name. This allows Genre grouping to be // Only group by the lowercase genre name. This allows Genre grouping to be
@ -1219,13 +1194,11 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
} }
} }
/** /**
* An ISO-8601/RFC 3339 Date. * An ISO-8601/RFC 3339 Date.
* *
* This class only encodes the timestamp spec and it's conversion to a human-readable date, * This class only encodes the timestamp spec and it's conversion to a human-readable date, without
* without any other time management or validation. In general, this should only be used for * any other time management or validation. In general, this should only be used for display.
* display.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@ -1240,16 +1213,17 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/** /**
* Resolve this instance into a human-readable date. * Resolve this instance into a human-readable date.
* @param context [Context] required to get human-readable names. * @param context [Context] required to get human-readable names.
* @return If the [Date] has a valid month and year value, a more fine-grained date * @return If the [Date] has a valid month and year value, a more fine-grained date (ex. "Jan
* (ex. "Jan 2020") will be returned. Otherwise, a plain year value (ex. "2020") is * 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will
* returned. Dates will be properly localized. * be properly localized.
*/ */
fun resolveDate(context: Context): String { fun resolveDate(context: Context): String {
if (month != null) { if (month != null) {
// Parse a date format from an ISO-ish format // Parse a date format from an ISO-ish format
val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat) val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
format.applyPattern("yyyy-MM") format.applyPattern("yyyy-MM")
val date = try { val date =
try {
format.parse("$year-$month") format.parse("$year-$month")
} catch (e: ParseException) { } catch (e: ParseException) {
null null
@ -1307,8 +1281,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
companion object { companion object {
/** /**
* A [Regex] that can parse a variable-precision ISO-8601 timestamp. * A [Regex] that can parse a variable-precision ISO-8601 timestamp. Derived from
* Derived from https://github.com/quodlibet/mutagen * https://github.com/quodlibet/mutagen
*/ */
private val ISO8601_REGEX = private val ISO8601_REGEX =
Regex( Regex(
@ -1326,9 +1300,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
* @param year The year component. * @param year The year component.
* @param month The month component. * @param month The month component.
* @param day The day component. * @param day The day component.
* @return A new [Date] consisting of the given components. May have reduced precision * @return A new [Date] consisting of the given components. May have reduced precision if
* if the components were partially invalid, and will be null if all components are * the components were partially invalid, and will be null if all components are invalid.
* invalid.
*/ */
fun from(year: Int, month: Int, day: Int) = fromTokens(listOf(year, month, day)) fun from(year: Int, month: Int, day: Int) = fromTokens(listOf(year, month, day))
@ -1338,9 +1311,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
* @param month The month component. * @param month The month component.
* @param day The day component. * @param day The day component.
* @param hour The hour component * @param hour The hour component
* @return A new [Date] consisting of the given components. May have reduced precision * @return A new [Date] consisting of the given components. May have reduced precision if
* if the components were partially invalid, and will be null if all components are * the components were partially invalid, and will be null if all components are invalid.
* invalid.
*/ */
fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) = fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) =
fromTokens(listOf(year, month, day, hour, minute)) fromTokens(listOf(year, month, day, hour, minute))
@ -1348,9 +1320,9 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/** /**
* Create a [Date] from a [String] timestamp. * Create a [Date] from a [String] timestamp.
* @param timestamp The ISO-8601 timestamp to parse. Can have reduced precision. * @param timestamp The ISO-8601 timestamp to parse. Can have reduced precision.
* @return A new [Date] consisting of the given components. May have reduced precision * @return A new [Date] consisting of the given components. May have reduced precision if
* if the components were partially invalid, and will be null if all components are * the components were partially invalid, and will be null if all components are invalid or
* invalid or if the timestamp is invalid. * if the timestamp is invalid.
*/ */
fun from(timestamp: String): Date? { fun from(timestamp: String): Date? {
val tokens = val tokens =
@ -1365,9 +1337,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/** /**
* Create a [Date] from the given non-validated tokens. * Create a [Date] from the given non-validated tokens.
* @param tokens The tokens to use for each date component, in order of precision. * @param tokens The tokens to use for each date component, in order of precision.
* @return A new [Date] consisting of the given components. May have reduced precision * @return A new [Date] consisting of the given components. May have reduced precision if
* if the components were partially invalid, and will be null if all components are * the components were partially invalid, and will be null if all components are invalid.
* invalid.
*/ */
private fun fromTokens(tokens: List<Int>): Date? { private fun fromTokens(tokens: List<Int>): Date? {
val validated = mutableListOf<Int>() val validated = mutableListOf<Int>()
@ -1380,8 +1351,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
} }
/** /**
* Validate a list of tokens provided by [src], and add the valid ones to [dst]. * Validate a list of tokens provided by [src], and add the valid ones to [dst]. Will stop
* Will stop as soon as an invalid token is found. * as soon as an invalid token is found.
* @param src The input tokens to validate. * @param src The input tokens to validate.
* @param dst The destination list to add valid tokens to. * @param dst The destination list to add valid tokens to.
*/ */
@ -1440,8 +1411,8 @@ private fun MessageDigest.update(n: Int?) {
/** /**
* Convert a [ByteArray] to a [UUID]. Assumes that the [ByteArray] has a length of 16. * Convert a [ByteArray] to a [UUID]. Assumes that the [ByteArray] has a length of 16.
* @return A [UUID] derived from the [ByteArray]'s contents. Internally, the two [Long]s * @return A [UUID] derived from the [ByteArray]'s contents. Internally, the two [Long]s in the
* in the [UUID] will be little-endian. * [UUID] will be little-endian.
*/ */
fun ByteArray.toUuid(): UUID { fun ByteArray.toUuid(): UUID {
check(size == 16) check(size == 16)

View file

@ -20,15 +20,15 @@ package org.oxycblt.auxio.music
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import org.oxycblt.auxio.music.storage.useQuery
import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.music.storage.useQuery
/** /**
* A repository granting access to the music library.. * A repository granting access to the music library..
* *
* This can be used to obtain certain music items, or await changes to the music library. * This can be used to obtain certain music items, or await changes to the music library. It is
* It is generally recommended to use this over Indexer to keep track of the library state, * generally recommended to use this over Indexer to keep track of the library state, as the
* as the interface will be less volatile. * interface will be less volatile.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@ -36,9 +36,9 @@ class MusicStore private constructor() {
private val callbacks = mutableListOf<Callback>() private val callbacks = mutableListOf<Callback>()
/** /**
* The current [Library]. May be null if a [Library] has not been successfully loaded yet. * The current [Library]. May be null if a [Library] has not been successfully loaded yet. This
* This can change, so it's highly recommended to not access this directly and instead * can change, so it's highly recommended to not access this directly and instead rely on
* rely on [Callback]. * [Callback].
*/ */
var library: Library? = null var library: Library? = null
set(value) { set(value) {
@ -49,9 +49,8 @@ class MusicStore private constructor() {
} }
/** /**
* Add a [Callback] to this instance. This can be used to receive changes in the music * Add a [Callback] to this instance. This can be used to receive changes in the music library.
* library. Will invoke all [Callback] methods to initialize the instance with the * Will invoke all [Callback] methods to initialize the instance with the current state.
* current state.
* @param callback The [Callback] to add. * @param callback The [Callback] to add.
* @see Callback * @see Callback
*/ */
@ -62,10 +61,9 @@ class MusicStore private constructor() {
} }
/** /**
* Remove a [Callback] from this instance, preventing it from recieving any further * Remove a [Callback] from this instance, preventing it from recieving any further updates.
* updates. * @param callback The [Callback] to remove. Does nothing if the [Callback] was never added in
* @param callback The [Callback] to remove. Does nothing if the [Callback] was never * the first place.
* added in the first place.
* @see Callback * @see Callback
*/ */
@Synchronized @Synchronized
@ -116,8 +114,8 @@ class MusicStore private constructor() {
/** /**
* Finds a [Music] item [T] in the library by it's [Music.UID]. * Finds a [Music] item [T] in the library by it's [Music.UID].
* @param uid The [Music.UID] to search for. * @param uid The [Music.UID] to search for.
* @return The [T] corresponding to the given [Music.UID], or null if nothing could be * @return The [T] corresponding to the given [Music.UID], or null if nothing could be found
* found or the [Music.UID] did not correspond to a [T]. * or the [Music.UID] did not correspond to a [T].
*/ */
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T @Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T

View file

@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.system.Indexer import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.util.logD
/** /**
* A [ViewModel] providing data specific to the music loading process. * A [ViewModel] providing data specific to the music loading process.

View file

@ -281,11 +281,13 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getSongComparator(isAscending: Boolean): Comparator<Song> = override fun getSongComparator(isAscending: Boolean): Comparator<Song> =
MultiComparator( MultiComparator(
compareByDynamic(isAscending) { it.durationMs }, compareBy(BasicComparator.SONG)) compareByDynamic(isAscending) { it.durationMs },
compareBy(BasicComparator.SONG))
override fun getAlbumComparator(isAscending: Boolean): Comparator<Album> = override fun getAlbumComparator(isAscending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(isAscending) { it.durationMs }, compareBy(BasicComparator.ALBUM)) compareByDynamic(isAscending) { it.durationMs },
compareBy(BasicComparator.ALBUM))
override fun getArtistComparator(isAscending: Boolean): Comparator<Artist> = override fun getArtistComparator(isAscending: Boolean): Comparator<Artist> =
MultiComparator( MultiComparator(
@ -294,7 +296,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getGenreComparator(isAscending: Boolean): Comparator<Genre> = override fun getGenreComparator(isAscending: Boolean): Comparator<Genre> =
MultiComparator( MultiComparator(
compareByDynamic(isAscending) { it.durationMs }, compareBy(BasicComparator.GENRE)) compareByDynamic(isAscending) { it.durationMs },
compareBy(BasicComparator.GENRE))
} }
/** /**
@ -310,7 +313,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getAlbumComparator(isAscending: Boolean): Comparator<Album> = override fun getAlbumComparator(isAscending: Boolean): Comparator<Album> =
MultiComparator( MultiComparator(
compareByDynamic(isAscending) { it.songs.size }, compareBy(BasicComparator.ALBUM)) compareByDynamic(isAscending) { it.songs.size },
compareBy(BasicComparator.ALBUM))
override fun getArtistComparator(isAscending: Boolean): Comparator<Artist> = override fun getArtistComparator(isAscending: Boolean): Comparator<Artist> =
MultiComparator( MultiComparator(
@ -319,7 +323,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
override fun getGenreComparator(isAscending: Boolean): Comparator<Genre> = override fun getGenreComparator(isAscending: Boolean): Comparator<Genre> =
MultiComparator( MultiComparator(
compareByDynamic(isAscending) { it.songs.size }, compareBy(BasicComparator.GENRE)) compareByDynamic(isAscending) { it.songs.size },
compareBy(BasicComparator.GENRE))
} }
/** /**
@ -430,8 +435,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
} }
/** /**
* Utility function to create a [Comparator] that sorts in ascending order based on * Utility function to create a [Comparator] that sorts in ascending order based on the
* the given [Comparator], with a selector based on the item itself. * given [Comparator], with a selector based on the item itself.
* @param comparator The [Comparator] to wrap. * @param comparator The [Comparator] to wrap.
* @return A new [Comparator] with the specified configuration. * @return A new [Comparator] with the specified configuration.
* @see compareBy * @see compareBy
@ -440,11 +445,9 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
compareBy(comparator) { it } compareBy(comparator) { it }
/** /**
* A [Comparator] that chains several other [Comparator]s together to form one * A [Comparator] that chains several other [Comparator]s together to form one comparison.
* comparison. * @param comparators The [Comparator]s to chain. These will be iterated through in order
* @param comparators The [Comparator]s to chain. These will be iterated through * during a comparison, with the first non-equal result becoming the result.
* in order during a comparison, with the first non-equal result becoming the
* result.
*/ */
private class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> { private class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> {
private val _comparators = comparators private val _comparators = comparators
@ -493,8 +496,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
} }
/** /**
* A [Comparator] that compares abstract [Music] values. Internally, this is similar * A [Comparator] that compares abstract [Music] values. Internally, this is similar to
* to [NullableComparator], however comparing [Music.collationKey] instead of [Comparable]. * [NullableComparator], however comparing [Music.collationKey] instead of [Comparable].
* @see NullableComparator * @see NullableComparator
* @see Music.collationKey * @see Music.collationKey
*/ */
@ -523,8 +526,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
} }
/** /**
* A [Comparator] that compares two possibly null values. Values will be considered * A [Comparator] that compares two possibly null values. Values will be considered lesser
* lesser if they are null, and greater if they are non-null. * if they are null, and greater if they are non-null.
*/ */
private class NullableComparator<T : Comparable<T>> private constructor() : Comparator<T?> { private class NullableComparator<T : Comparable<T>> private constructor() : Comparator<T?> {
override fun compare(a: T?, b: T?) = override fun compare(a: T?, b: T?) =

View file

@ -38,22 +38,20 @@ import org.oxycblt.auxio.util.requireBackgroundThread
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
interface CacheExtractor { interface CacheExtractor {
/** /** Initialize the Extractor by reading the cache data into memory. */
* Initialize the Extractor by reading the cache data into memory.
*/
fun init() fun init()
/** /**
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside
* alongside freeing up memory. * freeing up memory.
* @param rawSongs The songs to write into the cache. * @param rawSongs The songs to write into the cache.
*/ */
fun finalize(rawSongs: List<Song.Raw>) fun finalize(rawSongs: List<Song.Raw>)
/** /**
* Use the cache to populate the given [Song.Raw]. * Use the cache to populate the given [Song.Raw].
* @param rawSong The [Song.Raw] to attempt to populate. Note that this [Song.Raw] will * @param rawSong The [Song.Raw] to attempt to populate. Note that this [Song.Raw] will only
* only contain the bare minimum information required to load a cache entry. * contain the bare minimum information required to load a cache entry.
* @return An [ExtractionResult] representing the result of the operation. * @return An [ExtractionResult] representing the result of the operation.
* [ExtractionResult.PARSED] is not returned. * [ExtractionResult.PARSED] is not returned.
*/ */
@ -61,8 +59,8 @@ interface CacheExtractor {
} }
/** /**
* A [CacheExtractor] only capable of writing to the cache. This can be used to load music * A [CacheExtractor] only capable of writing to the cache. This can be used to load music with
* with without the cache if the user desires. * without the cache if the user desires.
* @param context [Context] required to read the cache database. * @param context [Context] required to read the cache database.
* @see CacheExtractor * @see CacheExtractor
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
@ -120,7 +118,8 @@ class ReadWriteCacheExtractor(private val context: Context) : WriteOnlyCacheExtr
} }
override fun populate(rawSong: Song.Raw): ExtractionResult { override fun populate(rawSong: Song.Raw): ExtractionResult {
val map = requireNotNull(cacheMap) { val map =
requireNotNull(cacheMap) {
"Must initialize this extractor before populating a raw song." "Must initialize this extractor before populating a raw song."
} }
@ -228,8 +227,8 @@ private class CacheDatabase(context: Context) :
/** /**
* Read out this database into memory. * Read out this database into memory.
* @return A mapping between the MediaStore IDs of the cache entries and a [Song.Raw] containing * @return A mapping between the MediaStore IDs of the cache entries and a [Song.Raw] containing
* the cacheable data for the entry. Note that any filesystem-dependent information * the cacheable data for the entry. Note that any filesystem-dependent information (excluding
* (excluding IDs and timestamps) is not cached. * IDs and timestamps) is not cached.
*/ */
fun read(): Map<Long, Song.Raw> { fun read(): Map<Long, Song.Raw> {
requireBackgroundThread() requireBackgroundThread()
@ -323,7 +322,9 @@ private class CacheDatabase(context: Context) :
raw.albumArtistSortNames = it.parseSQLMultiValue() raw.albumArtistSortNames = it.parseSQLMultiValue()
} }
cursor.getStringOrNull(genresIndex)?.let { raw.genreNames = it.parseSQLMultiValue() } cursor.getStringOrNull(genresIndex)?.let {
raw.genreNames = it.parseSQLMultiValue()
}
map[id] = raw map[id] = raw
} }
@ -376,20 +377,22 @@ private class CacheDatabase(context: Context) :
put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId) put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId)
put(Columns.ALBUM_NAME, rawSong.albumName) put(Columns.ALBUM_NAME, rawSong.albumName)
put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName) put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName)
put( put(Columns.ALBUM_TYPES, rawSong.albumTypes.toSQLMultiValue())
Columns.ALBUM_TYPES,
rawSong.albumTypes.toSQLMultiValue())
put( put(
Columns.ARTIST_MUSIC_BRAINZ_IDS, Columns.ARTIST_MUSIC_BRAINZ_IDS,
rawSong.artistMusicBrainzIds.toSQLMultiValue()) rawSong.artistMusicBrainzIds.toSQLMultiValue())
put(Columns.ARTIST_NAMES, rawSong.artistNames.toSQLMultiValue()) put(Columns.ARTIST_NAMES, rawSong.artistNames.toSQLMultiValue())
put(Columns.ARTIST_SORT_NAMES, rawSong.artistSortNames.toSQLMultiValue()) put(
Columns.ARTIST_SORT_NAMES,
rawSong.artistSortNames.toSQLMultiValue())
put( put(
Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS, Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS,
rawSong.albumArtistMusicBrainzIds.toSQLMultiValue()) rawSong.albumArtistMusicBrainzIds.toSQLMultiValue())
put(Columns.ALBUM_ARTIST_NAMES, rawSong.albumArtistNames.toSQLMultiValue()) put(
Columns.ALBUM_ARTIST_NAMES,
rawSong.albumArtistNames.toSQLMultiValue())
put( put(
Columns.ALBUM_ARTIST_SORT_NAMES, Columns.ALBUM_ARTIST_SORT_NAMES,
rawSong.albumArtistSortNames.toSQLMultiValue()) rawSong.albumArtistSortNames.toSQLMultiValue())
@ -416,8 +419,8 @@ private class CacheDatabase(context: Context) :
/** /**
* Transforms the multi-string list into a SQL-safe multi-string value. * Transforms the multi-string list into a SQL-safe multi-string value.
* @return A single string containing all values within the multi-string list, delimited * @return A single string containing all values within the multi-string list, delimited by a
* by a ";". Pre-existing ";" characters will be escaped. * ";". Pre-existing ";" characters will be escaped.
*/ */
private fun List<String>.toSQLMultiValue() = private fun List<String>.toSQLMultiValue() =
if (isNotEmpty()) { if (isNotEmpty()) {
@ -428,14 +431,12 @@ private class CacheDatabase(context: Context) :
/** /**
* Transforms the SQL-safe multi-string value into a multi-string list. * Transforms the SQL-safe multi-string value into a multi-string list.
* @return A list of strings corresponding to the delimited values present within the * @return A list of strings corresponding to the delimited values present within the original
* original string. Escaped delimiters are converted back into their normal forms. * string. Escaped delimiters are converted back into their normal forms.
*/ */
private fun String.parseSQLMultiValue() = splitEscaped { it == ';' } private fun String.parseSQLMultiValue() = splitEscaped { it == ';' }
/** /** Defines the columns used in this database. */
* Defines the columns used in this database.
*/
private object Columns { private object Columns {
/** @see Song.Raw.mediaStoreId */ /** @see Song.Raw.mediaStoreId */
const val MEDIA_STORE_ID = "msid" const val MEDIA_STORE_ID = "msid"

View file

@ -1,3 +1,20 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.extractor package org.oxycblt.auxio.music.extractor
/** /**
@ -5,18 +22,12 @@ package org.oxycblt.auxio.music.extractor
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
enum class ExtractionResult { enum class ExtractionResult {
/** /** A raw song was successfully extracted from the cache. */
* A raw song was successfully extracted from the cache.
*/
CACHED, CACHED,
/** /** A raw song was successfully extracted from parsing it's file. */
* A raw song was successfully extracted from parsing it's file.
*/
PARSED, PARSED,
/** /** A raw song could not be parsed. */
* A raw song could not be parsed.
*/
NONE NONE
} }

View file

@ -29,21 +29,21 @@ import androidx.core.database.getStringOrNull
import java.io.File import java.io.File
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.storage.Directory import org.oxycblt.auxio.music.storage.Directory
import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.music.storage.directoryCompat import org.oxycblt.auxio.music.storage.directoryCompat
import org.oxycblt.auxio.music.storage.mediaStoreVolumeNameCompat import org.oxycblt.auxio.music.storage.mediaStoreVolumeNameCompat
import org.oxycblt.auxio.music.storage.safeQuery import org.oxycblt.auxio.music.storage.safeQuery
import org.oxycblt.auxio.music.storage.storageVolumesCompat import org.oxycblt.auxio.music.storage.storageVolumesCompat
import org.oxycblt.auxio.music.storage.useQuery import org.oxycblt.auxio.music.storage.useQuery
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
* The layer that loads music from the [MediaStore] database. This is an intermediate step in the * The layer that loads music from the [MediaStore] database. This is an intermediate step in the
* music extraction process and primarily intended for redundancy for files not natively * music extraction process and primarily intended for redundancy for files not natively supported
* supported by [MetadataExtractor]. Solely relying on this is not recommended, as it often * by [MetadataExtractor]. Solely relying on this is not recommended, as it often produces bad
* produces bad metadata. * metadata.
* @param context [Context] required to query the media database. * @param context [Context] required to query the media database.
* @param cacheExtractor [CacheExtractor] implementation for cache optimizations. * @param cacheExtractor [CacheExtractor] implementation for cache optimizations.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
@ -69,15 +69,15 @@ abstract class MediaStoreExtractor(
private val genreNamesMap = mutableMapOf<Long, String>() private val genreNamesMap = mutableMapOf<Long, String>()
/** /**
* The [StorageVolume]s currently scanned by [MediaStore]. This should be used to transform * The [StorageVolume]s currently scanned by [MediaStore]. This should be used to transform path
* path information from the database into volume-aware paths. * information from the database into volume-aware paths.
*/ */
protected var volumes = listOf<StorageVolume>() protected var volumes = listOf<StorageVolume>()
private set private set
/** /**
* Initialize this instance. This involves setting up the required sub-extractors and * Initialize this instance. This involves setting up the required sub-extractors and querying
* querying the media database for music files. * the media database for music files.
* @return A [Cursor] of the music data returned from the database. * @return A [Cursor] of the music data returned from the database.
*/ */
open fun init(): Cursor { open fun init(): Cursor {
@ -124,11 +124,14 @@ abstract class MediaStoreExtractor(
// Now we can actually query MediaStore. // Now we can actually query MediaStore.
logD("Starting song query [proj: ${projection.toList()}, selector: $selector, args: $args]") logD("Starting song query [proj: ${projection.toList()}, selector: $selector, args: $args]")
val cursor = context.contentResolverSafe.safeQuery( val cursor =
context.contentResolverSafe
.safeQuery(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection, projection,
selector, selector,
args.toTypedArray()).also { cursor = it } args.toTypedArray())
.also { cursor = it }
logD("Song query succeeded [Projected total: ${cursor.count}]") logD("Song query succeeded [Projected total: ${cursor.count}]")
// Set up cursor indices for later use. // Set up cursor indices for later use.
@ -184,8 +187,8 @@ abstract class MediaStoreExtractor(
} }
/** /**
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside
* alongside freeing up memory. * freeing up memory.
* @param rawSongs The songs to write into the cache. * @param rawSongs The songs to write into the cache.
*/ */
fun finalize(rawSongs: List<Song.Raw>) { fun finalize(rawSongs: List<Song.Raw>) {
@ -222,8 +225,8 @@ abstract class MediaStoreExtractor(
} }
/** /**
* The database columns available to all android versions supported by Auxio. * The database columns available to all android versions supported by Auxio. Concrete
* Concrete implementations can extend this projection to add version-specific columns. * implementations can extend this projection to add version-specific columns.
*/ */
protected open val projection: Array<String> protected open val projection: Array<String>
get() = get() =
@ -244,8 +247,8 @@ abstract class MediaStoreExtractor(
AUDIO_COLUMN_ALBUM_ARTIST) AUDIO_COLUMN_ALBUM_ARTIST)
/** /**
* The companion template to add to the projection's selector whenever arguments are added * The companion template to add to the projection's selector whenever arguments are added by
* by [addDirToSelector]. * [addDirToSelector].
* @see addDirToSelector * @see addDirToSelector
*/ */
protected abstract val dirSelectorTemplate: String protected abstract val dirSelectorTemplate: String
@ -253,8 +256,8 @@ abstract class MediaStoreExtractor(
/** /**
* Add a [Directory] to the given list of projection selector arguments. * Add a [Directory] to the given list of projection selector arguments.
* @param dir The [Directory] to add. * @param dir The [Directory] to add.
* @param args The destination list to append selector arguments to that are analogous * @param args The destination list to append selector arguments to that are analogous to the
* to the given [Directory]. * given [Directory].
* @return true if the [Directory] was added, false otherwise. * @return true if the [Directory] was added, false otherwise.
* @see dirSelectorTemplate * @see dirSelectorTemplate
*/ */
@ -263,8 +266,8 @@ abstract class MediaStoreExtractor(
/** /**
* Populate a [Song.Raw] with the "File Data" of the given [MediaStore] [Cursor], which is the * Populate a [Song.Raw] with the "File Data" of the given [MediaStore] [Cursor], which is the
* data that cannot be cached. This includes any information not intrinsic to the file and * data that cannot be cached. This includes any information not intrinsic to the file and
* instead dependent on the file-system, which could change without invalidating the cache * instead dependent on the file-system, which could change without invalidating the cache due
* due to volume additions or removals. * to volume additions or removals.
* @param cursor The [Cursor] to read from. * @param cursor The [Cursor] to read from.
* @param raw The [Song.Raw] to populate. * @param raw The [Song.Raw] to populate.
* @see populateMetadata * @see populateMetadata
@ -281,9 +284,9 @@ abstract class MediaStoreExtractor(
} }
/** /**
* Populate a [Song.Raw] with the Metadata of the given [MediaStore] [Cursor], which is the * Populate a [Song.Raw] with the Metadata of the given [MediaStore] [Cursor], which is the data
* data about a [Song.Raw] that can be cached. This includes any information intrinsic to * about a [Song.Raw] that can be cached. This includes any information intrinsic to the file or
* the file or it's file format, such as music tags. * it's file format, such as music tags.
* @param cursor The [Cursor] to read from. * @param cursor The [Cursor] to read from.
* @param raw The [Song.Raw] to populate. * @param raw The [Song.Raw] to populate.
* @see populateFileData * @see populateFileData
@ -334,8 +337,8 @@ abstract class MediaStoreExtractor(
private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST
/** /**
* The external volume. This naming has existed since API 21, but no constant existed * The external volume. This naming has existed since API 21, but no constant existed for it
* for it until API 29. This will work on all versions that Auxio supports. * until API 29. This will work on all versions that Auxio supports.
*/ */
@Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL @Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
} }
@ -367,7 +370,8 @@ class Api21MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor)
override val projection: Array<String> override val projection: Array<String>
get() = get() =
super.projection + super.projection +
arrayOf(MediaStore.Audio.AudioColumns.TRACK, arrayOf(
MediaStore.Audio.AudioColumns.TRACK,
// Below API 29, we are restricted to the absolute path (Called DATA by // Below API 29, we are restricted to the absolute path (Called DATA by
// MedaStore) when working with audio files. // MedaStore) when working with audio files.
MediaStore.Audio.AudioColumns.DATA) MediaStore.Audio.AudioColumns.DATA)
@ -486,8 +490,8 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheExtractor: CacheE
} }
/** /**
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at * A [MediaStoreExtractor] that completes the music loading process in a way compatible with at API
* API 29. * 29.
* @param context [Context] required to query the media database. * @param context [Context] required to query the media database.
* @param cacheExtractor [CacheExtractor] implementation for cache functionality. * @param cacheExtractor [CacheExtractor] implementation for cache functionality.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
@ -521,8 +525,8 @@ open class Api29MediaStoreExtractor(context: Context, cacheExtractor: CacheExtra
} }
/** /**
* A [MediaStoreExtractor] that completes the music loading process in a way compatible from * A [MediaStoreExtractor] that completes the music loading process in a way compatible from API 30
* API 30 onwards. * onwards.
* @param context [Context] required to query the media database. * @param context [Context] required to query the media database.
* @param cacheExtractor [CacheExtractor] implementation for cache optimizations. * @param cacheExtractor [CacheExtractor] implementation for cache optimizations.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)

View file

@ -32,8 +32,8 @@ import org.oxycblt.auxio.util.logW
/** /**
* The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the * The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the
* last step in the music extraction process and is mostly responsible for papering over the * last step in the music extraction process and is mostly responsible for papering over the bad
* bad metadata that [MediaStoreExtractor] produces. * metadata that [MediaStoreExtractor] produces.
* *
* @param context [Context] required for reading audio files. * @param context [Context] required for reading audio files.
* @param mediaStoreExtractor [MediaStoreExtractor] implementation for cache optimizations and * @param mediaStoreExtractor [MediaStoreExtractor] implementation for cache optimizations and
@ -56,17 +56,17 @@ class MetadataExtractor(
fun init() = mediaStoreExtractor.init().count fun init() = mediaStoreExtractor.init().count
/** /**
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside
* alongside freeing up memory. * freeing up memory.
* @param rawSongs The songs to write into the cache. * @param rawSongs The songs to write into the cache.
*/ */
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreExtractor.finalize(rawSongs) fun finalize(rawSongs: List<Song.Raw>) = mediaStoreExtractor.finalize(rawSongs)
/** /**
* Parse all [Song.Raw] instances queued by the sub-extractors. This will first delegate * Parse all [Song.Raw] instances queued by the sub-extractors. This will first delegate to the
* to the sub-extractors before parsing the metadata itself. * sub-extractors before parsing the metadata itself.
* @param emit A callback that will be invoked with every new [Song.Raw] instance when * @param emit A callback that will be invoked with every new [Song.Raw] instance when they are
* they are successfully loaded. * successfully loaded.
*/ */
suspend fun parse(emit: suspend (Song.Raw) -> Unit) { suspend fun parse(emit: suspend (Song.Raw) -> Unit) {
while (true) { while (true) {
@ -122,8 +122,8 @@ class MetadataExtractor(
} }
/** /**
* Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed. * Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed. TODO:
* TODO: Re-unify with MetadataExtractor. * Re-unify with MetadataExtractor.
* @param context [Context] required to open the audio file. * @param context [Context] required to open the audio file.
* @param raw [Song.Raw] to process. * @param raw [Song.Raw] to process.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
@ -135,7 +135,8 @@ class Task(context: Context, private val raw: Song.Raw) {
private val future = private val future =
MetadataRetriever.retrieveMetadata( MetadataRetriever.retrieveMetadata(
context, context,
MediaItem.fromUri(requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri())) MediaItem.fromUri(
requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()))
/** /**
* Try to get a completed song from this [Task], if it has finished processing. * Try to get a completed song from this [Task], if it has finished processing.
@ -246,14 +247,17 @@ class Task(context: Context, private val raw: Song.Raw) {
// 5. ID3v2.3 Release Year, as it is the most common date type // 5. ID3v2.3 Release Year, as it is the most common date type
(textFrames["TDOR"]?.run { get(0).parseTimestamp() } (textFrames["TDOR"]?.run { get(0).parseTimestamp() }
?: textFrames["TDRC"]?.run { get(0).parseTimestamp() } ?: textFrames["TDRC"]?.run { get(0).parseTimestamp() }
?: textFrames["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(textFrames)) ?: textFrames["TDRL"]?.run { get(0).parseTimestamp() }
?: parseId3v23Date(textFrames))
?.let { raw.date = it } ?.let { raw.date = it }
// Album // Album
textFrames["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] } textFrames["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] }
textFrames["TALB"]?.let { raw.albumName = it[0] } textFrames["TALB"]?.let { raw.albumName = it[0] }
textFrames["TSOA"]?.let { raw.albumSortName = it[0] } textFrames["TSOA"]?.let { raw.albumSortName = it[0] }
(textFrames["TXXX:MusicBrainz Album Type"] ?: textFrames["GRP1"])?.let { raw.albumTypes = it } (textFrames["TXXX:MusicBrainz Album Type"] ?: textFrames["GRP1"])?.let {
raw.albumTypes = it
}
// Artist // Artist
textFrames["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it } textFrames["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it }
@ -274,9 +278,9 @@ class Task(context: Context, private val raw: Song.Raw) {
* Frames. * Frames.
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more * @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
* values. * values.
* @retrn A [Date] of a year value from TORY/TYER, a month and day value from TDAT, * @retrn A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a
* and a hour/minute value from TIME. No second value is included. The latter two fields may * hour/minute value from TIME. No second value is included. The latter two fields may not be
* not be included in they cannot be parsed. Will be null if a year value could not be parsed. * included in they cannot be parsed. Will be null if a year value could not be parsed.
*/ */
private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? { private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? {
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY // Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY
@ -313,8 +317,7 @@ class Task(context: Context, private val raw: Song.Raw) {
/** /**
* Complete this instance's [Song.Raw] with Vorbis comments. * Complete this instance's [Song.Raw] with Vorbis comments.
* @param comments A mapping between vorbis comment names and one or more vorbis comment * @param comments A mapping between vorbis comment names and one or more vorbis comment values.
* values.
*/ */
private fun populateWithVorbis(comments: Map<String, List<String>>) { private fun populateWithVorbis(comments: Map<String, List<String>>) {
// Song // Song
@ -363,8 +366,8 @@ class Task(context: Context, private val raw: Song.Raw) {
/** /**
* Copies and sanitizes a possibly native/non-UTF-8 string. * Copies and sanitizes a possibly native/non-UTF-8 string.
* @return A new string allocated in a memory-safe manner with any UTF-8 errors * @return A new string allocated in a memory-safe manner with any UTF-8 errors replaced with
* replaced with the Unicode replacement byte sequence. * the Unicode replacement byte sequence.
*/ */
private fun String.sanitize() = String(encodeToByteArray()) private fun String.sanitize() = String(encodeToByteArray())
} }

View file

@ -24,27 +24,25 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
/** /**
* Unpack the track number from a combined track + disc [Int] field. * Unpack the track number from a combined track + disc [Int] field. These fields appear within
* These fields appear within MediaStore's TRACK column, and combine the track and disc value * MediaStore's TRACK column, and combine the track and disc value into a single field where the
* into a single field where the disc number is the 4th+ digit. * disc number is the 4th+ digit.
* @return The track number extracted from the combined integer value, or null if the value * @return The track number extracted from the combined integer value, or null if the value was
* was zero. * zero.
*/ */
fun Int.unpackTrackNo() = mod(1000).nonZeroOrNull() fun Int.unpackTrackNo() = mod(1000).nonZeroOrNull()
/** /**
* Unpack the disc number from a combined track + disc [Int] field. * Unpack the disc number from a combined track + disc [Int] field. These fields appear within
* These fields appear within MediaStore's TRACK column, and combine the track and disc value * MediaStore's TRACK column, and combine the track and disc value into a single field where the
* into a single field where the disc number is the 4th+ digit. * disc number is the 4th+ digit.
* @return The disc number extracted from the combined integer field, or null if the value * @return The disc number extracted from the combined integer field, or null if the value was zero.
* was zero.
*/ */
fun Int.unpackDiscNo() = div(1000).nonZeroOrNull() fun Int.unpackDiscNo() = div(1000).nonZeroOrNull()
/** /**
* Parse the number out of a combined number + total position [String] field. * Parse the number out of a combined number + total position [String] field. These fields often
* These fields often appear in ID3v2 files, and consist of a number and an (optional) total * appear in ID3v2 files, and consist of a number and an (optional) total value delimited by a /.
* value delimited by a /.
* @return The number value extracted from the string field, or null if the value could not be * @return The number value extracted from the string field, or null if the value could not be
* parsed or if the value was zero. * parsed or if the value was zero.
*/ */
@ -59,24 +57,23 @@ fun Int.toDate() = Date.from(this)
/** /**
* Parse an integer year field from a [String] and transform it into a [Date]. * Parse an integer year field from a [String] and transform it into a [Date].
* @return A [Date] consisting of the year value, or null if the value could not * @return A [Date] consisting of the year value, or null if the value could not be parsed or if the
* be parsed or if the value was zero. * value was zero.
* @see Date.from * @see Date.from
*/ */
fun String.parseYear() = toIntOrNull()?.toDate() fun String.parseYear() = toIntOrNull()?.toDate()
/** /**
* Parse an ISO-8601 timestamp [String] into a [Date]. * Parse an ISO-8601 timestamp [String] into a [Date].
* @return A [Date] consisting of the year value plus one or more refinement values * @return A [Date] consisting of the year value plus one or more refinement values (ex. month,
* (ex. month, day), or null if the timestamp was not valid. * day), or null if the timestamp was not valid.
*/ */
fun String.parseTimestamp() = Date.from(this) fun String.parseTimestamp() = Date.from(this)
/** /**
* Split a [String] by the given selector, automatically handling escaped characters * Split a [String] by the given selector, automatically handling escaped characters that satisfy
* that satisfy the selector. * the selector.
* @param selector A block that determines if the string should be split at a given * @param selector A block that determines if the string should be split at a given character.
* character.
* @return One or more [String]s split by the selector. * @return One or more [String]s split by the selector.
*/ */
inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String> { inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String> {
@ -118,9 +115,9 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String>
} }
/** /**
* Parse a multi-value tag based on the user configuration. If the value is already composed of * Parse a multi-value tag based on the user configuration. If the value is already composed of more
* more than one value, nothing is done. Otherwise, this function will attempt to split it based * than one value, nothing is done. Otherwise, this function will attempt to split it based on the
* on the user's separator preferences. * user's separator preferences.
* @param settings [Settings] required to obtain user separator configuration. * @param settings [Settings] required to obtain user separator configuration.
* @return A new list of one or more [String]s. * @return A new list of one or more [String]s.
*/ */
@ -157,8 +154,8 @@ fun String.toUuidOrNull(): UUID? =
/** /**
* Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer * Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer
* representations of genre fields into their named counterparts, and split up singular * representations of genre fields into their named counterparts, and split up singular ID3v2-style
* ID3v2-style integer genre fields into one or more genres. * integer genre fields into one or more genres.
* @param settings [Settings] required to obtain user separator configuration. * @param settings [Settings] required to obtain user separator configuration.
* @return A list of one or more genre names.. * @return A list of one or more genre names..
*/ */
@ -197,16 +194,15 @@ private fun String.parseId3v1Genre(): String? =
} }
/** /**
* A [Regex] that implements parsing for ID3v2's genre format. * A [Regex] that implements parsing for ID3v2's genre format. Derived from mutagen:
* Derived from mutagen: https://github.com/quodlibet/mutagen * https://github.com/quodlibet/mutagen
*/ */
private val ID3V2_GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?") private val ID3V2_GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?")
/** /**
* Parse an ID3v2 integer genre field, which has support for multiple genre values and * Parse an ID3v2 integer genre field, which has support for multiple genre values and combined
* combined named/integer genres. * named/integer genres.
* @return A list of one or more genres, or null if the field is not a valid ID3v2 * @return A list of one or more genres, or null if the field is not a valid ID3v2 integer genre.
* integer genre.
*/ */
private fun String.parseId3v2Genre(): List<String>? { private fun String.parseId3v2Genre(): List<String>? {
val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues

View file

@ -25,13 +25,12 @@ import com.google.android.material.checkbox.MaterialCheckBox
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
/** /**
* A [ViewBindingDialogFragment] that allows the user to configure the separator characters * A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to
* used to split tags with multiple values. * split tags with multiple values. TODO: Add saved state for pending configurations.
* TODO: Add saved state for pending configurations.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() { class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {

View file

@ -57,8 +57,8 @@ class ArtistChoiceAdapter(private val listener: ClickableListListener) :
} }
/** /**
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical * A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [Artist] item, for
* [Artist] item, for use with [ArtistChoiceAdapter]. Use [new] to create an instance. * use with [ArtistChoiceAdapter]. Use [new] to create an instance.
*/ */
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
DialogRecyclerView.ViewHolder(binding.root) { DialogRecyclerView.ViewHolder(binding.root) {

View file

@ -23,7 +23,7 @@ import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.shared.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
/** /**
* An [ArtistPickerDialog] intended for when [Artist] navigation is ambiguous. * An [ArtistPickerDialog] intended for when [Artist] navigation is ambiguous.

View file

@ -27,16 +27,17 @@ import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.shared.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
/** /**
* The base class for dialogs that implements common behavior across all [Artist] pickers. * The base class for dialogs that implements common behavior across all [Artist] pickers. These are
* These are shown whenever what to do with an item's [Artist] is ambiguous, as there are * shown whenever what to do with an item's [Artist] is ambiguous, as there are multiple [Artist]'s
* multiple [Artist]'s to choose from. * to choose from.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener { abstract class ArtistPickerDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener {
protected val pickerModel: PickerViewModel by viewModels() protected val pickerModel: PickerViewModel by viewModels()
// Okay to leak this since the Listener will not be called until after initialization. // Okay to leak this since the Listener will not be called until after initialization.
private val artistAdapter = ArtistChoiceAdapter(@Suppress("LeakingThis") this) private val artistAdapter = ArtistChoiceAdapter(@Suppress("LeakingThis") this)

View file

@ -27,10 +27,9 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* a [ViewModel] that manages the current music picker state. * a [ViewModel] that manages the current music picker state. TODO: This really shouldn't exist.
* TODO: This really shouldn't exist. Make it so that the dialogs just contain the music * Make it so that the dialogs just contain the music themselves and then exit if the library
* themselves and then exit if the library changes. * changes. TODO: While we are at it, let's go and add ClickableSpan too to reduce the extent of
* TODO: While we are at it, let's go and add ClickableSpan too to reduce the extent of
* this dialog. * this dialog.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@ -46,7 +45,8 @@ class PickerViewModel : ViewModel(), MusicStore.Callback {
private val _currentArtists = MutableStateFlow<List<Artist>?>(null) private val _currentArtists = MutableStateFlow<List<Artist>?>(null)
/** /**
* The current [Artist] whose choices are being shown in the picker. Null/Empty if there is none. * The current [Artist] whose choices are being shown in the picker. Null/Empty if there is
* none.
*/ */
val currentArtists: StateFlow<List<Artist>?> val currentArtists: StateFlow<List<Artist>?>
get() = _currentArtists get() = _currentArtists
@ -90,5 +90,4 @@ class PickerViewModel : ViewModel(), MusicStore.Callback {
// Map the UIDs to artist instances and filter out the ones that can't be found. // Map the UIDs to artist instances and filter out the ones that can't be found.
_currentArtists.value = uids.mapNotNull { library.find<Artist>(it) }.ifEmpty { null } _currentArtists.value = uids.mapNotNull { library.find<Artist>(it) }.ifEmpty { null }
} }
} }

View file

@ -30,7 +30,8 @@ import org.oxycblt.auxio.util.inflater
* @param listener A [DirectoryAdapter.Listener] to bind interactions to. * @param listener A [DirectoryAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class DirectoryAdapter(private val listener: Listener) : RecyclerView.Adapter<MusicDirViewHolder>() { class DirectoryAdapter(private val listener: Listener) :
RecyclerView.Adapter<MusicDirViewHolder>() {
private val _dirs = mutableListOf<Directory>() private val _dirs = mutableListOf<Directory>()
/** /**
* The current list of [Directory]s, may not line up with [MusicDirectories] due to removals. * The current list of [Directory]s, may not line up with [MusicDirectories] due to removals.

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -58,8 +58,7 @@ class MusicDirsDialog :
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ -> .setPositiveButton(R.string.lbl_save) { _, _ ->
val dirs = settings.getMusicDirs(storageManager) val dirs = settings.getMusicDirs(storageManager)
val newDirs = val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
if (dirs != newDirs) { if (dirs != newDirs) {
logD("Committing changes") logD("Committing changes")
settings.setMusicDirs(newDirs) settings.setMusicDirs(newDirs)
@ -69,7 +68,8 @@ class MusicDirsDialog :
override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) {
val launcher = val launcher =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs) registerForActivityResult(
ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs)
// Now that the dialog exists, we get the view manually when the dialog is shown // Now that the dialog exists, we get the view manually when the dialog is shown
// and override its click listener so that the dialog does not auto-dismiss when we // and override its click listener so that the dialog does not auto-dismiss when we
@ -95,7 +95,9 @@ class MusicDirsDialog :
if (pendingDirs != null) { if (pendingDirs != null) {
dirs = dirs =
MusicDirectories( MusicDirectories(
pendingDirs.mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) }, pendingDirs.mapNotNull {
Directory.fromDocumentTreeUri(storageManager, it)
},
savedInstanceState.getBoolean(KEY_PENDING_MODE)) savedInstanceState.getBoolean(KEY_PENDING_MODE))
} }
} }
@ -170,9 +172,7 @@ class MusicDirsDialog :
} }
} }
/** /** Get if the UI has currently configured [MusicDirectories.shouldInclude] to be true. */
* Get if the UI has currently configured [MusicDirectories.shouldInclude] to be true.
*/
private fun isUiModeInclude(binding: DialogMusicDirsBinding) = private fun isUiModeInclude(binding: DialogMusicDirsBinding) =
binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include

View file

@ -1,3 +1,20 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.storage package org.oxycblt.auxio.music.storage
import android.content.Context import android.content.Context
@ -5,13 +22,12 @@ import android.os.storage.StorageManager
import android.os.storage.StorageVolume import android.os.storage.StorageVolume
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import org.oxycblt.auxio.R
import java.io.File import java.io.File
import org.oxycblt.auxio.R
/** /**
* A full absolute path to a file. Only intended for display purposes. For accessing files, * A full absolute path to a file. Only intended for display purposes. For accessing files, URIs are
* URIs are preferred in all cases due to scoped storage limitations. * preferred in all cases due to scoped storage limitations.
* @param name The name of the file. * @param name The name of the file.
* @param parent The parent [Directory] of the file. * @param parent The parent [Directory] of the file.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
@ -35,12 +51,12 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
context.getString(R.string.fmt_path, volume.getDescriptionCompat(context), relativePath) context.getString(R.string.fmt_path, volume.getDescriptionCompat(context), relativePath)
/** /**
* Converts this [Directory] instance into an opaque document tree path. * Converts this [Directory] instance into an opaque document tree path. This is a huge
* This is a huge violation of the document tree URI contract, but it's also the only * violation of the document tree URI contract, but it's also the only one can sensibly work
* one can sensibly work with these uris in the UI, and it doesn't exactly matter since * with these uris in the UI, and it doesn't exactly matter since we never write or read
* we never write or read directory. * directory.
* @return A URI [String] abiding by the document tree specification, or null * @return A URI [String] abiding by the document tree specification, or null if the [Directory]
* if the [Directory] is not valid. * is not valid.
*/ */
fun toDocumentTreeUri() = fun toDocumentTreeUri() =
// Document tree URIs consist of a prefixed volume name followed by a relative path. // Document tree URIs consist of a prefixed volume name followed by a relative path.
@ -63,9 +79,7 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
other is Directory && other.volume == volume && other.relativePath == relativePath other is Directory && other.volume == volume && other.relativePath == relativePath
companion object { companion object {
/** /** The name given to the internal volume when in a document tree URI. */
* The name given to the internal volume when in a document tree URI.
*/
private const val DOCUMENT_URI_PRIMARY_NAME = "primary" private const val DOCUMENT_URI_PRIMARY_NAME = "primary"
/** /**
@ -80,10 +94,9 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator)) volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator))
/** /**
* Create a new directory from a document tree URI. * Create a new directory from a document tree URI. This is a huge violation of the document
* This is a huge violation of the document tree URI contract, but it's also the only * tree URI contract, but it's also the only one can sensibly work with these uris in the
* one can sensibly work with these uris in the UI, and it doesn't exactly matter since * UI, and it doesn't exactly matter since we never write or read directory.
* we never write or read directory.
* @param storageManager [StorageManager] in order to obtain the [StorageVolume] specified * @param storageManager [StorageManager] in order to obtain the [StorageVolume] specified
* in the given URI. * in the given URI.
* @param uri The URI string to parse into a [Directory]. * @param uri The URI string to parse into a [Directory].
@ -109,12 +122,12 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
} }
/** /**
* Represents the configuration for specific directories to filter to/from when loading music. * Represents the configuration for specific directories to filter to/from when loading music. TODO:
* TODO: Migrate to a combined "Include + Exclude" system that is more sensible. * Migrate to a combined "Include + Exclude" system that is more sensible.
* @param dirs A list of [Directory] instances. How these are interpreted depends on * @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude]
* [shouldInclude]. * .
* @param shouldInclude True if the library should only load from the [Directory] instances, * @param shouldInclude True if the library should only load from the [Directory] instances, false
* false if the library should not load from the [Directory] instances. * if the library should not load from the [Directory] instances.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolean) data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolean)
@ -122,17 +135,17 @@ data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolea
/** /**
* A mime type of a file. Only intended for display. * A mime type of a file. Only intended for display.
* @param fromExtension The mime type obtained by analyzing the file extension. * @param fromExtension The mime type obtained by analyzing the file extension.
* @param fromFormat The mime type obtained by analyzing the file format. Null if could * @param fromFormat The mime type obtained by analyzing the file format. Null if could not be
* not be obtained. * obtained.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
data class MimeType(val fromExtension: String, val fromFormat: String?) { data class MimeType(val fromExtension: String, val fromFormat: String?) {
/** /**
* Resolve the mime type into a human-readable format name, such as "Ogg Vorbis". * Resolve the mime type into a human-readable format name, such as "Ogg Vorbis".
* @param context [Context] required to obtain human-readable strings. * @param context [Context] required to obtain human-readable strings.
* @return A human-readable name for this mime type. Will first try [fromFormat], * @return A human-readable name for this mime type. Will first try [fromFormat], then falling
* then falling back to [fromExtension], then falling back to the extension name, * back to [fromExtension], then falling back to the extension name, and then finally a
* and then finally a placeholder "No Format" string. * placeholder "No Format" string.
*/ */
fun resolveName(context: Context): String { fun resolveName(context: Context): String {
// We try our best to produce a more readable name for the common audio formats. // We try our best to produce a more readable name for the common audio formats.

View file

@ -34,8 +34,8 @@ import org.oxycblt.auxio.util.lazyReflectedMethod
// --- MEDIASTORE UTILITIES --- // --- MEDIASTORE UTILITIES ---
/** /**
* Get a content resolver that will not mangle MediaStore queries on certain devices. * Get a content resolver that will not mangle MediaStore queries on certain devices. See
* See https://github.com/OxygenCobalt/Auxio/issues/50 for more info. * https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
*/ */
val Context.contentResolverSafe: ContentResolver val Context.contentResolverSafe: ContentResolver
get() = applicationContext.contentResolver get() = applicationContext.contentResolver
@ -44,8 +44,8 @@ val Context.contentResolverSafe: ContentResolver
* A shortcut for querying the [ContentResolver] database. * A shortcut for querying the [ContentResolver] database.
* @param uri The [Uri] of content to retrieve. * @param uri The [Uri] of content to retrieve.
* @param projection A list of SQL columns to query from the database. * @param projection A list of SQL columns to query from the database.
* @param selector A SQL selection statement to filter results. Spaces where * @param selector A SQL selection statement to filter results. Spaces where arguments should be
* arguments should be filled in are represented with a "?". * filled in are represented with a "?".
* @param args The arguments used for the selector. * @param args The arguments used for the selector.
* @return A [Cursor] of the queried values, organized by the column projection. * @return A [Cursor] of the queried values, organized by the column projection.
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
@ -56,20 +56,18 @@ fun ContentResolver.safeQuery(
projection: Array<out String>, projection: Array<out String>,
selector: String? = null, selector: String? = null,
args: Array<String>? = null args: Array<String>? = null
) = requireNotNull(query(uri, projection, selector, args, null)) { ) = requireNotNull(query(uri, projection, selector, args, null)) { "ContentResolver query failed" }
"ContentResolver query failed"
}
/** /**
* A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s * A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s resources
* resources when no longer used. * when no longer used.
* @param uri The [Uri] of content to retrieve. * @param uri The [Uri] of content to retrieve.
* @param projection A list of SQL columns to query from the database. * @param projection A list of SQL columns to query from the database.
* @param selector A SQL selection statement to filter results. Spaces where * @param selector A SQL selection statement to filter results. Spaces where arguments should be
* arguments should be filled in are represented with a "?". * filled in are represented with a "?".
* @param args The arguments used for the selector. * @param args The arguments used for the selector.
* @param block The block of code to run with the queried [Cursor]. Will not be ran if the * @param block The block of code to run with the queried [Cursor]. Will not be ran if the [Cursor]
* [Cursor] is empty. * is empty.
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
* @see ContentResolver.query * @see ContentResolver.query
*/ */
@ -81,9 +79,7 @@ inline fun <reified R> ContentResolver.useQuery(
block: (Cursor) -> R block: (Cursor) -> R
) = safeQuery(uri, projection, selector, args).use(block) ) = safeQuery(uri, projection, selector, args).use(block)
/** /** Album art [MediaStore] database is not a built-in constant, have to define it ourselves. */
* Album art [MediaStore] database is not a built-in constant, have to define it ourselves.
*/
private val EXTERNAL_COVERS_URI = Uri.parse("content://media/external/audio/albumart") private val EXTERNAL_COVERS_URI = Uri.parse("content://media/external/audio/albumart")
/** /**
@ -92,11 +88,12 @@ private val EXTERNAL_COVERS_URI = Uri.parse("content://media/external/audio/albu
* @see ContentUris.withAppendedId * @see ContentUris.withAppendedId
* @see MediaStore.Audio.Media.EXTERNAL_CONTENT_URI * @see MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
*/ */
fun Long.toAudioUri() = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this) fun Long.toAudioUri() =
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this)
/** /**
* Convert a [MediaStore] Album ID into a [Uri] to it's system-provided album cover. This cover * Convert a [MediaStore] Album ID into a [Uri] to it's system-provided album cover. This cover will
* will be fast to load, but will be lower quality. * be fast to load, but will be lower quality.
* @return An external storage image [Uri]. May not exist. * @return An external storage image [Uri]. May not exist.
* @see ContentUris.withAppendedId * @see ContentUris.withAppendedId
*/ */
@ -114,10 +111,9 @@ fun Long.toCoverUri() = ContentUris.withAppendedId(EXTERNAL_COVERS_URI, this)
private val SM_API21_GET_VOLUME_LIST_METHOD: Method by private val SM_API21_GET_VOLUME_LIST_METHOD: Method by
lazyReflectedMethod(StorageManager::class, "getVolumeList") lazyReflectedMethod(StorageManager::class, "getVolumeList")
/** /**
* Provides the analogous method to [StorageVolume.getDirectory] method that is usable from * Provides the analogous method to [StorageVolume.getDirectory] method that is usable from API 21
* API 21 to API 23, in which the [StorageVolume] API was hidden and differed greatly. * to API 23, in which the [StorageVolume] API was hidden and differed greatly.
* @see StorageVolume.getDirectory * @see StorageVolume.getDirectory
*/ */
@Suppress("NewApi") @Suppress("NewApi")
@ -175,8 +171,8 @@ val StorageVolume.directoryCompat: String?
fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context) fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context)
/** /**
* If this [StorageVolume] is considered the "Primary" volume where the Android System is * If this [StorageVolume] is considered the "Primary" volume where the Android System is kept. May
* kept. May still be a removable volume. * still be a removable volume.
* @see StorageVolume.isPrimary * @see StorageVolume.isPrimary
*/ */
val StorageVolume.isPrimaryCompat: Boolean val StorageVolume.isPrimaryCompat: Boolean
@ -191,8 +187,8 @@ val StorageVolume.isEmulatedCompat: Boolean
@SuppressLint("NewApi") get() = isEmulated @SuppressLint("NewApi") get() = isEmulated
/** /**
* If this [StorageVolume] represents the "Internal Shared Storage" volume, also known as * If this [StorageVolume] represents the "Internal Shared Storage" volume, also known as "primary"
* "primary" to [MediaStore] and Document [Uri]s, obtained in a version compatible manner. * to [MediaStore] and Document [Uri]s, obtained in a version compatible manner.
*/ */
val StorageVolume.isInternalCompat: Boolean val StorageVolume.isInternalCompat: Boolean
// Must contain the android system AND be an emulated drive, as non-emulated system // Must contain the android system AND be an emulated drive, as non-emulated system
@ -200,24 +196,24 @@ val StorageVolume.isInternalCompat: Boolean
get() = isPrimaryCompat && isEmulatedCompat get() = isPrimaryCompat && isEmulatedCompat
/** /**
* The unique identifier for this [StorageVolume], obtained in a version compatible manner * The unique identifier for this [StorageVolume], obtained in a version compatible manner Can be
* Can be null. * null.
* @see StorageVolume.getUuid * @see StorageVolume.getUuid
*/ */
val StorageVolume.uuidCompat: String? val StorageVolume.uuidCompat: String?
@SuppressLint("NewApi") get() = uuid @SuppressLint("NewApi") get() = uuid
/** /**
* The current state of this [StorageVolume], such as "mounted" or "read-only", obtained in * The current state of this [StorageVolume], such as "mounted" or "read-only", obtained in a
* a version compatible manner. * version compatible manner.
* @see StorageVolume.getState * @see StorageVolume.getState
*/ */
val StorageVolume.stateCompat: String val StorageVolume.stateCompat: String
@SuppressLint("NewApi") get() = state @SuppressLint("NewApi") get() = state
/** /**
* Returns the name of this volume that can be used to interact with [MediaStore], in * Returns the name of this volume that can be used to interact with [MediaStore], in a version
* a version compatible manner. Will be null if the volume is not scanned by [MediaStore]. * compatible manner. Will be null if the volume is not scanned by [MediaStore].
* @see StorageVolume.getMediaStoreVolumeName * @see StorageVolume.getMediaStoreVolumeName
*/ */
val StorageVolume.mediaStoreVolumeNameCompat: String? val StorageVolume.mediaStoreVolumeNameCompat: String?

View file

@ -43,10 +43,10 @@ import org.oxycblt.auxio.util.logW
/** /**
* Core music loading state class. * Core music loading state class.
* *
* This class provides low-level access into the exact state of the music loading process. * This class provides low-level access into the exact state of the music loading process. **This
* **This class should not be used in most cases.** It is highly volatile and provides far * class should not be used in most cases.** It is highly volatile and provides far more information
* more information than is usually needed. Use [MusicStore] instead if you do not need to * than is usually needed. Use [MusicStore] instead if you do not need to work with the exact music
* work with the exact music loading state. * loading state.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@ -61,9 +61,9 @@ class Indexer private constructor() {
get() = indexingState != null get() = indexingState != null
/** /**
* Whether this instance has not completed a loading process and is not currently * Whether this instance has not completed a loading process and is not currently loading music.
* loading music. This often occurs early in an app's lifecycle, and consumers should * This often occurs early in an app's lifecycle, and consumers should try to avoid showing any
* try to avoid showing any state when this flag is true. * state when this flag is true.
*/ */
val isIndeterminate: Boolean val isIndeterminate: Boolean
get() = lastResponse == null && indexingState == null get() = lastResponse == null && indexingState == null
@ -105,9 +105,9 @@ class Indexer private constructor() {
} }
/** /**
* Register the [Callback] for this instance. This can be used to receive rapid-fire updates * Register the [Callback] for this instance. This can be used to receive rapid-fire updates to
* to the current music loading state. There can be only one [Callback] at a time. * the current music loading state. There can be only one [Callback] at a time. Will invoke all
* Will invoke all [Callback] methods to initialize the instance with the current state. * [Callback] methods to initialize the instance with the current state.
* @param callback The [Callback] to add. * @param callback The [Callback] to add.
*/ */
@Synchronized @Synchronized
@ -125,10 +125,9 @@ class Indexer private constructor() {
} }
/** /**
* Unregister a [Callback] from this instance, preventing it from recieving any further * Unregister a [Callback] from this instance, preventing it from recieving any further updates.
* updates. * @param callback The [Callback] to unregister. Must be the current [Callback]. Does nothing if
* @param callback The [Callback] to unregister. Must be the current [Callback]. Does * invoked by another [Callback] implementation.
* nothing if invoked by another [Callback] implementation.
* @see Callback * @see Callback
*/ */
@Synchronized @Synchronized
@ -145,8 +144,8 @@ class Indexer private constructor() {
* Start the indexing process. This should be done from in the background from [Controller]'s * Start the indexing process. This should be done from in the background from [Controller]'s
* context after a command has been received to start the process. * context after a command has been received to start the process.
* @param context [Context] required to load music. * @param context [Context] required to load music.
* @param withCache Whether to use the cache or not when loading. If false, the cache will * @param withCache Whether to use the cache or not when loading. If false, the cache will still
* still be written, but no cache entries will be loaded into the new library. * be written, but no cache entries will be loaded into the new library.
*/ */
suspend fun index(context: Context, withCache: Boolean) { suspend fun index(context: Context, withCache: Boolean) {
if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) == if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
@ -186,9 +185,9 @@ class Indexer private constructor() {
} }
/** /**
* Request that the music library should be reloaded. This should be used by components that * Request that the music library should be reloaded. This should be used by components that do
* do not manage the indexing process in order to signal that the [Controller] should call * not manage the indexing process in order to signal that the [Controller] should call [index]
* [index] eventually. * eventually.
* @param withCache Whether to use the cache when loading music. Does nothing if there is no * @param withCache Whether to use the cache when loading music. Does nothing if there is no
* [Controller]. * [Controller].
*/ */
@ -199,8 +198,8 @@ class Indexer private constructor() {
} }
/** /**
* Reset the current loading state to signal that the instance is not loading. This should * Reset the current loading state to signal that the instance is not loading. This should be
* be called by [Controller] after it's indexing co-routine was cancelled. * called by [Controller] after it's indexing co-routine was cancelled.
*/ */
@Synchronized @Synchronized
fun reset() { fun reset() {
@ -211,15 +210,16 @@ class Indexer private constructor() {
/** /**
* Internal implementation of the music loading process. * Internal implementation of the music loading process.
* @param context [Context] required to load music. * @param context [Context] required to load music.
* @param withCache Whether to use the cache or not when loading. If false, the cache will * @param withCache Whether to use the cache or not when loading. If false, the cache will still
* still be written, but no cache entries will be loaded into the new library. * be written, but no cache entries will be loaded into the new library.
* @return A newly-loaded [MusicStore.Library], or null if nothing was loaded. * @return A newly-loaded [MusicStore.Library], or null if nothing was loaded.
*/ */
private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? { private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? {
// Create the chain of extractors. Each extractor builds on the previous and // Create the chain of extractors. Each extractor builds on the previous and
// enables version-specific features in order to create the best possible music // enables version-specific features in order to create the best possible music
// experience. // experience.
val cacheDatabase = if (withCache) { val cacheDatabase =
if (withCache) {
ReadWriteCacheExtractor(context) ReadWriteCacheExtractor(context)
} else { } else {
WriteOnlyCacheExtractor(context) WriteOnlyCacheExtractor(context)
@ -255,11 +255,11 @@ class Indexer private constructor() {
/** /**
* Load a list of [Song]s from the device. * Load a list of [Song]s from the device.
* @param metadataExtractor The completed [MetadataExtractor] instance to use to load * @param metadataExtractor The completed [MetadataExtractor] instance to use to load [Song.Raw]
* [Song.Raw] instances. * instances.
* @param settings [Settings] required to create [Song] instances. * @param settings [Settings] required to create [Song] instances.
* @return A possibly empty list of [Song]s. These [Song]s will be incomplete and * @return A possibly empty list of [Song]s. These [Song]s will be incomplete and must be linked
* must be linked with parent [Album], [Artist], and [Genre] items in order to be usable. * with parent [Album], [Artist], and [Genre] items in order to be usable.
*/ */
private suspend fun buildSongs( private suspend fun buildSongs(
metadataExtractor: MetadataExtractor, metadataExtractor: MetadataExtractor,
@ -301,10 +301,10 @@ class Indexer private constructor() {
/** /**
* Build a list of [Album]s from the given [Song]s. * Build a list of [Album]s from the given [Song]s.
* @param songs The [Song]s to build [Album]s from. These will be linked with their * @param songs The [Song]s to build [Album]s from. These will be linked with their respective
* respective [Album]s when created. * [Album]s when created.
* @return A non-empty list of [Album]s. These [Album]s will be incomplete and * @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked
* must be linked with parent [Artist] instances in order to be usable. * with parent [Artist] instances in order to be usable.
*/ */
private fun buildAlbums(songs: List<Song>): List<Album> { private fun buildAlbums(songs: List<Song>): List<Album> {
// Group songs by their singular raw album, then map the raw instances and their // Group songs by their singular raw album, then map the raw instances and their
@ -316,17 +316,17 @@ class Indexer private constructor() {
} }
/** /**
* Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required * Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required as
* as they group into [Artist] instances much differently, with [Song]s being grouped * they group into [Artist] instances much differently, with [Song]s being grouped primarily by
* primarily by artist names, and [Album]s being grouped primarily by album artist names. * artist names, and [Album]s being grouped primarily by album artist names.
* @param songs The [Song]s to build [Artist]s from. One [Song] can result in * @param songs The [Song]s to build [Artist]s from. One [Song] can result in the creation of
* the creation of one or more [Artist] instances. These will be linked with their * one or more [Artist] instances. These will be linked with their respective [Artist]s when
* respective [Artist]s when created. * created.
* @param albums The [Album]s to build [Artist]s from. One [Album] can result in * @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of
* the creation of one or more [Artist] instances. These will be linked with their * one or more [Artist] instances. These will be linked with their respective [Artist]s when
* respective [Artist]s when created. * created.
* @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined * @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings
* groupings of [Song]s and [Album]s. * of [Song]s and [Album]s.
*/ */
private fun buildArtists(songs: List<Song>, albums: List<Album>): List<Artist> { private fun buildArtists(songs: List<Song>, albums: List<Album>): List<Artist> {
// Add every raw artist credited to each Song/Album to the grouping. This way, // Add every raw artist credited to each Song/Album to the grouping. This way,
@ -353,9 +353,9 @@ class Indexer private constructor() {
/** /**
* Group up [Song]s into [Genre] instances. * Group up [Song]s into [Genre] instances.
* @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in * @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of
* the creation of one or more [Genre] instances. These will be linked with their * one or more [Genre] instances. These will be linked with their respective [Genre]s when
* respective [Genre]s when created. * created.
* @return A non-empty list of [Genre]s. * @return A non-empty list of [Genre]s.
*/ */
private fun buildGenres(songs: List<Song>): List<Genre> { private fun buildGenres(songs: List<Song>): List<Genre> {
@ -376,8 +376,8 @@ class Indexer private constructor() {
/** /**
* Emit a new [State.Indexing] state. This can be used to signal the current state of the music * Emit a new [State.Indexing] state. This can be used to signal the current state of the music
* loading process to external code. Assumes that the callee has already checked if they * loading process to external code. Assumes that the callee has already checked if they have
* have not been canceled and thus have the ability to emit a new state. * not been canceled and thus have the ability to emit a new state.
* @param indexing The new [Indexing] state to emit, or null if no loading process is occurring. * @param indexing The new [Indexing] state to emit, or null if no loading process is occurring.
*/ */
@Synchronized @Synchronized
@ -393,8 +393,8 @@ class Indexer private constructor() {
/** /**
* Emit a new [State.Complete] state. This can be used to signal the completion of the music * Emit a new [State.Complete] state. This can be used to signal the completion of the music
* loading process to external code. Will check if the callee has not been canceled and thus * loading process to external code. Will check if the callee has not been canceled and thus has
* has the ability to emit a new state * the ability to emit a new state
* @param response The new [Response] to emit, representing the outcome of the music loading * @param response The new [Response] to emit, representing the outcome of the music loading
* process. * process.
*/ */
@ -439,8 +439,7 @@ class Indexer private constructor() {
*/ */
sealed class Indexing { sealed class Indexing {
/** /**
* Music loading is occurring, but no definite estimate can be put on the current * Music loading is occurring, but no definite estimate can be put on the current progress.
* progress.
*/ */
object Indeterminate : Indexing() object Indeterminate : Indexing()
@ -477,8 +476,8 @@ class Indexer private constructor() {
* A callback for rapid-fire changes in the music loading state. * A callback for rapid-fire changes in the music loading state.
* *
* This is only useful for code that absolutely must show the current loading process. * This is only useful for code that absolutely must show the current loading process.
* Otherwise, [MusicStore.Callback] is highly recommended due to it's updates only * Otherwise, [MusicStore.Callback] is highly recommended due to it's updates only consisting of
* consisting of the [MusicStore.Library]. * the [MusicStore.Library].
*/ */
interface Callback { interface Callback {
/** /**
@ -493,13 +492,13 @@ class Indexer private constructor() {
} }
/** /**
* Context that runs the music loading process. Implementations should be capable of * Context that runs the music loading process. Implementations should be capable of running the
* running the background for long periods of time without android killing the process. * background for long periods of time without android killing the process.
*/ */
interface Controller : Callback { interface Controller : Callback {
/** /**
* Called when a new music loading process was requested. Implementations should * Called when a new music loading process was requested. Implementations should forward
* forward this to [index]. * this to [index].
* @param withCache Whether to use the cache or not when loading. If false, the cache should * @param withCache Whether to use the cache or not when loading. If false, the cache should
* still be written, but no cache entries will be loaded into the new library. * still be written, but no cache entries will be loaded into the new library.
* @see index * @see index
@ -511,9 +510,8 @@ class Indexer private constructor() {
@Volatile private var INSTANCE: Indexer? = null @Volatile private var INSTANCE: Indexer? = null
/** /**
* A version-compatible identifier for the read external storage permission required * A version-compatible identifier for the read external storage permission required by the
* by the system to load audio. * system to load audio. TODO: Move elsewhere.
* TODO: Move elsewhere.
*/ */
val PERMISSION_READ_AUDIO = val PERMISSION_READ_AUDIO =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

View file

@ -23,7 +23,7 @@ import androidx.core.app.NotificationCompat
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.shared.ForegroundServiceNotification import org.oxycblt.auxio.service.ForegroundServiceNotification
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newMainPendingIntent import org.oxycblt.auxio.util.newMainPendingIntent
@ -89,11 +89,12 @@ class IndexingNotification(private val context: Context) :
} }
/** /**
* A static [ForegroundServiceNotification] that signals to the user that the app is currently monitoring * A static [ForegroundServiceNotification] that signals to the user that the app is currently
* the music library for changes. * monitoring the music library for changes.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class ObservingNotification(context: Context) : ForegroundServiceNotification(context, INDEXER_CHANNEL) { class ObservingNotification(context: Context) :
ForegroundServiceNotification(context, INDEXER_CHANNEL) {
init { init {
setSmallIcon(R.drawable.ic_indexer_24) setSmallIcon(R.drawable.ic_indexer_24)
setCategory(NotificationCompat.CATEGORY_SERVICE) setCategory(NotificationCompat.CATEGORY_SERVICE)

View file

@ -35,20 +35,20 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ForegroundManager
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
* A [Service] that manages the background music loading process. * A [Service] that manages the background music loading process.
* *
* Loading music is a time-consuming process that would likely be killed by the system before * Loading music is a time-consuming process that would likely be killed by the system before it
* it could complete if ran anywhere else. So, this [Service] manages the music loading process * could complete if ran anywhere else. So, this [Service] manages the music loading process as an
* as an instance of [Indexer.Controller]. * instance of [Indexer.Controller].
* *
* This [Service] also handles automatic rescanning, as that is a similarly long-running * This [Service] also handles automatic rescanning, as that is a similarly long-running background
* background operation that would be unsuitable elsewhere in the app. * operation that would be unsuitable elsewhere in the app.
* *
* TODO: Unify with PlaybackService as part of the service independence project * TODO: Unify with PlaybackService as part of the service independence project
* *
@ -121,8 +121,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
indexer.reset() indexer.reset()
} }
// Start a new music loading job on a co-routine. // Start a new music loading job on a co-routine.
currentIndexJob = indexScope.launch { currentIndexJob = indexScope.launch { indexer.index(this@IndexerService, withCache) }
indexer.index(this@IndexerService, withCache) }
} }
override fun onIndexerStateChanged(state: Indexer.State?) { override fun onIndexerStateChanged(state: Indexer.State?) {
@ -165,8 +164,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
// --- INTERNAL --- // --- INTERNAL ---
/** /**
* Update the current state to "Active", in which the service signals that music * Update the current state to "Active", in which the service signals that music loading is
* loading is on-going. * on-going.
* @param state The current music loading state. * @param state The current music loading state.
*/ */
private fun updateActiveSession(state: Indexer.Indexing) { private fun updateActiveSession(state: Indexer.Indexing) {
@ -184,8 +183,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
} }
/** /**
* Update the current state to "Idle", in which it either does nothing or signals * Update the current state to "Idle", in which it either does nothing or signals that it's
* that it's currently monitoring the music library for changes. * currently monitoring the music library for changes.
*/ */
private fun updateIdleSession() { private fun updateIdleSession() {
if (settings.shouldBeObserving) { if (settings.shouldBeObserving) {
@ -208,9 +207,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
wakeLock.releaseSafe() wakeLock.releaseSafe()
} }
/** /** Utility to safely acquire a [PowerManager.WakeLock] without crashes/inefficiency. */
* Utility to safely acquire a [PowerManager.WakeLock] without crashes/inefficiency.
*/
private fun PowerManager.WakeLock.acquireSafe() { private fun PowerManager.WakeLock.acquireSafe() {
// Avoid unnecessary acquire calls. // Avoid unnecessary acquire calls.
if (!wakeLock.isHeld) { if (!wakeLock.isHeld) {
@ -222,9 +219,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
} }
} }
/** /** Utility to safely release a [PowerManager.WakeLock] without crashes/inefficiency. */
* Utility to safely release a [PowerManager.WakeLock] without crashes/inefficiency.
*/
private fun PowerManager.WakeLock.releaseSafe() { private fun PowerManager.WakeLock.releaseSafe() {
// Avoid unnecessary release calls. // Avoid unnecessary release calls.
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
@ -259,7 +254,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
* known to the user as automatic rescanning. The active (and not passive) nature of observing * known to the user as automatic rescanning. The active (and not passive) nature of observing
* the database is what requires [IndexerService] to stay foreground when this is enabled. * the database is what requires [IndexerService] to stay foreground when this is enabled.
*/ */
private inner class SystemContentObserver : ContentObserver(Handler(Looper.getMainLooper())), Runnable { private inner class SystemContentObserver :
ContentObserver(Handler(Looper.getMainLooper())), Runnable {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
init { init {

View file

@ -25,9 +25,9 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getAttrColorCompat

View file

@ -33,9 +33,9 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.shared.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast

View file

@ -41,8 +41,7 @@ import org.oxycblt.auxio.util.inflater
* @param listener A [Listener] to bind interactions to. * @param listener A [Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class QueueAdapter(private val listener: Listener) : class QueueAdapter(private val listener: Listener) : RecyclerView.Adapter<QueueSongViewHolder>() {
RecyclerView.Adapter<QueueSongViewHolder>() {
private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK) private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK)
// Since PlayingIndicator adapter relies on an item value, we cannot use it for this // Since PlayingIndicator adapter relies on an item value, we cannot use it for this
// adapter, as one item can appear at several points in the UI. Use a similar implementation // adapter, as one item can appear at several points in the UI. Use a similar implementation
@ -72,8 +71,8 @@ class QueueAdapter(private val listener: Listener) :
} }
/** /**
* Synchronously update the list with new items. This is exceedingly slow for large diffs, * Synchronously update the list with new items. This is exceedingly slow for large diffs, so
* so only use it for trivial updates. * only use it for trivial updates.
* @param newList The new [Song]s for the adapter to display. * @param newList The new [Song]s for the adapter to display.
*/ */
fun submitList(newList: List<Song>) { fun submitList(newList: List<Song>) {
@ -81,8 +80,8 @@ class QueueAdapter(private val listener: Listener) :
} }
/** /**
* Replace the list with a new list. This is exceedingly slow for large diffs, * Replace the list with a new list. This is exceedingly slow for large diffs, so only use it
* so only use it for trivial updates. * for trivial updates.
* @param newList The new [Song]s for the adapter to display. * @param newList The new [Song]s for the adapter to display.
*/ */
fun replaceList(newList: List<Song>) { fun replaceList(newList: List<Song>) {
@ -90,8 +89,8 @@ class QueueAdapter(private val listener: Listener) :
} }
/** /**
* Set the position of the currently playing item in the queue. This will mark the item * Set the position of the currently playing item in the queue. This will mark the item as
* as playing and any previous items as played. * playing and any previous items as played.
* @param index The position of the currently playing item in the queue. * @param index The position of the currently playing item in the queue.
* @param isPlaying Whether playback is ongoing or paused. * @param isPlaying Whether playback is ongoing or paused.
*/ */
@ -122,9 +121,7 @@ class QueueAdapter(private val listener: Listener) :
} }
} }
/** /** A listener for queue list events. */
* A listener for queue list events.
*/
interface Listener { interface Listener {
/** /**
* Called when a [RecyclerView.ViewHolder] in the list as clicked. * Called when a [RecyclerView.ViewHolder] in the list as clicked.
@ -152,21 +149,15 @@ class QueueAdapter(private val listener: Listener) :
*/ */
class QueueSongViewHolder private constructor(private val binding: ItemQueueSongBinding) : class QueueSongViewHolder private constructor(private val binding: ItemQueueSongBinding) :
PlayingIndicatorAdapter.ViewHolder(binding.root) { PlayingIndicatorAdapter.ViewHolder(binding.root) {
/** /** The "body" view of this [QueueSongViewHolder] that shows the [Song] information. */
* The "body" view of this [QueueSongViewHolder] that shows the [Song] information.
*/
val bodyView: View val bodyView: View
get() = binding.body get() = binding.body
/** /** The background view of this [QueueSongViewHolder] that shows the delete icon. */
* The background view of this [QueueSongViewHolder] that shows the delete icon.
*/
val backgroundView: View val backgroundView: View
get() = binding.background get() = binding.background
/** /** The actual background drawable of this [QueueSongViewHolder] that can be manipulated. */
* The actual background drawable of this [QueueSongViewHolder] that can be manipulated.
*/
val backgroundDrawable = val backgroundDrawable =
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply { MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface) fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
@ -174,9 +165,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
alpha = 0 alpha = 0
} }
/** /** If this queue item is considered "in the future" (i.e has not played yet). */
* If this queue item is considered "in the future" (i.e has not played yet).
*/
var isFuture: Boolean var isFuture: Boolean
get() = binding.songAlbumCover.isEnabled get() = binding.songAlbumCover.isEnabled
set(value) { set(value) {
@ -205,9 +194,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
*/ */
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
fun bind(song: Song, listener: QueueAdapter.Listener) { fun bind(song: Song, listener: QueueAdapter.Listener) {
binding.body.setOnClickListener { binding.body.setOnClickListener { listener.onClick(this) }
listener.onClick(this)
}
binding.songAlbumCover.bind(song) binding.songAlbumCover.bind(song)
binding.songName.text = song.resolveName(binding.context) binding.songName.text = song.resolveName(binding.context)

View file

@ -28,8 +28,8 @@ import org.oxycblt.auxio.util.getInteger
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
* A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in the queue * A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in the queue UI,
* UI, such as an animation when lifting items. * such as an animation when lifting items.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHelper.Callback() { class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHelper.Callback() {
@ -73,7 +73,8 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
holder.itemView holder.itemView
.animate() .animate()
.translationZ(elevation) .translationZ(elevation)
.setDuration(recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong()) .setDuration(
recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong())
.setUpdateListener { .setUpdateListener {
bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt() bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt()
} }
@ -114,7 +115,8 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
holder.itemView holder.itemView
.animate() .animate()
.translationZ(0f) .translationZ(0f)
.setDuration(recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong()) .setDuration(
recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong())
.setUpdateListener { .setUpdateListener {
bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt() bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt()
} }

View file

@ -26,7 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD

View file

@ -52,8 +52,8 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
/** /**
* Start playing the the queue item at the given index. * Start playing the the queue item at the given index.
* @param adapterIndex The index of the queue item to play. Does nothing if the index is out * @param adapterIndex The index of the queue item to play. Does nothing if the index is out of
* of range. * range.
*/ */
fun goto(adapterIndex: Int) { fun goto(adapterIndex: Int) {
if (adapterIndex !in playbackManager.queue.indices) { if (adapterIndex !in playbackManager.queue.indices) {
@ -65,8 +65,8 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
/** /**
* Remove a queue item at the given index. * Remove a queue item at the given index.
* @param adapterIndex The index of the queue item to play. Does nothing if the index is * @param adapterIndex The index of the queue item to play. Does nothing if the index is out of
* out of range. * range.
*/ */
fun removeQueueDataItem(adapterIndex: Int) { fun removeQueueDataItem(adapterIndex: Int) {
if (adapterIndex <= playbackManager.index || if (adapterIndex <= playbackManager.index ||
@ -93,16 +93,12 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
return true return true
} }
/** /** Finish a replace flag specified by [replaceQueue]. */
* Finish a replace flag specified by [replaceQueue].
*/
fun finishReplace() { fun finishReplace() {
replaceQueue = null replaceQueue = null
} }
/** /** Finish a scroll operation started by [scrollTo]. */
* Finish a scroll operation started by [scrollTo].
*/
fun finishScrollTo() { fun finishScrollTo() {
scrollTo = null scrollTo = null
} }

View file

@ -25,7 +25,7 @@ import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
/** /**

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.shared.ForegroundServiceNotification import org.oxycblt.auxio.service.ForegroundServiceNotification
import org.oxycblt.auxio.util.newBroadcastPendingIntent import org.oxycblt.auxio.util.newBroadcastPendingIntent
import org.oxycblt.auxio.util.newMainPendingIntent import org.oxycblt.auxio.util.newMainPendingIntent

View file

@ -54,8 +54,8 @@ import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ForegroundManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetComponent
import org.oxycblt.auxio.widgets.WidgetProvider import org.oxycblt.auxio.widgets.WidgetProvider

View file

@ -51,8 +51,8 @@ abstract class BaseBottomSheetBehavior<V : View>(context: Context, attributeSet:
abstract fun createBackground(context: Context): Drawable abstract fun createBackground(context: Context): Drawable
/** /**
* Called when window insets are being applied to the [View] this [BaseBottomSheetBehavior] * Called when window insets are being applied to the [View] this [BaseBottomSheetBehavior] is
* is linked to. * linked to.
* @param child The child view recieving the [WindowInsets]. * @param child The child view recieving the [WindowInsets].
* @param insets The [WindowInsets] to apply. * @param insets The [WindowInsets] to apply.
* @return The (possibly modified) [WindowInsets]. * @return The (possibly modified) [WindowInsets].

View file

@ -72,8 +72,8 @@ class SearchAdapter(private val listener: SelectableListListener) :
override fun isItemFullWidth(position: Int) = differ.currentList[position] is Header override fun isItemFullWidth(position: Int) = differ.currentList[position] is Header
/** /**
* Asynchronously update the list with new items. Assumes that the list only contains * Asynchronously update the list with new items. Assumes that the list only contains supported
* supported data.. * data..
* @param newList The new [Item]s for the adapter to display. * @param newList The new [Item]s for the adapter to display.
* @param callback A block called when the asynchronous update is completed. * @param callback A block called when the asynchronous update is completed.
*/ */

View file

@ -208,9 +208,7 @@ class SearchFragment : ListFragment<FragmentSearchBinding>() {
} }
} }
/** /** Safely hide the keyboard from this view. */
* Safely hide the keyboard from this view.
*/
private fun InputMethodManager.hide() { private fun InputMethodManager.hide() {
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS) hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
} }

View file

@ -30,13 +30,9 @@ import kotlinx.coroutines.yield
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
@ -129,7 +125,9 @@ class SearchViewModel(application: Application) :
} }
if (filterMode == null || filterMode == MusicMode.SONGS) { if (filterMode == null || filterMode == MusicMode.SONGS) {
library.songs.searchListImpl(query) { q, song -> song.path.name.contains(q) }?.let { library.songs
.searchListImpl(query) { q, song -> song.path.name.contains(q) }
?.let {
results.add(Header(R.string.lbl_songs)) results.add(Header(R.string.lbl_songs))
results.addAll(sort.songs(it)) results.addAll(sort.songs(it))
} }
@ -141,16 +139,17 @@ class SearchViewModel(application: Application) :
/** /**
* Search a given [Music] list. * Search a given [Music] list.
* @param query The query to search for. The routine will compare this query to the names * @param query The query to search for. The routine will compare this query to the names of
* of each object in the list and * each object in the list and
* @param fallback Additional comparison code to run if the item does not match the query * @param fallback Additional comparison code to run if the item does not match the query
* initially. This can be used to compare against additional attributes to improve search * initially. This can be used to compare against additional attributes to improve search result
* result quality. * quality.
*/ */
private inline fun <T : Music> List<T>.searchListImpl( private inline fun <T : Music> List<T>.searchListImpl(
query: String, query: String,
fallback: (String, T) -> Boolean = { _, _ -> false } fallback: (String, T) -> Boolean = { _, _ -> false }
) = filter { ) =
filter {
// See if the plain resolved name matches the query. This works for most situations. // See if the plain resolved name matches the query. This works for most situations.
val name = it.resolveName(context) val name = it.resolveName(context)
if (name.contains(query, ignoreCase = true)) { if (name.contains(query, ignoreCase = true)) {
@ -167,7 +166,8 @@ class SearchViewModel(application: Application) :
// As a last-ditch effort, see if the normalized name matches. This will replace // As a last-ditch effort, see if the normalized name matches. This will replace
// any non-alphabetical characters with their alphabetical representations, which // any non-alphabetical characters with their alphabetical representations, which
// could make it match the query. // could make it match the query.
val normalizedName = NORMALIZATION_SANITIZE_REGEX.replace( val normalizedName =
NORMALIZATION_SANITIZE_REGEX.replace(
Normalizer.normalize(name, Normalizer.Form.NFKD), "") Normalizer.normalize(name, Normalizer.Form.NFKD), "")
if (normalizedName.contains(query, ignoreCase = true)) { if (normalizedName.contains(query, ignoreCase = true)) {
return@filter true return@filter true
@ -212,7 +212,6 @@ class SearchViewModel(application: Application) :
search(lastQuery) search(lastQuery)
} }
companion object { companion object {
/** /**
* Converts the output of [Normalizer] to remove any junk characters added by it's * Converts the output of [Normalizer] to remove any junk characters added by it's

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.shared package org.oxycblt.auxio.service
import android.app.Service import android.app.Service
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
@ -31,17 +31,15 @@ import org.oxycblt.auxio.util.logD
class ForegroundManager(private val service: Service) { class ForegroundManager(private val service: Service) {
private var isForeground = false private var isForeground = false
/** /** Release this instance. */
* Release this instance.
*/
fun release() { fun release() {
tryStopForeground() tryStopForeground()
} }
/** /**
* Try to enter a foreground state. * Try to enter a foreground state.
* @param notification The [ForegroundServiceNotification] to show in order to signal the foreground * @param notification The [ForegroundServiceNotification] to show in order to signal the
* state. * foreground state.
* @return true if the state was changed, false otherwise * @return true if the state was changed, false otherwise
* @see Service.startForeground * @see Service.startForeground
*/ */

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.shared package org.oxycblt.auxio.service
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
@ -51,14 +51,11 @@ abstract class ForegroundServiceNotification(context: Context, info: ChannelInfo
*/ */
abstract val code: Int abstract val code: Int
/** /** Post this notification using [NotificationManagerCompat]. */
* Post this notification using [NotificationManagerCompat].
*/
fun post() { fun post() {
// This is safe to call without the POST_NOTIFICATIONS permission, as it's a foreground // This is safe to call without the POST_NOTIFICATIONS permission, as it's a foreground
// notification. // notification.
@Suppress("MissingPermission") @Suppress("MissingPermission") notificationManager.notify(code, build())
notificationManager.notify(code, build())
} }
/** /**

View file

@ -33,7 +33,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentAboutBinding import org.oxycblt.auxio.databinding.FragmentAboutBinding
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.shared.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast

View file

@ -35,13 +35,13 @@ import org.oxycblt.auxio.music.storage.MusicDirectories
import org.oxycblt.auxio.playback.ActionMode import org.oxycblt.auxio.playback.ActionMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
import org.oxycblt.auxio.settings.accent.Accent import org.oxycblt.auxio.ui.accent.Accent
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. * A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. Object
* Object mutability * mutability
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class Settings(private val context: Context, private val callback: Callback? = null) : class Settings(private val context: Context, private val callback: Callback? = null) :
@ -55,8 +55,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
/** /**
* Migrate any settings from an old version into their modern counterparts. This can cause * Migrate any settings from an old version into their modern counterparts. This can cause data
* data loss depending on the feasibility of a migration. * loss depending on the feasibility of a migration.
*/ */
fun migrate() { fun migrate() {
if (inner.contains(OldKeys.KEY_ACCENT3)) { if (inner.contains(OldKeys.KEY_ACCENT3)) {
@ -153,8 +153,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
/** /**
* Release this instance and any callbacks held by it. This is not needed if no [Callback] * Release this instance and any callbacks held by it. This is not needed if no [Callback] was
* was originally attached. * originally attached.
*/ */
fun release() { fun release() {
inner.unregisterOnSharedPreferenceChangeListener(this) inner.unregisterOnSharedPreferenceChangeListener(this)
@ -164,9 +164,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
unlikelyToBeNull(callback).onSettingChanged(key) unlikelyToBeNull(callback).onSettingChanged(key)
} }
/** /** TODO: Remove this */
* TODO: Remove this
*/
interface Callback { interface Callback {
fun onSettingChanged(key: String) fun onSettingChanged(key: String)
} }
@ -264,8 +262,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
?: MusicMode.SONGS ?: MusicMode.SONGS
/** /**
* What MusicParent item to play from when a Song is played from the detail view. * What MusicParent item to play from when a Song is played from the detail view. Will be null
* Will be null if configured to play from the currently shown item. * if configured to play from the currently shown item.
*/ */
val detailPlaybackMode: MusicMode? val detailPlaybackMode: MusicMode?
get() = get() =
@ -329,8 +327,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
/** /**
* A string of characters representing the desired separator characters to denote * A string of characters representing the desired separator characters to denote multi-value
* multi-value tags. * tags.
*/ */
var musicSeparators: String? var musicSeparators: String?
// Differ from convention and store a string of separator characters instead of an int // Differ from convention and store a string of separator characters instead of an int

View file

@ -23,7 +23,7 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialFadeThrough
import org.oxycblt.auxio.databinding.FragmentSettingsBinding import org.oxycblt.auxio.databinding.FragmentSettingsBinding
import org.oxycblt.auxio.shared.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
/** /**
* A [Fragment] wrapper containing the preference fragment and a companion Toolbar. * A [Fragment] wrapper containing the preference fragment and a companion Toolbar.

View file

@ -114,8 +114,8 @@ constructor(
/** /**
* Get the index of the current value. * Get the index of the current value.
* @return The index of the current value within [values], or -1 if the [IntListPreference] * @return The index of the current value within [values], or -1 if the [IntListPreference] is
* is not set. * not set.
*/ */
fun getValueIndex(): Int { fun getValueIndex(): Int {
val curValue = currentValue val curValue = currentValue
@ -148,9 +148,7 @@ constructor(
} }
} }
/** /** Copy of ListPreference's [Preference.SummaryProvider] for this [IntListPreference]. */
* Copy of ListPreference's [Preference.SummaryProvider] for this [IntListPreference].
*/
private inner class IntListSummaryProvider : SummaryProvider<IntListPreference> { private inner class IntListSummaryProvider : SummaryProvider<IntListPreference> {
override fun provideSummary(preference: IntListPreference): CharSequence { override fun provideSummary(preference: IntListPreference): CharSequence {
val index = getValueIndex() val index = getValueIndex()

View file

@ -22,8 +22,8 @@ import android.util.AttributeSet
import androidx.preference.DialogPreference import androidx.preference.DialogPreference
/** /**
* Wraps a [DialogPreference] to be instantiatable. This has no purpose other to ensure that * Wraps a [DialogPreference] to be instantiatable. This has no purpose other to ensure that custom
* custom dialog preferences are handled. * dialog preferences are handled.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class WrappedDialogPreference class WrappedDialogPreference

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.shared package org.oxycblt.auxio.ui
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
@ -35,8 +35,8 @@ import org.oxycblt.auxio.util.coordinatorLayoutBehavior
* 1. Lift state failing to update when list data changes. * 1. Lift state failing to update when list data changes.
* 2. Expansion causing jumping in [RecyclerView] instances. * 2. Expansion causing jumping in [RecyclerView] instances.
* *
* Note: This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what * Note: This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what scrolling
* scrolling view to use. Failure to specify this will result in the layout not working. * view to use. Failure to specify this will result in the layout not working.
* *
* Derived from Material Files: https://github.com/zhanghai/MaterialFiles * Derived from Material Files: https://github.com/zhanghai/MaterialFiles
* *
@ -70,8 +70,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/** /**
* Expand this [AppBarLayout] with respect to the given [RecyclerView], preventing it from * Expand this [AppBarLayout] with respect to the given [RecyclerView], preventing it from
* jumping around. * jumping around.
* @param recycler [RecyclerView] to expand with, or null if one is currently unavailable. * @param recycler [RecyclerView] to expand with, or null if one is currently unavailable. TODO:
* TODO: Is it possible to use liftOnScrollTargetViewId to avoid the [RecyclerView] argument? * Is it possible to use liftOnScrollTargetViewId to avoid the [RecyclerView] argument?
*/ */
fun expandWithRecycler(recycler: RecyclerView?) { fun expandWithRecycler(recycler: RecyclerView?) {
setExpanded(true) setExpanded(true)
@ -108,13 +108,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/** /**
* An [AppBarLayout.OnOffsetChangedListener] that will automatically move the given * An [AppBarLayout.OnOffsetChangedListener] that will automatically move the given
* [RecyclerView] as the [AppBarLayout] expands. Should be added right when the view * [RecyclerView] as the [AppBarLayout] expands. Should be added right when the view is
* is expanding. Will be removed automatically. * expanding. Will be removed automatically.
* @param recycler [RecyclerView] to scroll with the [AppBarLayout]. * @param recycler [RecyclerView] to scroll with the [AppBarLayout].
*/ */
private class ExpansionHackListener(private val recycler: RecyclerView) : private class ExpansionHackListener(private val recycler: RecyclerView) :
OnOffsetChangedListener { OnOffsetChangedListener {
private val offsetAnimationMaxEndTime = (AnimationUtils.currentAnimationTimeMillis() + private val offsetAnimationMaxEndTime =
(AnimationUtils.currentAnimationTimeMillis() +
APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION) APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION)
private var currentVerticalOffset: Int? = null private var currentVerticalOffset: Int? = null
@ -123,8 +124,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
AnimationUtils.currentAnimationTimeMillis() > offsetAnimationMaxEndTime) { AnimationUtils.currentAnimationTimeMillis() > offsetAnimationMaxEndTime) {
// AppBarLayout crashes with IndexOutOfBoundsException when a non-last listener // AppBarLayout crashes with IndexOutOfBoundsException when a non-last listener
// removes itself, so we have to do the removal asynchronously. // removes itself, so we have to do the removal asynchronously.
appBarLayout.postOnAnimation { appBarLayout.postOnAnimation { appBarLayout.removeOnOffsetChangedListener(this) }
appBarLayout.removeOnOffsetChangedListener(this) }
} }
// If possible, scroll by the offset delta between this update and the last update. // If possible, scroll by the offset delta between this update and the last update.
@ -137,9 +137,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
companion object { companion object {
/** /** @see AppBarLayout.BaseBehavior.MAX_OFFSET_ANIMATION_DURATION */
* @see AppBarLayout.BaseBehavior.MAX_OFFSET_ANIMATION_DURATION
*/
private const val APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION = 600 private const val APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION = 600
} }
} }

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.shared package org.oxycblt.auxio.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
@ -25,14 +25,11 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /** A [ViewModel] that handles complicated navigation functionality. */
* A [ViewModel] that handles complicated navigation functionality.
*/
class NavigationViewModel : ViewModel() { class NavigationViewModel : ViewModel() {
private val _mainNavigationAction = MutableStateFlow<MainNavigationAction?>(null) private val _mainNavigationAction = MutableStateFlow<MainNavigationAction?>(null)
/** /**
* Flag for navigation within the main navigation graph. Only intended for use by * Flag for navigation within the main navigation graph. Only intended for use by MainFragment.
* MainFragment.
*/ */
val mainNavigationAction: StateFlow<MainNavigationAction?> val mainNavigationAction: StateFlow<MainNavigationAction?>
get() = _mainNavigationAction get() = _mainNavigationAction
@ -47,17 +44,17 @@ class NavigationViewModel : ViewModel() {
private val _exploreNavigationArtists = MutableStateFlow<List<Artist>?>(null) private val _exploreNavigationArtists = MutableStateFlow<List<Artist>?>(null)
/** /**
* Variation of [exploreNavigationItem] for situations where the choice of [Artist] * Variation of [exploreNavigationItem] for situations where the choice of [Artist] to navigate
* to navigate to is ambiguous. Only intended for use by MainFragment, as the resolved * to is ambiguous. Only intended for use by MainFragment, as the resolved choice will
* choice will eventually be assigned to [exploreNavigationItem]. * eventually be assigned to [exploreNavigationItem].
*/ */
val exploreNavigationArtists: StateFlow<List<Artist>?> val exploreNavigationArtists: StateFlow<List<Artist>?>
get() = _exploreNavigationArtists get() = _exploreNavigationArtists
/** /**
* Navigate to something in the main navigation graph. This can be used by UIs in the explore * Navigate to something in the main navigation graph. This can be used by UIs in the explore
* navigation graph to trigger navigation in the higher-level main navigation graph. * navigation graph to trigger navigation in the higher-level main navigation graph. Will do
* Will do nothing if already navigating. * nothing if already navigating.
* @param action The [MainNavigationAction] to perform. * @param action The [MainNavigationAction] to perform.
*/ */
fun mainNavigateTo(action: MainNavigationAction) { fun mainNavigateTo(action: MainNavigationAction) {
@ -81,8 +78,7 @@ class NavigationViewModel : ViewModel() {
/** /**
* Navigate to a given [Music] item. Will do nothing if already navigating. * Navigate to a given [Music] item. Will do nothing if already navigating.
* @param item The [Music] to navigate to. * @param item The [Music] to navigate to. TODO: Extend to song properties???
* TODO: Extend to song properties???
*/ */
fun exploreNavigateTo(item: Music) { fun exploreNavigateTo(item: Music) {
if (_exploreNavigationItem.value != null) { if (_exploreNavigationItem.value != null) {
@ -96,8 +92,8 @@ class NavigationViewModel : ViewModel() {
/** /**
* Navigate to an [Artist] out of a list of [Artist]s, like [exploreNavigateTo]. * Navigate to an [Artist] out of a list of [Artist]s, like [exploreNavigateTo].
* @param artists The [Artist]s to navigate to. In the case of multiple artists, the * @param artists The [Artist]s to navigate to. In the case of multiple artists, the user will
* user will be prompted with a choice on which [Artist] to navigate to. * be prompted with a choice on which [Artist] to navigate to.
*/ */
fun exploreNavigateTo(artists: List<Artist>) { fun exploreNavigateTo(artists: List<Artist>) {
if (_exploreNavigationArtists.value != null) { if (_exploreNavigationArtists.value != null) {
@ -126,8 +122,8 @@ class NavigationViewModel : ViewModel() {
/** /**
* Represents the possible actions within the main navigation graph. This can be used with * Represents the possible actions within the main navigation graph. This can be used with
* [NavigationViewModel] to initiate navigation in the main navigation graph from anywhere * [NavigationViewModel] to initiate navigation in the main navigation graph from anywhere in the
* in the app, including outside the main navigation graph. * app, including outside the main navigation graph.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
sealed class MainNavigationAction { sealed class MainNavigationAction {

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.shared package org.oxycblt.auxio.ui
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -86,8 +86,8 @@ abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
} }
/** /**
* Delegate to automatically create and destroy an object derived from the [ViewBinding]. * Delegate to automatically create and destroy an object derived from the [ViewBinding]. TODO:
* TODO: Phase this out, it's really dumb * Phase this out, it's really dumb
* @param create Block to create the object from the [ViewBinding]. * @param create Block to create the object from the [ViewBinding].
*/ */
fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> { fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
@ -140,9 +140,7 @@ abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
logD("Fragment destroyed") logD("Fragment destroyed")
} }
/** /** Internal implementation of [lifecycleObject]. */
* Internal implementation of [lifecycleObject].
*/
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) { private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
fun populate(binding: VB) { fun populate(binding: VB) {
data = create(binding) data = create(binding)

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.shared package org.oxycblt.auxio.ui
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -76,8 +76,8 @@ abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
} }
/** /**
* Delegate to automatically create and destroy an object derived from the [ViewBinding]. * Delegate to automatically create and destroy an object derived from the [ViewBinding]. TODO:
* TODO: Phase this out, it's really dumb * Phase this out, it's really dumb
* @param create Block to create the object from the [ViewBinding]. * @param create Block to create the object from the [ViewBinding].
*/ */
fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> { fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
@ -121,9 +121,7 @@ abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
logD("Fragment destroyed") logD("Fragment destroyed")
} }
/** /** Internal implementation of [lifecycleObject]. */
* Internal implementation of [lifecycleObject].
*/
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) { private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
fun populate(binding: VB) { fun populate(binding: VB) {
data = create(binding) data = create(binding)

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.settings.accent package org.oxycblt.auxio.ui.accent
import android.os.Build import android.os.Build
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -120,8 +120,7 @@ class Accent private constructor(val index: Int) : Item {
val theme: Int val theme: Int
get() = ACCENT_THEMES[index] get() = ACCENT_THEMES[index]
/** /**
* The black theme resource for this accent. Identical to [theme], but with a black * The black theme resource for this accent. Identical to [theme], but with a black background.
* background.
*/ */
val blackTheme: Int val blackTheme: Int
get() = ACCENT_BLACK_THEMES[index] get() = ACCENT_BLACK_THEMES[index]
@ -137,8 +136,8 @@ class Accent private constructor(val index: Int) : Item {
/** /**
* Create a new instance. * Create a new instance.
* @param index The unique number for this particular accent. * @param index The unique number for this particular accent.
* @return A new [Accent] with the specified [index]. If [index] is not within the * @return A new [Accent] with the specified [index]. If [index] is not within the range of
* range of valid accents, [index] will be [DEFAULT] instead. * valid accents, [index] will be [DEFAULT] instead.
*/ */
fun from(index: Int): Accent { fun from(index: Int): Accent {
if (index !in 0 until MAX) { if (index !in 0 until MAX) {
@ -148,9 +147,7 @@ class Accent private constructor(val index: Int) : Item {
return Accent(index) return Accent(index)
} }
/** /** The default accent. */
* The default accent.
*/
val DEFAULT = val DEFAULT =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Use dynamic coloring on devices that support it. // Use dynamic coloring on devices that support it.
@ -160,9 +157,7 @@ class Accent private constructor(val index: Int) : Item {
5 5
} }
/** /** The amount of valid accents. */
* The amount of valid accents.
*/
val MAX = val MAX =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ACCENT_THEMES.size ACCENT_THEMES.size

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.settings.accent package org.oxycblt.auxio.ui.accent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.settings.accent package org.oxycblt.auxio.ui.accent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -26,7 +26,7 @@ import org.oxycblt.auxio.databinding.DialogAccentBinding
import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -35,7 +35,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* A [ViewBindingDialogFragment] that allows the user to configure the current [Accent]. * A [ViewBindingDialogFragment] that allows the user to configure the current [Accent].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>(), ClickableListListener { class AccentCustomizeDialog :
ViewBindingDialogFragment<DialogAccentBinding>(), ClickableListListener {
private var accentAdapter = AccentAdapter(this) private var accentAdapter = AccentAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.settings.accent package org.oxycblt.auxio.ui.accent
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
@ -27,8 +27,8 @@ import org.oxycblt.auxio.util.getDimenPixels
/** /**
* A [GridLayoutManager] that automatically sets the span size in order to use the most possible * A [GridLayoutManager] that automatically sets the span size in order to use the most possible
* space in the [RecyclerView]. * space in the [RecyclerView]. Derived from this StackOverflow answer:
* Derived from this StackOverflow answer: https://stackoverflow.com/a/30256880/14143986 * https://stackoverflow.com/a/30256880/14143986
*/ */
class AccentGridLayoutManager( class AccentGridLayoutManager(
context: Context, context: Context,

View file

@ -47,17 +47,13 @@ import org.oxycblt.auxio.MainActivity
val Context.inflater: LayoutInflater val Context.inflater: LayoutInflater
get() = LayoutInflater.from(this) get() = LayoutInflater.from(this)
/** /** Whether the device is in night mode or not. */
* Whether the device is in night mode or not.
*/
val Context.isNight val Context.isNight
get() = get() =
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES Configuration.UI_MODE_NIGHT_YES
/** /** Whether the device is in landscape mode or not. */
* Whether the device is in landscape mode or not.
*/
val Context.isLandscape val Context.isLandscape
get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
@ -152,9 +148,7 @@ fun Context.showToast(@StringRes stringRes: Int) {
Toast.makeText(applicationContext, getString(stringRes), Toast.LENGTH_SHORT).show() Toast.makeText(applicationContext, getString(stringRes), Toast.LENGTH_SHORT).show()
} }
/** /** Create a [PendingIntent] that will launch the app activity when launched. */
* Create a [PendingIntent] that will launch the app activity when launched.
*/
fun Context.newMainPendingIntent(): PendingIntent = fun Context.newMainPendingIntent(): PendingIntent =
PendingIntent.getActivity( PendingIntent.getActivity(
this, this,

View file

@ -50,11 +50,10 @@ import kotlinx.coroutines.launch
* Get if this [View] contains the given [PointF], with optional leeway. * Get if this [View] contains the given [PointF], with optional leeway.
* @param x The x value of the point to check. * @param x The x value of the point to check.
* @param y The y value of the point to check. * @param y The y value of the point to check.
* @param minTouchTargetSize A minimum size to use when checking the value. * @param minTouchTargetSize A minimum size to use when checking the value. This can be used to
* This can be used to extend the range where a point is considered "contained" * extend the range where a point is considered "contained" by the [View] beyond it's actual size.
* by the [View] beyond it's actual size. * @return true if the [PointF] is contained by the view, false otherwise. Adapted from
* @return true if the [PointF] is contained by the view, false otherwise. * AndroidFastScroll: https://github.com/zhanghai/AndroidFastScroll
* Adapted from AndroidFastScroll: https://github.com/zhanghai/AndroidFastScroll
*/ */
fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) = fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) =
isUnderImpl(x, left, right, (parent as View).width, minTouchTargetSize) && isUnderImpl(x, left, right, (parent as View).width, minTouchTargetSize) &&
@ -66,8 +65,7 @@ fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) =
* @param viewStart The start of the view bounds, on the same axis as [position]. * @param viewStart The start of the view bounds, on the same axis as [position].
* @param viewEnd The end of the view bounds, on the same axis as [position] * @param viewEnd The end of the view bounds, on the same axis as [position]
* @param parentEnd The end of the parent bounds, on the same axis as [position]. * @param parentEnd The end of the parent bounds, on the same axis as [position].
* @param minTouchTargetSize The minimum size to use when checking if the value is * @param minTouchTargetSize The minimum size to use when checking if the value is in range.
* in range.
*/ */
private fun isUnderImpl( private fun isUnderImpl(
position: Float, position: Float,
@ -98,27 +96,21 @@ private fun isUnderImpl(
return position >= touchTargetStart && position < touchTargetEnd return position >= touchTargetStart && position < touchTargetEnd
} }
/** /** Whether this [View] is using an RTL layout direction. */
* Whether this [View] is using an RTL layout direction.
*/
val View.isRtl: Boolean val View.isRtl: Boolean
get() = layoutDirection == View.LAYOUT_DIRECTION_RTL get() = layoutDirection == View.LAYOUT_DIRECTION_RTL
/** /** Whether this [Drawable] is using an RTL layout direction. */
* Whether this [Drawable] is using an RTL layout direction.
*/
val Drawable.isRtl: Boolean val Drawable.isRtl: Boolean
get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
/** /** Get a [Context] from a [ViewBinding]'s root [View]. */
* Get a [Context] from a [ViewBinding]'s root [View].
*/
val ViewBinding.context: Context val ViewBinding.context: Context
get() = root.context get() = root.context
/** /**
* Compute if this [RecyclerView] can scroll through their items, or if the items can all fit on * Compute if this [RecyclerView] can scroll through their items, or if the items can all fit on one
* one screen. * screen.
*/ */
fun RecyclerView.canScroll() = computeVerticalScrollRange() > height fun RecyclerView.canScroll() = computeVerticalScrollRange() > height
@ -131,8 +123,8 @@ val View.coordinatorLayoutBehavior: CoordinatorLayout.Behavior<View>?
/** /**
* Collect a [StateFlow] into [block] in a lifecycle-aware manner *eventually.* Due to co-routine * Collect a [StateFlow] into [block] in a lifecycle-aware manner *eventually.* Due to co-routine
* launching, the initializing call will occur ~100ms after draw time. If this is not desirable, * launching, the initializing call will occur ~100ms after draw time. If this is not desirable, use
* use [collectImmediately]. * [collectImmediately].
* @param stateFlow The [StateFlow] to collect. * @param stateFlow The [StateFlow] to collect.
* @param block The code to run when the [StateFlow] updates. * @param block The code to run when the [StateFlow] updates.
*/ */
@ -142,8 +134,8 @@ fun <T> Fragment.collect(stateFlow: StateFlow<T>, block: (T) -> Unit) {
/** /**
* Collect a [StateFlow] into a [block] in a lifecycle-aware manner *immediately.* This will * Collect a [StateFlow] into a [block] in a lifecycle-aware manner *immediately.* This will
* immediately run an initializing call to ensure the UI is set up before draw-time. Note * immediately run an initializing call to ensure the UI is set up before draw-time. Note that this
* that this will result in two initializing calls. * will result in two initializing calls.
* @param stateFlow The [StateFlow] to collect. * @param stateFlow The [StateFlow] to collect.
* @param block The code to run when the [StateFlow] updates. * @param block The code to run when the [StateFlow] updates.
*/ */
@ -153,8 +145,8 @@ fun <T> Fragment.collectImmediately(stateFlow: StateFlow<T>, block: (T) -> Unit)
} }
/** /**
* Like [collectImmediately], but with two [StateFlow] instances that are collected * Like [collectImmediately], but with two [StateFlow] instances that are collected with the same
* with the same block. * block.
* @param a The first [StateFlow] to collect. * @param a The first [StateFlow] to collect.
* @param b The second [StateFlow] to collect. * @param b The second [StateFlow] to collect.
* @param block The code to run when either [StateFlow] updates. * @param block The code to run when either [StateFlow] updates.
@ -173,8 +165,8 @@ fun <T1, T2> Fragment.collectImmediately(
} }
/** /**
* Like [collectImmediately], but with three [StateFlow] instances that are collected * Like [collectImmediately], but with three [StateFlow] instances that are collected with the same
* with the same block. * block.
* @param a The first [StateFlow] to collect. * @param a The first [StateFlow] to collect.
* @param b The second [StateFlow] to collect. * @param b The second [StateFlow] to collect.
* @param c The third [StateFlow] to collect. * @param c The third [StateFlow] to collect.
@ -192,9 +184,9 @@ fun <T1, T2, T3> Fragment.collectImmediately(
} }
/** /**
* Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State]. * Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State]. This
* This should always been used when launching [Fragment] co-routines was it will not result * should always been used when launching [Fragment] co-routines was it will not result in
* in unexpected behavior. * unexpected behavior.
* @param state The [Lifecycle.State] to launch the co-routine in. * @param state The [Lifecycle.State] to launch the co-routine in.
* @param block The block to run in the co-routine. * @param block The block to run in the co-routine.
* @see repeatOnLifecycle * @see repeatOnLifecycle
@ -208,40 +200,36 @@ private fun Fragment.launch(
/** /**
* An extension to [viewModels] that automatically provides an * An extension to [viewModels] that automatically provides an
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] * [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used.
* is used.
*/ */
inline fun <reified T : AndroidViewModel> Fragment.androidViewModels() = inline fun <reified T : AndroidViewModel> Fragment.androidViewModels() =
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) } viewModels<T> { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) }
/** /**
* An extension to [viewModels] that automatically provides an * An extension to [viewModels] that automatically provides an
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] * [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used. Note
* is used. Note that this implementation is for an [AppCompatActivity], and thus * that this implementation is for an [AppCompatActivity], and thus makes this functionally
* makes this functionally equivalent in scope to [androidActivityViewModels]. * equivalent in scope to [androidActivityViewModels].
*/ */
inline fun <reified T : AndroidViewModel> AppCompatActivity.androidViewModels() = inline fun <reified T : AndroidViewModel> AppCompatActivity.androidViewModels() =
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(application) } viewModels<T> { ViewModelProvider.AndroidViewModelFactory(application) }
/** /**
* An extension to [activityViewModels] that automatically provides an * An extension to [activityViewModels] that automatically provides an
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] * [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used.
* is used.
*/ */
inline fun <reified T : AndroidViewModel> Fragment.androidActivityViewModels() = inline fun <reified T : AndroidViewModel> Fragment.androidActivityViewModels() =
activityViewModels<T> { activityViewModels<T> {
ViewModelProvider.AndroidViewModelFactory(requireActivity().application) ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
} }
/** /** The [Context] provided to an [AndroidViewModel]. */
* The [Context] provided to an [AndroidViewModel].
*/
inline val AndroidViewModel.context: Context inline val AndroidViewModel.context: Context
get() = getApplication() get() = getApplication()
/** /**
* Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor] * Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor] is
* is loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor] * loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor]
* resources. * resources.
* @param tableName The name of the table to query all columns in. * @param tableName The name of the table to query all columns in.
* @param block The code block to run with the loaded [Cursor]. * @param block The code block to run with the loaded [Cursor].
@ -250,8 +238,8 @@ inline fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R)
query(tableName, null, null, null, null, null, null)?.use(block) query(tableName, null, null, null, null, null, null)?.use(block)
/** /**
* Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner * Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner This
* This can be used to prevent [View] elements from intersecting with the navigation bars. * can be used to prevent [View] elements from intersecting with the navigation bars.
*/ */
val WindowInsets.systemBarInsetsCompat: Insets val WindowInsets.systemBarInsetsCompat: Insets
get() = get() =
@ -266,9 +254,9 @@ val WindowInsets.systemBarInsetsCompat: Insets
/** /**
* Get the "System Gesture" [Insets] in this [WindowInsets] instance in a version-compatible manner * Get the "System Gesture" [Insets] in this [WindowInsets] instance in a version-compatible manner
* This can be used to prevent [View] elements from intersecting with the navigation bars and * This can be used to prevent [View] elements from intersecting with the navigation bars and their
* their extended gesture hit-boxes. Note that "System Bar" insets will be used if the system * extended gesture hit-boxes. Note that "System Bar" insets will be used if the system does not
* does not provide gesture insets. * provide gesture insets.
*/ */
val WindowInsets.systemGestureInsetsCompat: Insets val WindowInsets.systemGestureInsetsCompat: Insets
get() = get() =

View file

@ -53,8 +53,8 @@ fun Long.nonZeroOrNull() = if (this > 0) this else null
fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null
/** /**
* Lazily set up a reflected field. Automatically handles visibility changes. * Lazily set up a reflected field. Automatically handles visibility changes. Adapted from Material
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles * Files: https://github.com/zhanghai/MaterialFiles
* @param clazz The [KClass] to reflect into. * @param clazz The [KClass] to reflect into.
* @param field The name of the field to obtain. * @param field The name of the field to obtain.
*/ */
@ -62,8 +62,8 @@ fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {
clazz.java.getDeclaredField(field).also { it.isAccessible = true } clazz.java.getDeclaredField(field).also { it.isAccessible = true }
} }
/** /**
* Lazily set up a reflected method. Automatically handles visibility changes. * Lazily set up a reflected method. Automatically handles visibility changes. Adapted from Material
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles * Files: https://github.com/zhanghai/MaterialFiles
* @param clazz The [KClass] to reflect into. * @param clazz The [KClass] to reflect into.
* @param field The name of the method to obtain. * @param field The name of the method to obtain.
*/ */
@ -72,9 +72,9 @@ fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy {
} }
/** /**
* Assert that the execution is currently on a background thread. This is helpful for * Assert that the execution is currently on a background thread. This is helpful for functions that
* functions that don't necessarily require suspend, but still want to ensure that they * don't necessarily require suspend, but still want to ensure that they are being called with a
* are being called with a co-routine. * co-routine.
* @throws IllegalStateException If the execution is not on a background thread. * @throws IllegalStateException If the execution is not on a background thread.
*/ */
fun requireBackgroundThread() { fun requireBackgroundThread() {

View file

@ -52,15 +52,15 @@ fun Any.logW(msg: String) = Log.w(autoTag, msg)
fun Any.logE(msg: String) = Log.e(autoTag, msg) fun Any.logE(msg: String) = Log.e(autoTag, msg)
/** /**
* The LogCat-suitable tag for this string. Consists of the object's name, or "Anonymous Object" * The LogCat-suitable tag for this string. Consists of the object's name, or "Anonymous Object" if
* if the object does not exist. * the object does not exist.
*/ */
private val Any.autoTag: String private val Any.autoTag: String
get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}" get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}"
/** /**
* Please don't plagiarize Auxio! * Please don't plagiarize Auxio! You are free to remove this as long as you continue to keep your
* You are free to remove this as long as you continue to keep your source open. * source open.
*/ */
@Suppress("KotlinConstantConditions") @Suppress("KotlinConstantConditions")
private fun copyleftNotice(): Boolean { private fun copyleftNotice(): Boolean {

View file

@ -35,9 +35,8 @@ import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
* A component that manages the "Now Playing" state. * A component that manages the "Now Playing" state. This is kept separate from the [WidgetProvider]
* This is kept separate from the [WidgetProvider] itself to prevent possible memory * itself to prevent possible memory leaks and enable extension to more widgets in the future.
* leaks and enable extension to more widgets in the future.
* @param context [Context] required to manage AppWidgetProviders. * @param context [Context] required to manage AppWidgetProviders.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@ -52,9 +51,7 @@ class WidgetComponent(private val context: Context) :
playbackManager.addCallback(this) playbackManager.addCallback(this)
} }
/** /** Update [WidgetProvider] with the current playback state. */
* Update [WidgetProvider] with the current playback state.
*/
fun update() { fun update() {
val song = playbackManager.song val song = playbackManager.song
if (song == null) { if (song == null) {
@ -104,9 +101,7 @@ class WidgetComponent(private val context: Context) :
}) })
} }
/** /** Release this instance, preventing any further events from updating the widget instances. */
* Release this instance, preventing any further events from updating the widget instances.
*/
fun release() { fun release() {
provider.release() provider.release()
settings.release() settings.release()

Some files were not shown because too many files have changed in this diff Show more