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()
companion object {
/**
* The ID of the "Shuffle All" shortcut.
*/
/** The ID of the "Shuffle All" shortcut. */
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"
}
}

View file

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

View file

@ -113,10 +113,9 @@ class MainActivity : AppCompatActivity() {
}
/**
* Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action]
* that can be used in the playback system.
* @param intent The (new) [Intent] given to this [MainActivity], or null if there
* is no intent.
* Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action] that can be used
* in the playback system.
* @param intent The (new) [Intent] given to this [MainActivity], or null if there is no intent.
* @return true If the analogous [InternalPlayer.Action] to the given [Intent] was started,
* 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.PlaybackViewModel
import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior
import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.*
/**
@ -348,8 +348,8 @@ class MainFragment :
}
/**
* A [OnBackPressedCallback] that overrides the back button to first navigate out of
* internal app components, such as the Bottom Sheets or Explore Navigation.
* A [OnBackPressedCallback] that overrides the back button to first navigate out of internal
* app components, such as the Bottom Sheets or Explore Navigation.
*/
private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) {
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
* components that the back button should close first, the instance is disabled and
* back navigation is delegated to the system.
* Force this instance to update whether it's enabled or not. If there are no app components
* that the back button should close first, the instance is disabled and back navigation is
* delegated to the system.
*
* Normally, this callback would have just called the [MainActivity.onBackPressed]
* if there were no components to close, but that prevents adaptive back navigation
* from working on Android 14+, so we must do it this way.
* Normally, this callback would have just called the [MainActivity.onBackPressed] if there
* were no components to close, but that prevents adaptive back navigation from working on
* Android 14+, so we must do it this way.
*/
fun invalidateEnabled() {
val binding = requireBinding()
@ -397,7 +397,7 @@ class MainFragment :
isEnabled =
queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
exploreNavController.currentDestination?.id !=
exploreNavController.graph.startDestinationId
}

View file

@ -174,7 +174,6 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists)
}
private fun updateAlbum(album: Album?) {
if (album == null) {
// 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
// as a UID, as that is the only safe way to parcel an artist.
private val args: ArtistDetailFragmentArgs by navArgs()
private val detailAdapter =
ArtistDetailAdapter(this)
private val detailAdapter = ArtistDetailAdapter(this)
override fun onCreate(savedInstanceState: Bundle?) {
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
import androidx.annotation.StringRes
@ -35,4 +52,4 @@ data class DetailSong(val song: Song, val properties: Properties?) {
val sampleRateHz: Int?,
val resolvedMimeType: MimeType
)
}
}

View file

@ -31,7 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
import java.lang.reflect.Field
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.lazyReflectedField
@ -75,12 +75,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// 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.
val newTitleView = (TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply {
// 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
// animation..
alpha = 0f
}
val newTitleView =
(TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply {
// 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
// animation..
alpha = 0f
}
this.titleView = newTitleView
return newTitleView
@ -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
// (i.e the header)
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.*
/**
* [AndroidViewModel] that manages the Song, Album, Artist, and Genre detail views.
* Keeps track of the current item they are showing, sub-data to display, and configuration.
* Since this ViewModel requires a context, it must be instantiated [AndroidViewModel]'s Factory.
* [AndroidViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of
* the current item they are showing, sub-data to display, and configuration. Since this ViewModel
* requires a context, it must be instantiated [AndroidViewModel]'s Factory.
* @param application [Application] context required to initialize certain information.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -61,8 +61,8 @@ class DetailViewModel(application: Application) :
private val _currentSong = MutableStateFlow<DetailSong?>(null)
/**
* The current [DetailSong] to display. Null if there is nothing to show.
* TODO: De-couple Song and Properties?
* The current [DetailSong] to display. Null if there is nothing to show. TODO: De-couple Song
* and Properties?
*/
val currentSong: StateFlow<DetailSong?>
get() = _currentSong
@ -224,7 +224,7 @@ class DetailViewModel(application: Application) :
* @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid.
*/
fun setGenreUid(uid: Music.UID) {
if (_currentGenre.value?.uid == uid) {
if (_currentGenre.value?.uid == uid) {
// Nothing to do.
return
}
@ -232,7 +232,7 @@ class DetailViewModel(application: Application) :
_currentGenre.value = requireMusic<Genre>(uid).also { refreshGenreList(it) }
}
private fun <T: Music> requireMusic(uid: Music.UID): T =
private fun <T : Music> requireMusic(uid: Music.UID): T =
requireNotNull(unlikelyToBeNull(musicStore.library).find(uid)) { "Invalid id provided" }
/**
@ -392,8 +392,8 @@ class DetailViewModel(application: Application) :
/**
* A simpler mapping of [Album.Type] used for grouping and sorting songs.
* @param headerTitleRes The title string resource to use for a header created
* out of an instance of this enum.
* @param headerTitleRes The title string resource to use for a header created out of an
* instance of this enum.
*/
private enum class AlbumGrouping(@StringRes val headerTitleRes: Int) {
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
// as a UID, as that is the only safe way to parcel an genre.
private val args: GenreDetailFragmentArgs by navArgs()
private val detailAdapter =
GenreDetailAdapter(this)
private val detailAdapter = GenreDetailAdapter(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View file

@ -25,8 +25,8 @@ import com.google.android.material.textfield.TextInputEditText
import org.oxycblt.auxio.R
/**
* A [TextInputEditText] that deliberately restricts all input except for selection. This will
* work just like a normal block of selectable/copyable text, but with nicer aesthetics.
* A [TextInputEditText] that deliberately restricts all input except for selection. This will work
* just like a normal block of selectable/copyable text, but with nicer aesthetics.
*
* 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.databinding.DialogSongDetailBinding
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.collectImmediately

View file

@ -27,8 +27,8 @@ import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
import org.oxycblt.auxio.detail.DiscHeader
import org.oxycblt.auxio.list.SelectableListListener
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.SimpleItemCallback
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.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.SelectableListListener
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.SimpleItemCallback
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.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.detail.SortHeader
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.recycler.*
import org.oxycblt.auxio.util.context
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.Sort
import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.*
/**
@ -100,8 +100,7 @@ class HomeFragment :
override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
override fun getSelectionToolbar(binding: FragmentHomeBinding) =
binding.homeSelectionToolbar
override fun getSelectionToolbar(binding: FragmentHomeBinding) = binding.homeSelectionToolbar
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
@ -239,7 +238,8 @@ class HomeFragment :
}
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
if (homeModel.currentTabModes.size == 1) {
@ -256,32 +256,36 @@ class HomeFragment :
}
// Set up the mapping between the ViewPager and TabLayout.
TabLayoutMediator(binding.homeTabs, binding.homePager,
AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes)).attach()
TabLayoutMediator(
binding.homeTabs,
binding.homePager,
AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes))
.attach()
}
private fun updateCurrentTab(tabMode: MusicMode) {
// Update the sort options to align with those allowed by the tab
val isVisible: (Int) -> Boolean = when (tabMode) {
// Disallow sorting by count for songs
MusicMode.SONGS -> { id -> id != R.id.option_sort_count }
// Disallow sorting by album for albums
MusicMode.ALBUMS -> { id -> id != R.id.option_sort_album }
// Only allow sorting by name, count, and duration for artists
MusicMode.ARTISTS -> { id ->
id == R.id.option_sort_asc ||
id == R.id.option_sort_name ||
id == R.id.option_sort_count ||
id == R.id.option_sort_duration
val isVisible: (Int) -> Boolean =
when (tabMode) {
// Disallow sorting by count for songs
MusicMode.SONGS -> { id -> id != R.id.option_sort_count }
// Disallow sorting by album for albums
MusicMode.ALBUMS -> { id -> id != R.id.option_sort_album }
// Only allow sorting by name, count, and duration for artists
MusicMode.ARTISTS -> { id ->
id == R.id.option_sort_asc ||
id == R.id.option_sort_name ||
id == R.id.option_sort_count ||
id == R.id.option_sort_duration
}
// Only allow sorting by name, count, and duration for genres
MusicMode.GENRES -> { id ->
id == R.id.option_sort_asc ||
id == R.id.option_sort_name ||
id == R.id.option_sort_count ||
id == R.id.option_sort_duration
}
}
// Only allow sorting by name, count, and duration for genres
MusicMode.GENRES -> { id ->
id == R.id.option_sort_asc ||
id == R.id.option_sort_name ||
id == R.id.option_sort_count ||
id == R.id.option_sort_duration
}
}
val sortMenu = requireNotNull(sortItem.subMenu)
val toHighlight = homeModel.getSortForTab(tabMode)
@ -289,8 +293,9 @@ class HomeFragment :
for (option in sortMenu) {
// Check the ascending option and corresponding sort option to align with
// the current sort of the tab.
option.isChecked = option.itemId == toHighlight.mode.itemId
|| (option.itemId == R.id.option_sort_asc && toHighlight.isAscending)
option.isChecked =
option.itemId == toHighlight.mode.itemId ||
(option.itemId == R.id.option_sort_asc && toHighlight.isAscending)
// Disable options that are not allowed by the isVisible lambda
option.isVisible = isVisible(option.itemId)
@ -454,8 +459,8 @@ class HomeFragment :
}
/**
* Get the ID of the RecyclerView contained by [ViewPager2] tab represented with
* the given [MusicMode].
* Get the ID of the RecyclerView contained by [ViewPager2] tab represented with the given
* [MusicMode].
* @param tabMode The [MusicMode] of the tab.
* @return The ID of the RecyclerView contained by the given tab.
*/
@ -478,8 +483,7 @@ class HomeFragment :
private val tabs: List<MusicMode>,
fragmentManager: FragmentManager,
lifecycleOwner: LifecycleOwner
) :
FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle ) {
) : FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle) {
override fun getItemCount() = tabs.size

View file

@ -44,60 +44,50 @@ class HomeViewModel(application: Application) :
private val settings = Settings(application, this)
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>>
get() = _songsList
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>>
get() = _albumsLists
private val _artistsList = MutableStateFlow(listOf<Artist>())
/**
* A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view.
* Note that if "Hide collaborators" is on, this list will not include [Artist]s
* where [Artist.isCollaborator] is true.
* A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. Note that
* if "Hide collaborators" is on, this list will not include [Artist]s where
* [Artist.isCollaborator] is true.
*/
val artistsList: MutableStateFlow<List<Artist>>
get() = _artistsList
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>>
get() = _genresList
/**
* A list of [MusicMode] corresponding to the current [Tab] configuration, excluding
* invisible [Tab]s.
* A list of [MusicMode] corresponding to the current [Tab] configuration, excluding invisible
* [Tab]s.
*/
var currentTabModes: List<MusicMode> = makeTabModes()
private set
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
private val _shouldRecreate = MutableStateFlow(false)
/**
* A marker to re-create all library tabs, usually initiated by a settings change.
* When this flag is true, all tabs (and their respective ViewPager2 fragments) will be
* re-created from scratch.
* A marker to re-create all library tabs, usually initiated by a settings change. When this
* flag is true, all tabs (and their respective ViewPager2 fragments) will be re-created from
* scratch.
*/
val shouldRecreate: StateFlow<Boolean> = _shouldRecreate
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
init {
@ -136,7 +126,6 @@ class HomeViewModel(application: Application) :
currentTabModes = makeTabModes()
_shouldRecreate.value = true
}
context.getString(R.string.set_key_hide_collaborators) -> {
// Changes in the hide collaborator setting will change the artist contents
// 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.
* @return A list of the [MusicMode]s for each visible [Tab] in the configuration,
* ordered in the same way as the configuration.
* @return A list of the [MusicMode]s for each visible [Tab] in the configuration, ordered in
* the same way as the configuration.
*/
private fun makeTabModes() =
settings.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
private fun makeTabModes() = 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.graphics.Canvas
import android.graphics.PointF
import android.graphics.Rect
import android.util.AttributeSet
import android.view.Gravity
@ -72,22 +71,18 @@ class FastScrollRecyclerView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
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 {
/**
* Get text to use in the popup at the specified position.
* @param pos The position in the list.
* @return A [String] to use in the popup. Null if there is no applicable text for
* the popup at [pos].
* @return A [String] to use in the popup. Null if there is no applicable text for the popup
* at [pos].
*/
fun getPopup(pos: Int): String?
}
/**
* A listener for fast scroller interactions.
*/
/** A listener for fast scroller interactions. */
interface Listener {
/**
* 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.
* @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 albumAdapter = AlbumAdapter(this)
// 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.
* @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 homeAdapter = ArtistAdapter(this)

View file

@ -43,7 +43,10 @@ import org.oxycblt.auxio.util.collectImmediately
* A [ListFragment] that shows a list of [Genre]s.
* @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 homeAdapter = GenreAdapter(this)

View file

@ -47,7 +47,10 @@ import org.oxycblt.auxio.util.collectImmediately
* A [ListFragment] that shows a list of [Song]s.
* @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 homeAdapter = SongAdapter(this)
// 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.TabLayoutMediator
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.MusicMode
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)
/**
* A visible tab. This will be visible in the tab configuration view, but not in the
* home view.
* A visible tab. This will be visible in the tab configuration view, but not in the home view.
* @param mode The type of list in the home view this instance corresponds to.
*/
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
/**
* The default tab sequence, in integer form.
* This represents a set of four visible tabs ordered as "Song", "Album", "Artist", and
* "Genre".
* The default tab sequence, in integer form. This represents a set of four visible tabs
* ordered as "Song", "Album", "Artist", and "Genre".
*/
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. */
interface Listener {
/**
* Called when a tab is clicked, requesting that the visibility should be inverted
* (i.e Visible -> Invisible and vice versa).
* Called when a tab is clicked, requesting that the visibility should be inverted (i.e
* Visible -> Invisible and vice versa).
* @param tabMode The [MusicMode] of the tab clicked.
*/
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.music.MusicMode
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.logD

View file

@ -30,7 +30,7 @@ class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callbac
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) = // Allow dragging up and down only
makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN)
makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN)
override fun onChildDraw(
c: Canvas,

View file

@ -31,25 +31,26 @@ import org.oxycblt.auxio.music.Song
* A utility to provide bitmaps in a race-less manner.
*
* 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
* drawn. This utility resolves this by keeping track of the current request, and disposing
* it as soon as a new request is queued or if another, competing request is newer.
* [ImageRequest]s may cause a race condition that results in the incorrect image being drawn. This
* utility resolves this by keeping track of the current request, and disposing it as soon as a new
* request is queued or if another, competing request is newer.
*
* @param context [Context] required to load images.
* @author Alexander Capehart (OxygenCobalt)
*/
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)
/** The target that will receive the requested [Bitmap]. */
interface Target {
/**
* Configure the [ImageRequest.Builder] to enable [Target]-specific configuration.
* @param builder The [ImageRequest.Builder] that will be used to request the
* desired [Bitmap].
* @return The same [ImageRequest.Builder] in order to easily chain configuration
* methods.
* @param builder The [ImageRequest.Builder] that will be used to request the desired
* [Bitmap].
* @return The same [ImageRequest.Builder] in order to easily chain configuration methods.
*/
fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder
@ -80,39 +81,38 @@ class BitmapProvider(private val context: Context) {
currentRequest = null
val imageRequest =
target.onConfigRequest(
ImageRequest.Builder(context)
.data(song)
// Use ORIGINAL sizing, as we are not loading into any View-like component.
.size(Size.ORIGINAL)
.transformations(SquareFrameTransform.INSTANCE))
// Override the target in order to deliver the bitmap to the given
// callback.
.target(
onSuccess = {
synchronized(this) {
if (currentHandle == handle) {
// Has not been superceded by a new request, can deliver
// this result.
target.onCompleted(it.toBitmap())
}
target
.onConfigRequest(
ImageRequest.Builder(context)
.data(song)
// Use ORIGINAL sizing, as we are not loading into any View-like component.
.size(Size.ORIGINAL)
.transformations(SquareFrameTransform.INSTANCE))
// Override the target in order to deliver the bitmap to the given
// callback.
.target(
onSuccess = {
synchronized(this) {
if (currentHandle == handle) {
// Has not been superceded by a new request, can deliver
// this result.
target.onCompleted(it.toBitmap())
}
},
onError = {
synchronized(this) {
if (currentHandle == handle) {
// Has not been superceded by a new request, can deliver
// this result.
target.onCompleted(null)
}
}
},
onError = {
synchronized(this) {
if (currentHandle == handle) {
// Has not been superceded by a new request, can deliver
// this result.
target.onCompleted(null)
}
})
}
})
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
fun release() {
++currentHandle

View file

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

View file

@ -33,8 +33,8 @@ import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat
/**
* A view that displays an activation (i.e playback) indicator, with an accented styling and
* an animated equalizer icon.
* A view that displays an activation (i.e playback) indicator, with an accented styling and an
* animated equalizer icon.
*
* This is only meant for use with [ImageGroup]. Due to limitations with [AnimationDrawable]
* 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)
/**
* The corner radius of this view. This allows the outer ImageGroup to apply it's
* corner radius to this view without any attribute hacks.
* The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius
* to this view without any attribute hacks.
*/
var cornerRadius = 0f
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,
* the animated playing icon will be shown. If false, the static paused icon will be shown.
* Whether this view should be indicated to have ongoing playback or not. If true, the animated
* playing icon will be shown. If false, the static paused icon will be shown.
*/
var isPlaying: Boolean
get() = drawable == playingIndicatorDrawable

View file

@ -48,8 +48,8 @@ import org.oxycblt.auxio.util.getDrawableCompat
*
* - Tonal background
* - Rounded corners based on user preferences
* - Built-in support for binding image data or using a static icon with the same
* styling as placeholder drawables.
* - Built-in support for binding image data or using a static icon with the same styling as
* placeholder drawables.
*
* @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.
* @param music The music to find.
* @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
* one field for the name of the [Music].
* @param descRes The content description string resource to use. The resource must have one
* field for the name of the [Music].
*/
private fun bindImpl(music: Music, @DrawableRes errorRes: Int, @StringRes descRes: Int) {
// 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
* of [StyledImageView].
* A [Drawable] wrapper that re-styles the drawable to better align with the style of
* [StyledImageView].
* @param context [Context] required for initialization.
* @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].
* Use [SongFactory] or [AlbumFactory] for instantiation.
* Generic [Fetcher] for [Album] covers. Works with both [Album] and [Song]. Use [SongFactory] or
* [AlbumFactory] for instantiation.
* @author Alexander Capehart (OxygenCobalt)
*/
class AlbumCoverFetcher
@ -66,7 +66,7 @@ private constructor(private val context: Context, private val album: Album) : Fe
dataSource = DataSource.DISK)
}
/** A [Fetcher.Factory] implementation that works with [Song]s.*/
/** A [Fetcher.Factory] implementation that works with [Song]s. */
class SongFactory : Fetcher.Factory<Song> {
override fun create(data: Song, options: Options, imageLoader: ImageLoader) =
AlbumCoverFetcher(options.context, data.album)
@ -129,8 +129,8 @@ private constructor(
* Map at most N [T] items a collection into a collection of [R], ignoring [T] that cannot be
* transformed into [R].
* @param n The maximum amount of items to map.
* @param transform The function that transforms data [T] from the original list into
* data [R] in the new list. Can return null if the [T] cannot be transformed into an [R].
* @param transform The function that transforms data [T] from the original list into data [R] in
* 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.
*/
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
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.metadata.flac.PictureFrame
import com.google.android.exoplayer2.metadata.id3.ApicFrame
import java.io.ByteArrayInputStream
import java.io.InputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
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.util.logD
import org.oxycblt.auxio.util.logW
import java.io.ByteArrayInputStream
import java.io.InputStream
/**
* Internal utilities for loading album covers.
@ -26,8 +43,8 @@ object Covers {
* Fetch an album cover, respecting the current cover configuration.
* @param context [Context] required to load the image.
* @param album [Album] to load the cover from.
* @return An [InputStream] of image data if the cover loading was successful, null if the
* cover loading failed or should not occur.
* @return An [InputStream] of image data if the cover loading was successful, null if the cover
* loading failed or should not occur.
*/
suspend fun fetch(context: Context, album: Album): InputStream? {
val settings = Settings(context)
@ -45,8 +62,8 @@ object Covers {
}
/**
* Load an [Album] cover directly from one of it's Song files. This attempts
* the following in order:
* Load an [Album] cover directly from one of it's Song files. This attempts the following in
* order:
* - [MediaMetadataRetriever], as it has the best support and speed.
* - ExoPlayer's [MetadataRetriever], as some devices (notably Samsung) can have broken
* [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
import android.content.Context
@ -14,9 +31,9 @@ import coil.fetch.SourceResult
import coil.size.Dimension
import coil.size.Size
import coil.size.pxOrElse
import java.io.InputStream
import okio.buffer
import okio.source
import java.io.InputStream
/**
* Utilities for constructing Artist and Genre images.
@ -24,8 +41,8 @@ import java.io.InputStream
*/
object Images {
/**
* Create a mosaic image from the given image [InputStream]s.
* Derived from phonograph: https://github.com/kabouzeid/Phonograph
* Create a mosaic image from the given image [InputStream]s. Derived from phonograph:
* https://github.com/kabouzeid/Phonograph
* @param context [Context] required to generate the mosaic.
* @param streams [InputStream]s of image data to create the mosaic out of.
* @param size [Size] of the Mosaic to generate.
@ -94,4 +111,4 @@ object Images {
val size = pxOrElse { 512 }
return if (size.mod(2) > 0) size + 1 else size
}
}
}

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
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
/**

View file

@ -27,8 +27,8 @@ import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.selection.SelectionFragment
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.util.logD
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
* more or less corresponds to an [onClick] implementation in a non-[ListFragment].
* Called when [onClick] is called, but does not result in the item being selected. This more or
* less corresponds to an [onClick] implementation in a non-[ListFragment].
* @param music The [Music] item that was clicked.
*/
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
* closed when the view is destroyed. If a menu is already opened, this call is ignored.
* Opens a menu in the context of a [Song]. This menu will be managed by the Fragment and closed
* 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 menuRes The resource of the menu to load.
* @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
* destroyed. If a menu is already opened, this call is ignored.
* Open a menu. This menu will be managed by the Fragment and closed 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 menuRes The resource of the menu to load.
* @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
import android.view.MotionEvent
import android.view.View
import android.widget.Button
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
/**
* A basic listener for list interactions.
* TODO: Supply a ViewHolder on clicks (allows editable lists to be standardized into a listener.)
* A basic listener for list interactions. TODO: Supply a ViewHolder on clicks (allows editable
* lists to be standardized into a listener.)
* @author Alexander Capehart (OxygenCobalt)
*/
interface ClickableListListener {

View file

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

View file

@ -100,17 +100,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
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) {
init {
// ViewHolders are not automatically full-width in dialogs, manually resize
// them to be as such.
root.layoutParams =
LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
root.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
}
}
}

View file

@ -21,7 +21,6 @@ import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.list.Item
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.
@ -36,8 +35,7 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
private var isPlaying = false
/**
* The current list of the adapter. This is used to update items if the indicator
* state changes.
* The current list of the adapter. This is used to update items if the indicator state changes.
*/
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) {
/**
* Update the playing indicator within this [RecyclerView.ViewHolder].
* @param isActive True if this item is playing, false otherwise.
* @param isPlaying True if playback is ongoing, false if paused. If this
* is true, [isActive] will also be true.
* @param isPlaying True if playback is ongoing, false if paused. If this is true,
* [isActive] will also be true.
*/
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) {
/**
* 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
/**
* A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method.
* Use this whenever creating [DiffUtil.ItemCallback] implementations with an [Item]
* subclass.
* A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method. Use this
* whenever creating [DiffUtil.ItemCallback] implementations with an [Item] subclass.
* @author Alexander Capehart (OxygenCobalt)
*/
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
* AsyncListDiffer, at the cost of performance.
* Derived from Material Files: https://github.com/zhanghai/MaterialFiles
* AsyncListDiffer, at the cost of performance. Derived from Material Files:
* https://github.com/zhanghai/MaterialFiles
* @author Hai Zhang, Alexander Capehart (OxygenCobalt)
*/
class SyncListDiffer<T>(
@ -111,8 +111,8 @@ class SyncListDiffer<T>(
}
/**
* Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only
* use it if the changes are trivial.
* Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only use it
* if the changes are trivial.
* @param newList The list to update to.
*/
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
* update synchronously, but too chaotic to update asynchronously.
* Replace this list with a new list. This is good for large diffs that are too slow to update
* synchronously, but too chaotic to update asynchronously.
* @param newList The list to update to.
*/
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.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre

View file

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

View file

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

View file

@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.util.logD
/**
* A [ViewModel] that manages the current selection.
@ -31,10 +30,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback {
private val musicStore = MusicStore.getInstance()
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>>
get() = _selected
@ -49,14 +45,15 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback {
// Sanitize the selection to remove items that no longer exist and thus
// won't appear in any list.
_selected.value = _selected.value.mapNotNull {
when (it) {
is Song -> library.sanitize(it)
is Album -> library.sanitize(it)
is Artist -> library.sanitize(it)
is Genre -> library.sanitize(it)
_selected.value =
_selected.value.mapNotNull {
when (it) {
is Song -> library.sanitize(it)
is Album -> library.sanitize(it)
is Artist -> library.sanitize(it)
is Genre -> library.sanitize(it)
}
}
}
}
override fun onCleared() {
@ -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
* removed. Otherwise, it will be added.
* Select a new [Music] item. If this item is already within the selected items, the item will
* be removed. Otherwise, it will be added.
* @param music The [Music] item to select.
*/
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.
* @return The list of selected items before it was cleared.
*/
fun consume() =
_selected.value.also { _selected.value = listOf() }
fun consume() = _selected.value.also { _selected.value = listOf() }
}

View file

@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* 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")
package org.oxycblt.auxio.music
@ -25,6 +24,8 @@ import android.os.Parcelable
import java.security.MessageDigest
import java.text.CollationKey
import java.text.Collator
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.UUID
import kotlin.math.max
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.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull
import java.text.ParseException
import java.text.SimpleDateFormat
// --- MUSIC MODELS ---
/**
* Abstract music data. This contains universal information about all concrete music implementations,
* such as identification information and names.
* Abstract music data. This contains universal information about all concrete music
* implementations, such as identification information and names.
* @author Alexander Capehart (OxygenCobalt)
*/
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
* nearly all cases.
* @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
* the item does not have a name, an analogous "Unknown X" name is returned.
* @return A human-readable string representing the name of this music. In the case that the
* item does not have a name, an analogous "Unknown X" name is returned.
*/
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
* not only when sorting music, but also trying to locate music based on a fuzzy search by
* the user. Will be null if the item has no known sort name.
* The raw sort name of this item as it was extracted from the file-system. This can be used not
* only when sorting music, but also trying to locate music based on a fuzzy search by the user.
* Will be null if the item has no known sort name.
*/
abstract val rawSortName: String?
/**
* A [CollationKey] derived from [rawName] and [rawSortName] that can be used to sort items
* in a semantically-correct manner. Will be null if the item has no name.
* A [CollationKey] derived from [rawName] and [rawSortName] that can be used to sort items in a
* semantically-correct manner. Will be null if the item has no name.
*
* The key will have the following attributes:
* - If [rawSortName] is present, this key will be derived from it. Otherwise [rawName]
* is used.
* - If [rawSortName] is present, this key will be derived from it. Otherwise [rawName] is used.
* - 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.
*/
abstract val collationKey: CollationKey?
/**
* Finalize this item once the music library has been fully constructed. This is where
* any final ordering or sanity checking should occur.
* **This function is internal to the music package. Do not use it elsewhere.**
* Finalize this item once the music library has been fully constructed. This is where any final
* ordering or sanity checking should occur. **This function is internal to the music package.
* Do not use it elsewhere.**
*/
abstract fun _finalize()
@ -128,20 +126,20 @@ sealed class Music : Item {
* A unique identifier for a piece of music.
*
* [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
* several improvements to music management in this app, including:
* either a hash of meaningful metadata or the MusicBrainz ID spec. Using this enables several
* improvements to music management in this app, including:
*
* - Proper differentiation of identical music. It's common for large, well-tagged libraries
* to have functionally duplicate items that are differentiated with MusicBrainz IDs, and so
* [UID] allows us to properly differentiate between these in the app.
* - Proper differentiation of identical music. It's common for large, well-tagged libraries to
* have functionally duplicate items that are differentiated with MusicBrainz IDs, and so [UID]
* allows us to properly differentiate between these in the app.
* - 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
* 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
* or drift as the music library changes.
* specific files metadata configuration, which is unlikely to collide with another item or
* drift as the music library changes.
*
* Note: Generally try to use [UID] as a black box that can only be read, written, and
* compared. It will not be fun if you try to manipulate it in any other manner.
* Note: Generally try to use [UID] as a black box that can only be read, written, and compared.
* It will not be fun if you try to manipulate it in any other manner.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@ -180,11 +178,11 @@ sealed class Music : Item {
companion object {
/**
* Creates an auxio-style [UID] with a [UUID] composed of a hash of the
* non-subjective, unlikely-to-change metadata of the music.
* Creates an auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
* unlikely-to-change metadata of the music.
* @param mode The analogous [MusicMode] of the item that created this [UID].
* @param updates Block to update the [MessageDigest] hash with the metadata of
* the item. Make sure the metadata hashed semantically aligns with the format
* @param updates Block to update the [MessageDigest] hash with the metadata of the
* item. Make sure the metadata hashed semantically aligns with the format
* specification.
* @return A new auxio-style [UID].
*/
@ -192,10 +190,11 @@ sealed class Music : Item {
// 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
// this into a UUID too.
val uuid = MessageDigest.getInstance("MD5").run {
updates()
digest().toUuid()
}
val uuid =
MessageDigest.getInstance("MD5").run {
updates()
digest().toUuid()
}
return UID(Format.AUXIO, mode, uuid)
}
@ -235,7 +234,8 @@ sealed class Music : Item {
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
return UID(format, mode, uuid)
@ -254,9 +254,7 @@ sealed class Music : Item {
* @author Alexander Capehart (OxygenCobalt)
*/
sealed class MusicParent : Music() {
/**
* The [Song]s in this this group.
*/
/** The [Song]s in this this group. */
abstract val songs: List<Song>
// Note: Append song contents to MusicParent equality so that Groups with
@ -279,8 +277,8 @@ sealed class MusicParent : Music() {
*/
class Song constructor(raw: Raw, settings: Settings) : Music() {
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
?: UID.auxio(MusicMode.SONGS) {
// Song UIDs are based on the raw data without parsing so that they remain
// consistent across music setting changes. Parents are not held up to the
@ -310,14 +308,14 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
val date = raw.date
/**
* The URI to the audio file that this instance was created from. This can be used to
* access the audio file in a way that is scoped-storage-safe.
* The URI to the audio file that this instance was created from. This can be used to access the
* audio file in a way that is scoped-storage-safe.
*/
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
* favored instead for accessing the audio file.
* The [Path] to this audio file. This is only intended for display, [uri] should be favored
* instead for accessing the audio file.
*/
val path =
Path(
@ -341,8 +339,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
private var _album: Album? = null
/**
* The parent [Album]. If the metadata did not specify an album, it's parent directory is
* used instead.
* The parent [Album]. If the metadata did not specify an album, it's parent directory is used
* instead.
*/
val album: Album
get() = unlikelyToBeNull(_album)
@ -371,23 +369,23 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
private val _artists = mutableListOf<Artist>()
/**
* The parent [Artist]s of this [Song]. Is often one, but there can be multiple if more
* than one [Artist] name was specified in the metadata. Unliked [Album], artists are
* prioritized for this field.
* The parent [Artist]s of this [Song]. Is often one, but there can be multiple if more than one
* [Artist] name was specified in the metadata. Unliked [Album], artists are prioritized for
* this field.
*/
val artists: List<Artist>
get() = _artists
/**
* Resolves one or more [Artist]s into a single piece of human-readable names.
* @param context [Context] required for [resolveName].
* TODO Internationalize the list formatter.
* @param context [Context] required for [resolveName]. TODO Internationalize the list
* formatter.
*/
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
/**
* Checks if the [Artist] *display* of this [Song] and another [Song] are equal. This
* will only compare surface-level names, and not [Music.UID]s.
* Checks if the [Artist] *display* of this [Song] and another [Song] are equal. This will only
* compare surface-level names, and not [Music.UID]s.
* @param other The [Song] to compare to.
* @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>()
/**
* The parent [Genre]s of this [Song]. Is often one, but there can be multiple if more
* than one [Genre] name was specified in the metadata.
* The parent [Genre]s of this [Song]. Is often one, but there can be multiple if more than one
* [Genre] name was specified in the metadata.
*/
val genres: List<Genre>
get() = _genres
@ -420,9 +418,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
// --- INTERNAL FIELDS ---
/**
* The [Album.Raw] instances collated by the [Song]. This can be used to group [Song]s into
* an [Album].
* **This is only meant for use within the music package.**
* The [Album.Raw] instances collated by the [Song]. This can be used to group [Song]s into an
* [Album]. **This is only meant for use within the music package.**
*/
val _rawAlbum =
Album.Raw(
@ -435,19 +432,17 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
/**
* The [Artist.Raw] instances collated by the [Song]. The artists of the song take
* priority, followed by the album artists. If there are no artists, this field will
* be a single "unknown" [Artist.Raw]. This can be used to group up [Song]s into
* an [Artist].
* **This is only meant for use within the music package.**
* The [Artist.Raw] instances collated by the [Song]. The artists of the song take priority,
* followed by the album artists. If there are no artists, this field will be a single "unknown"
* [Artist.Raw]. This can be used to group up [Song]s into an [Artist]. **This is only meant for
* use within the music package.**
*/
val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(Artist.Raw()) }
/**
* The [Genre.Raw] instances collated by the [Song]. This can be used to group up
* [Song]s into a [Genre]. ID3v2 Genre names are automatically converted to their
* resolved names.
* **This is only meant for use within the music package.**
* The [Genre.Raw] instances collated by the [Song]. This can be used to group up [Song]s into a
* [Genre]. ID3v2 Genre names are automatically converted to their resolved names. **This is
* only meant for use within the music package.**
*/
val _rawGenres =
raw.genreNames
@ -457,8 +452,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
/**
* Links this [Song] with a parent [Album].
* @param album The parent [Album] to link to.
* **This is only meant for use within the music package.**
* @param album The parent [Album] to link to. **This is only meant for use within the music
* package.**
*/
fun _link(album: Album) {
_album = album
@ -466,8 +461,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
/**
* Links this [Song] with a parent [Artist].
* @param artist The parent [Artist] to link to.
* **This is only meant for use within the music package.**
* @param artist The parent [Artist] to link to. **This is only meant for use within the music
* package.**
*/
fun _link(artist: Artist) {
_artists.add(artist)
@ -475,8 +470,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
/**
* Links this [Song] with a parent [Genre].
* @param genre The parent [Genre] to link to.
* **This is only meant for use within the music package.**
* @param genre The parent [Genre] to link to. **This is only meant for use within the music
* package.**
*/
fun _link(genre: 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.
* **This is only meant for use within the music package.**
* Raw information about a [Song] obtained from the filesystem/Extractor instances. **This is
* only meant for use within the music package.**
*/
class Raw
constructor(
/**
* The ID of the [Song]'s audio file, obtained from MediaStore. Note that this
* ID is highly unstable and should only be used for accessing the audio file.
* The ID of the [Song]'s audio file, obtained from MediaStore. Note that this ID is highly
* unstable and should only be used for accessing the audio file.
*/
var mediaStoreId: Long? = null,
/** @see Song.dateAdded */
@ -583,8 +578,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
*/
class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent() {
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ALBUMS, it) }
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ALBUMS, it) }
?: UID.auxio(MusicMode.ALBUMS) {
// Hash based on only names despite the presence of a date to increase stability.
// I don't know if there is any situation where an artist will have two albums with
@ -598,23 +593,20 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
override fun resolveName(context: Context) = rawName
/**
* The earliest [Date] this album was released.
* Will be null if no valid date was present in the metadata of any [Song].
* TODO: Date ranges?
* The earliest [Date] this album was released. Will be null if no valid date was present in the
* metadata of any [Song]. TODO: Date ranges?
*/
val date: Date?
/**
* The [Type] of this album, signifying the type of release it actually is.
* Defaults to [Type.Album].
* The [Type] of this album, signifying the type of release it actually is. Defaults to
* [Type.Album].
*/
val type = raw.type ?: Type.Album(null)
/**
* The URI to a MediaStore-provided album cover. These images will be fast to load, but
* at the cost of image quality.
* The URI to a MediaStore-provided album cover. These images will be fast to load, but at the
* cost of image quality.
*/
val coverUri = raw.mediaStoreId.toCoverUri()
/** 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>()
/**
* The parent [Artist]s of this [Album]. Is often one, but there can be multiple if more
* than one [Artist] name was specified in the metadata of the [Song]'s. Unlike [Song],
* album artists are prioritized for this field.
* The parent [Artist]s of this [Album]. Is often one, but there can be multiple if more than
* one [Artist] name was specified in the metadata of the [Song]'s. Unlike [Song], album artists
* are prioritized for this field.
*/
val artists: List<Artist>
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) }
/**
* Checks if the [Artist] *display* of this [Album] and another [Album] are equal. This
* will only compare surface-level names, and not [Music.UID]s.
* Checks if the [Artist] *display* of this [Album] and another [Album] are equal. This will
* only compare surface-level names, and not [Music.UID]s.
* @param other The [Album] to compare to.
* @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
* priority, followed by the artists. If there are no artists, this field will
* be a single "unknown" [Artist.Raw]. This can be used to group up [Album]s into
* an [Artist].
* **This is only meant for use within the music package.**
* priority, followed by the artists. If there are no artists, this field will be a single
* "unknown" [Artist.Raw]. This can be used to group up [Album]s into an [Artist]. **This is
* only meant for use within the music package.**
*/
val _rawArtists = raw.rawArtists
/**
* Links this [Album] with a parent [Artist].
* @param artist The parent [Artist] to link to.
* **This is only meant for use within the music package.**
* @param artist The parent [Artist] to link to. **This is only meant for use within the music
* package.**
*/
fun _link(artist: 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.
*
* This class is derived from the MusicBrainz Release Group Type specification. It can
* be found at: https://musicbrainz.org/doc/Release_Group/Type
* This class is derived from the MusicBrainz Release Group Type specification. It can be found
* at: https://musicbrainz.org/doc/Release_Group/Type
* @author Alexander Capehart (OxygenCobalt)
*/
sealed class Type {
@ -732,10 +723,10 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
* considered "Plain".
*/
abstract val refinement: Refinement?
/** The string resource corresponding to the name of this release type to show in the UI. */
abstract val stringRes: Int
/**
* A plain album.
* @param refinement A specification of what kind of performance this release is. If null,
@ -751,7 +742,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
Refinement.REMIX -> R.string.lbl_album_remix
}
}
/**
* A "Extended Play", or EP. Usually a smaller release consisting of 4-5 songs.
* @param refinement A specification of what kind of performance this release is. If null,
@ -767,7 +758,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
Refinement.REMIX -> R.string.lbl_ep_remix
}
}
/**
* A single. Usually a release consisting of 1-2 songs.
* @param refinement A specification of what kind of performance this release is. If null,
@ -783,7 +774,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
Refinement.REMIX -> R.string.lbl_single_remix
}
}
/**
* A compilation. Usually consists of many songs from a variety of artists.
* @param refinement A specification of what kind of performance this release is. If null,
@ -799,7 +790,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
Refinement.REMIX -> R.string.lbl_compilation_remix
}
}
/**
* A soundtrack. Similar to a [Compilation], but created for a specific piece of (usually
* visual) media.
@ -807,11 +798,11 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
object Soundtrack : Type() {
override val refinement: Refinement?
get() = null
override val stringRes: Int
get() = R.string.lbl_soundtrack
}
/**
* A (DJ) Mix. These are usually one large track consisting of the artist playing several
* sub-tracks with smooth transitions between them.
@ -819,45 +810,38 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
object Mix : Type() {
override val refinement: Refinement?
get() = null
override val stringRes: Int
get() = R.string.lbl_mix
}
/**
* A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist]
* or a future release.
* A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist] or
* a future release.
*/
object Mixtape : Type() {
override val refinement: Refinement?
get() = null
override val stringRes: Int
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 {
/**
* A release consisting of a live performance
*/
/** A release consisting of a live performance */
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
}
companion object {
/**
* Parse a [Type] from a string formatted with the MusicBrainz Release Group Type
* specification.
* @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
* were not valid.
* @return A [Type] consisting of the given types, or null if the types were not valid.
*/
fun parse(types: List<String>): Type? {
val primary = types.getOrNull(0) ?: return null
@ -872,14 +856,14 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
else -> types.parseSecondaryTypes(0) { Album(it) }
}
}
/**
* Parse "secondary" types (i.e not [Album], [EP], or [Single]) from a string formatted
* with the MusicBrainz Release Group Type specification.
* @param index The index of the release type to parse.
* @param convertRefinement Code to convert a [Refinement] into a [Type]
* corresponding to the callee's context. This is used in order to handle secondary
* times that are actually [Refinement]s.
* @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding
* to the callee's context. This is used in order to handle secondary times that are
* actually [Refinement]s.
* @return A [Type] corresponding to the secondary type found at that index.
*/
private inline fun List<String>.parseSecondaryTypes(
@ -896,14 +880,14 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
parseSecondaryTypeImpl(secondary, convertRefinement)
}
}
/**
* Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond
* to any child values.
* Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond to
* any child values.
* @param type The release type value to parse.
* @param convertRefinement Code to convert a [Refinement] into a [Type]
* corresponding to the callee's context. This is used in order to handle secondary
* times that are actually [Refinement]s.
* @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding
* to the callee's context. This is used in order to handle secondary times that are
* actually [Refinement]s.
*/
private inline fun parseSecondaryTypeImpl(
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.
* **This is only meant for use within the music package.**
* Raw information about an [Album] obtained from the component [Song] instances. **This is only
* meant for use within the music package.**
*/
class Raw(
/**
* The ID of the [Album]'s grouping, obtained from MediaStore. Note that this
* ID is highly unstable and should only be used for accessing the system-provided
* cover art.
* The ID of the [Album]'s grouping, obtained from MediaStore. Note that this ID is highly
* unstable and should only be used for accessing the system-provided cover art.
*/
val mediaStoreId: Long,
/** @see Music.uid */
@ -970,18 +953,18 @@ 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
* from within the library, derived from [Song]s and [Album]s respectively.
* An abstract artist. These are actually a combination of the artist and album artist tags from
* within the library, derived from [Song]s and [Album]s respectively.
* @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],
* either through artist or album artist tags. Providing [Song]s to the artist is optional.
* These instances will be linked to this [Artist].
* @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist], either
* through artist or album artist tags. Providing [Song]s to the artist is optional. These instances
* will be linked to this [Artist].
* @author Alexander Capehart (OxygenCobalt)
*/
class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ARTISTS, it) }
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ARTISTS, it) }
?: UID.auxio(MusicMode.ARTISTS) { update(raw.name) }
override val rawName = raw.name
override val rawSortName = raw.sortName
@ -990,21 +973,21 @@ class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicP
override val songs: List<Song>
/**
* All of the [Album]s this artist is credited to. Note that any [Song] credited to this
* artist will have it's [Album] considered to be "indirectly" linked to this [Artist], and
* thus included in this list.
* All of the [Album]s this artist is credited to. Note that any [Song] credited to this artist
* will have it's [Album] considered to be "indirectly" linked to this [Artist], and thus
* included in this list.
*/
val albums: List<Album>
/**
* The duration of all [Song]s in the artist, in milliseconds.
* Will be null if there are no songs.
* The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no
* songs.
*/
val durationMs: Long?
/**
* Whether this artist is considered a "collaborator", i.e it is not directly credited on
* any [Album].
* Whether this artist is considered a "collaborator", i.e it is not directly credited on any
* [Album].
*/
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) }
/**
* Checks if the [Genre] *display* of this [Artist] and another [Artist] are equal. This
* will only compare surface-level names, and not [Music.UID]s.
* Checks if the [Genre] *display* of this [Artist] and another [Artist] are equal. This will
* only compare surface-level names, and not [Music.UID]s.
* @param other The [Artist] to compare to.
* @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]
* list. This can be used to create a consistent ordering within child [Artist] lists
* based on the original tag order.
* list. This can be used to create a consistent ordering within child [Artist] lists based on
* the original tag order.
* @param rawArtists The [Artist.Raw] instances to check. It is assumed that this [Artist]'s
* [Artist.Raw] will be within the list.
* @return The index of the [Artist]'s [Artist.Raw] within the list.
* **This is only meant for use within the music package.**
* @return The index of the [Artist]'s [Artist.Raw] within the list. **This is only meant for
* use within the music package.**
*/
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 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>
/**
* 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>
/**
* 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
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]
* list. This can be used to create a consistent ordering within child [Genre] lists
* based on the original tag order.
* list. This can be used to create a consistent ordering within child [Genre] lists based on
* the original tag order.
* @param rawGenres The [Genre.Raw] instances to check. It is assumed that this [Genre]'s
* [Genre.Raw] will be within the list.
* @return The index of the [Genre]'s [Genre.Raw] within the list.
* **This is only meant for use within the music package.**
* @return The index of the [Genre]'s [Genre.Raw] within the list. **This is only meant for use
* within the music package.**
*/
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.
* **This is only meant for use within the music package.**
* Raw information about a [Genre] obtained from the component [Song] instances. **This is only
* meant for use within the music package.**
*/
class Raw(
/**
* @see Music.rawName
*/
/** @see Music.rawName */
val name: String? = null
) {
// 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.
*
* This class only encodes the timestamp spec and it's conversion to a human-readable date,
* without any other time management or validation. In general, this should only be used for
* display.
* This class only encodes the timestamp spec and it's conversion to a human-readable date, without
* any other time management or validation. In general, this should only be used for display.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@ -1240,20 +1213,21 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/**
* Resolve this instance into a human-readable date.
* @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
* (ex. "Jan 2020") will be returned. Otherwise, a plain year value (ex. "2020") is
* returned. Dates will be properly localized.
* @return If the [Date] has a valid month and year value, a more fine-grained date (ex. "Jan
* 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will
* be properly localized.
*/
fun resolveDate(context: Context): String {
if (month != null) {
// Parse a date format from an ISO-ish format
val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
format.applyPattern("yyyy-MM")
val date = try {
format.parse("$year-$month")
} catch (e: ParseException) {
null
}
val date =
try {
format.parse("$year-$month")
} catch (e: ParseException) {
null
}
if (date != null) {
// Reformat as a readable month and year
@ -1307,8 +1281,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
companion object {
/**
* A [Regex] that can parse a variable-precision ISO-8601 timestamp.
* Derived from https://github.com/quodlibet/mutagen
* A [Regex] that can parse a variable-precision ISO-8601 timestamp. Derived from
* https://github.com/quodlibet/mutagen
*/
private val ISO8601_REGEX =
Regex(
@ -1326,9 +1300,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
* @param year The year component.
* @param month The month component.
* @param day The day component.
* @return A new [Date] consisting of the given components. May have reduced precision
* if the components were partially invalid, and will be null if all components are
* invalid.
* @return A new [Date] consisting of the given components. May have reduced precision if
* the components were partially invalid, and will be null if all components are invalid.
*/
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 day The day component.
* @param hour The hour component
* @return A new [Date] consisting of the given components. May have reduced precision
* if the components were partially invalid, and will be null if all components are
* invalid.
* @return A new [Date] consisting of the given components. May have reduced precision if
* the components were partially invalid, and will be null if all components are invalid.
*/
fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) =
fromTokens(listOf(year, month, day, hour, minute))
@ -1348,14 +1320,14 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/**
* Create a [Date] from a [String] timestamp.
* @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
* if the components were partially invalid, and will be null if all components are
* invalid or if the timestamp is invalid.
* @return A new [Date] consisting of the given components. May have reduced precision if
* the components were partially invalid, and will be null if all components are invalid or
* if the timestamp is invalid.
*/
fun from(timestamp: String): Date? {
val tokens =
// Match the input with the timestamp regex
(ISO8601_REGEX.matchEntire(timestamp) ?: return null)
// Match the input with the timestamp regex
(ISO8601_REGEX.matchEntire(timestamp) ?: return null)
.groupValues
// Filter to the specific tokens we want and convert them to integer tokens.
.mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null }
@ -1365,9 +1337,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/**
* Create a [Date] from the given non-validated tokens.
* @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
* if the components were partially invalid, and will be null if all components are
* invalid.
* @return A new [Date] consisting of the given components. May have reduced precision if
* the components were partially invalid, and will be null if all components are invalid.
*/
private fun fromTokens(tokens: List<Int>): Date? {
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].
* Will stop as soon as an invalid token is found.
* Validate a list of tokens provided by [src], and add the valid ones to [dst]. Will stop
* as soon as an invalid token is found.
* @param src The input tokens to validate.
* @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.
* @return A [UUID] derived from the [ByteArray]'s contents. Internally, the two [Long]s
* in the [UUID] will be little-endian.
* @return A [UUID] derived from the [ByteArray]'s contents. Internally, the two [Long]s in the
* [UUID] will be little-endian.
*/
fun ByteArray.toUuid(): UUID {
check(size == 16)

View file

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

View file

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

View file

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

View file

@ -29,21 +29,21 @@ import androidx.core.database.getStringOrNull
import java.io.File
import org.oxycblt.auxio.music.Song
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.mediaStoreVolumeNameCompat
import org.oxycblt.auxio.music.storage.safeQuery
import org.oxycblt.auxio.music.storage.storageVolumesCompat
import org.oxycblt.auxio.music.storage.useQuery
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD
/**
* 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
* supported by [MetadataExtractor]. Solely relying on this is not recommended, as it often
* produces bad metadata.
* music extraction process and primarily intended for redundancy for files not natively supported
* by [MetadataExtractor]. Solely relying on this is not recommended, as it often produces bad
* metadata.
* @param context [Context] required to query the media database.
* @param cacheExtractor [CacheExtractor] implementation for cache optimizations.
* @author Alexander Capehart (OxygenCobalt)
@ -69,15 +69,15 @@ abstract class MediaStoreExtractor(
private val genreNamesMap = mutableMapOf<Long, String>()
/**
* The [StorageVolume]s currently scanned by [MediaStore]. This should be used to transform
* path information from the database into volume-aware paths.
* The [StorageVolume]s currently scanned by [MediaStore]. This should be used to transform path
* information from the database into volume-aware paths.
*/
protected var volumes = listOf<StorageVolume>()
private set
/**
* Initialize this instance. This involves setting up the required sub-extractors and
* querying the media database for music files.
* Initialize this instance. This involves setting up the required sub-extractors and querying
* the media database for music files.
* @return A [Cursor] of the music data returned from the database.
*/
open fun init(): Cursor {
@ -124,11 +124,14 @@ abstract class MediaStoreExtractor(
// Now we can actually query MediaStore.
logD("Starting song query [proj: ${projection.toList()}, selector: $selector, args: $args]")
val cursor = context.contentResolverSafe.safeQuery(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,
selector,
args.toTypedArray()).also { cursor = it }
val cursor =
context.contentResolverSafe
.safeQuery(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,
selector,
args.toTypedArray())
.also { cursor = it }
logD("Song query succeeded [Projected total: ${cursor.count}]")
// 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,
* alongside freeing up memory.
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside
* freeing up memory.
* @param rawSongs The songs to write into the cache.
*/
fun finalize(rawSongs: List<Song.Raw>) {
@ -222,8 +225,8 @@ abstract class MediaStoreExtractor(
}
/**
* The database columns available to all android versions supported by Auxio.
* Concrete implementations can extend this projection to add version-specific columns.
* The database columns available to all android versions supported by Auxio. Concrete
* implementations can extend this projection to add version-specific columns.
*/
protected open val projection: Array<String>
get() =
@ -244,8 +247,8 @@ abstract class MediaStoreExtractor(
AUDIO_COLUMN_ALBUM_ARTIST)
/**
* The companion template to add to the projection's selector whenever arguments are added
* by [addDirToSelector].
* The companion template to add to the projection's selector whenever arguments are added by
* [addDirToSelector].
* @see addDirToSelector
*/
protected abstract val dirSelectorTemplate: String
@ -253,8 +256,8 @@ abstract class MediaStoreExtractor(
/**
* Add a [Directory] to the given list of projection selector arguments.
* @param dir The [Directory] to add.
* @param args The destination list to append selector arguments to that are analogous
* to the given [Directory].
* @param args The destination list to append selector arguments to that are analogous to the
* given [Directory].
* @return true if the [Directory] was added, false otherwise.
* @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
* 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
* due to volume additions or removals.
* instead dependent on the file-system, which could change without invalidating the cache due
* to volume additions or removals.
* @param cursor The [Cursor] to read from.
* @param raw The [Song.Raw] to populate.
* @see populateMetadata
@ -281,9 +284,9 @@ abstract class MediaStoreExtractor(
}
/**
* Populate a [Song.Raw] with the Metadata of the given [MediaStore] [Cursor], which is the
* data about a [Song.Raw] that can be cached. This includes any information intrinsic to
* the file or it's file format, such as music tags.
* Populate a [Song.Raw] with the Metadata of the given [MediaStore] [Cursor], which is the data
* about a [Song.Raw] that can be cached. This includes any information intrinsic to the file or
* it's file format, such as music tags.
* @param cursor The [Cursor] to read from.
* @param raw The [Song.Raw] to populate.
* @see populateFileData
@ -334,8 +337,8 @@ abstract class MediaStoreExtractor(
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
* for it until API 29. This will work on all versions that Auxio supports.
* The external volume. This naming has existed since API 21, but no constant existed for it
* until API 29. This will work on all versions that Auxio supports.
*/
@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>
get() =
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
// MedaStore) when working with audio files.
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
* API 29.
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at API
* 29.
* @param context [Context] required to query the media database.
* @param cacheExtractor [CacheExtractor] implementation for cache functionality.
* @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
* API 30 onwards.
* A [MediaStoreExtractor] that completes the music loading process in a way compatible from API 30
* onwards.
* @param context [Context] required to query the media database.
* @param cacheExtractor [CacheExtractor] implementation for cache optimizations.
* @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
* last step in the music extraction process and is mostly responsible for papering over the
* bad metadata that [MediaStoreExtractor] produces.
* last step in the music extraction process and is mostly responsible for papering over the bad
* metadata that [MediaStoreExtractor] produces.
*
* @param context [Context] required for reading audio files.
* @param mediaStoreExtractor [MediaStoreExtractor] implementation for cache optimizations and
@ -56,17 +56,17 @@ class MetadataExtractor(
fun init() = mediaStoreExtractor.init().count
/**
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache,
* alongside freeing up memory.
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside
* freeing up memory.
* @param rawSongs The songs to write into the cache.
*/
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreExtractor.finalize(rawSongs)
/**
* Parse all [Song.Raw] instances queued by the sub-extractors. This will first delegate
* to the sub-extractors before parsing the metadata itself.
* @param emit A callback that will be invoked with every new [Song.Raw] instance when
* they are successfully loaded.
* Parse all [Song.Raw] instances queued by the sub-extractors. This will first delegate to the
* sub-extractors before parsing the metadata itself.
* @param emit A callback that will be invoked with every new [Song.Raw] instance when they are
* successfully loaded.
*/
suspend fun parse(emit: suspend (Song.Raw) -> Unit) {
while (true) {
@ -122,8 +122,8 @@ class MetadataExtractor(
}
/**
* Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed.
* TODO: Re-unify with MetadataExtractor.
* Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed. TODO:
* Re-unify with MetadataExtractor.
* @param context [Context] required to open the audio file.
* @param raw [Song.Raw] to process.
* @author Alexander Capehart (OxygenCobalt)
@ -135,7 +135,8 @@ class Task(context: Context, private val raw: Song.Raw) {
private val future =
MetadataRetriever.retrieveMetadata(
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.
@ -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
(textFrames["TDOR"]?.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 }
// Album
textFrames["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] }
textFrames["TALB"]?.let { raw.albumName = 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
textFrames["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it }
@ -274,9 +278,9 @@ class Task(context: Context, private val raw: Song.Raw) {
* Frames.
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
* values.
* @retrn A [Date] of a year value from TORY/TYER, a month and day value from TDAT,
* and a hour/minute value from TIME. No second value is included. The latter two fields may
* not be included in they cannot be parsed. Will be null if a year value could not be parsed.
* @retrn A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a
* hour/minute value from TIME. No second value is included. The latter two fields may not be
* 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? {
// 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.
* @param comments A mapping between vorbis comment names and one or more vorbis comment
* values.
* @param comments A mapping between vorbis comment names and one or more vorbis comment values.
*/
private fun populateWithVorbis(comments: Map<String, List<String>>) {
// Song
@ -363,8 +366,8 @@ class Task(context: Context, private val raw: Song.Raw) {
/**
* 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
* replaced with the Unicode replacement byte sequence.
* @return A new string allocated in a memory-safe manner with any UTF-8 errors replaced with
* the Unicode replacement byte sequence.
*/
private fun String.sanitize() = String(encodeToByteArray())
}

View file

@ -24,27 +24,25 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.nonZeroOrNull
/**
* Unpack the track number from a combined track + disc [Int] field.
* These fields appear within MediaStore's TRACK column, and combine the track and disc value
* into a single field where the disc number is the 4th+ digit.
* @return The track number extracted from the combined integer value, or null if the value
* was zero.
* Unpack the track number from a combined track + disc [Int] field. These fields appear within
* MediaStore's TRACK column, and combine the track and disc value into a single field where the
* disc number is the 4th+ digit.
* @return The track number extracted from the combined integer value, or null if the value was
* zero.
*/
fun Int.unpackTrackNo() = mod(1000).nonZeroOrNull()
/**
* Unpack the disc number from a combined track + disc [Int] field.
* These fields appear within MediaStore's TRACK column, and combine the track and disc value
* into a single field where the disc number is the 4th+ digit.
* @return The disc number extracted from the combined integer field, or null if the value
* was zero.
* Unpack the disc number from a combined track + disc [Int] field. These fields appear within
* MediaStore's TRACK column, and combine the track and disc value into a single field where the
* disc number is the 4th+ digit.
* @return The disc number extracted from the combined integer field, or null if the value was zero.
*/
fun Int.unpackDiscNo() = div(1000).nonZeroOrNull()
/**
* Parse the number out of a combined number + total position [String] field.
* These fields often appear in ID3v2 files, and consist of a number and an (optional) total
* value delimited by a /.
* Parse the number out of a combined number + total position [String] field. These fields often
* appear in ID3v2 files, and consist of a number and an (optional) total value delimited by a /.
* @return The number value extracted from the string field, or null if the value could not be
* 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].
* @return A [Date] consisting of the year value, or null if the value could not
* be parsed or if the value was zero.
* @return A [Date] consisting of the year value, or null if the value could not be parsed or if the
* value was zero.
* @see Date.from
*/
fun String.parseYear() = toIntOrNull()?.toDate()
/**
* Parse an ISO-8601 timestamp [String] into a [Date].
* @return A [Date] consisting of the year value plus one or more refinement values
* (ex. month, day), or null if the timestamp was not valid.
* @return A [Date] consisting of the year value plus one or more refinement values (ex. month,
* day), or null if the timestamp was not valid.
*/
fun String.parseTimestamp() = Date.from(this)
/**
* Split a [String] by the given selector, automatically handling escaped characters
* that satisfy the selector.
* @param selector A block that determines if the string should be split at a given
* character.
* Split a [String] by the given selector, automatically handling escaped characters that satisfy
* the selector.
* @param selector A block that determines if the string should be split at a given character.
* @return One or more [String]s split by the selector.
*/
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
* more than one value, nothing is done. Otherwise, this function will attempt to split it based
* on the user's separator preferences.
* Parse a multi-value tag based on the user configuration. If the value is already composed of more
* than one value, nothing is done. Otherwise, this function will attempt to split it based on the
* user's separator preferences.
* @param settings [Settings] required to obtain user separator configuration.
* @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
* representations of genre fields into their named counterparts, and split up singular
* ID3v2-style integer genre fields into one or more genres.
* representations of genre fields into their named counterparts, and split up singular ID3v2-style
* integer genre fields into one or more genres.
* @param settings [Settings] required to obtain user separator configuration.
* @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.
* Derived from mutagen: https://github.com/quodlibet/mutagen
* A [Regex] that implements parsing for ID3v2's genre format. Derived from mutagen:
* https://github.com/quodlibet/mutagen
*/
private val ID3V2_GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?")
/**
* Parse an ID3v2 integer genre field, which has support for multiple genre values and
* combined named/integer genres.
* @return A list of one or more genres, or null if the field is not a valid ID3v2
* integer genre.
* Parse an ID3v2 integer genre field, which has support for multiple genre values and combined
* named/integer genres.
* @return A list of one or more genres, or null if the field is not a valid ID3v2 integer genre.
*/
private fun String.parseId3v2Genre(): List<String>? {
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.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
/**
* A [ViewBindingDialogFragment] that allows the user to configure the separator characters
* used to split tags with multiple values.
* TODO: Add saved state for pending configurations.
* A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to
* split tags with multiple values. TODO: Add saved state for pending configurations.
* @author Alexander Capehart (OxygenCobalt)
*/
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
* [Artist] item, for use with [ArtistChoiceAdapter]. Use [new] to create an instance.
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [Artist] item, for
* use with [ArtistChoiceAdapter]. Use [new] to create an instance.
*/
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
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.list.Item
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.

View file

@ -27,16 +27,17 @@ import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
/**
* The base class for dialogs that implements common behavior across all [Artist] pickers.
* These are shown whenever what to do with an item's [Artist] is ambiguous, as there are
* multiple [Artist]'s to choose from.
* The base class for dialogs that implements common behavior across all [Artist] pickers. These are
* shown whenever what to do with an item's [Artist] is ambiguous, as there are multiple [Artist]'s
* to choose from.
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener {
abstract class ArtistPickerDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener {
protected val pickerModel: PickerViewModel by viewModels()
// Okay to leak this since the Listener will not be called until after initialization.
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
/**
* a [ViewModel] that manages the current music picker state.
* TODO: This really shouldn't exist. Make it so that the dialogs just contain the music
* themselves and then exit if the library changes.
* TODO: While we are at it, let's go and add ClickableSpan too to reduce the extent of
* a [ViewModel] that manages the current music picker state. TODO: This really shouldn't exist.
* Make it so that the dialogs just contain the music themselves and then exit if the library
* changes. TODO: While we are at it, let's go and add ClickableSpan too to reduce the extent of
* this dialog.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -46,7 +45,8 @@ class PickerViewModel : ViewModel(), MusicStore.Callback {
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>?>
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.
_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.
* @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>()
/**
* 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.databinding.DialogMusicDirsBinding
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.getSystemServiceCompat
import org.oxycblt.auxio.util.logD
@ -58,8 +58,7 @@ class MusicDirsDialog :
.setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ ->
val dirs = settings.getMusicDirs(storageManager)
val newDirs =
MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
if (dirs != newDirs) {
logD("Committing changes")
settings.setMusicDirs(newDirs)
@ -69,7 +68,8 @@ class MusicDirsDialog :
override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) {
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
// and override its click listener so that the dialog does not auto-dismiss when we
@ -95,7 +95,9 @@ class MusicDirsDialog :
if (pendingDirs != null) {
dirs =
MusicDirectories(
pendingDirs.mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) },
pendingDirs.mapNotNull {
Directory.fromDocumentTreeUri(storageManager, it)
},
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) =
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
import android.content.Context
@ -5,13 +22,12 @@ import android.os.storage.StorageManager
import android.os.storage.StorageVolume
import android.webkit.MimeTypeMap
import com.google.android.exoplayer2.util.MimeTypes
import org.oxycblt.auxio.R
import java.io.File
import org.oxycblt.auxio.R
/**
* A full absolute path to a file. Only intended for display purposes. For accessing files,
* URIs are preferred in all cases due to scoped storage limitations.
* A full absolute path to a file. Only intended for display purposes. For accessing files, URIs are
* preferred in all cases due to scoped storage limitations.
* @param name The name of the file.
* @param parent The parent [Directory] of the file.
* @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)
/**
* Converts this [Directory] instance into an opaque document tree path.
* This is a huge violation of the document tree URI contract, but it's also the only
* one can sensibly work with these uris in the UI, and it doesn't exactly matter since
* we never write or read directory.
* @return A URI [String] abiding by the document tree specification, or null
* if the [Directory] is not valid.
* Converts this [Directory] instance into an opaque document tree path. This is a huge
* violation of the document tree URI contract, but it's also the only one can sensibly work
* with these uris in the UI, and it doesn't exactly matter since we never write or read
* directory.
* @return A URI [String] abiding by the document tree specification, or null if the [Directory]
* is not valid.
*/
fun toDocumentTreeUri() =
// 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
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"
/**
@ -80,10 +94,9 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator))
/**
* Create a new directory from a document tree URI.
* This is a huge violation of the document tree URI contract, but it's also the only
* one can sensibly work with these uris in the UI, and it doesn't exactly matter since
* we never write or read directory.
* Create a new directory from a document tree URI. This is a huge violation of the document
* tree URI contract, but it's also the only one can sensibly work with these uris in the
* UI, and it doesn't exactly matter since we never write or read directory.
* @param storageManager [StorageManager] in order to obtain the [StorageVolume] specified
* in the given URI.
* @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.
* TODO: Migrate to a combined "Include + Exclude" system that is more sensible.
* @param dirs A list of [Directory] instances. How these are interpreted depends on
* [shouldInclude].
* @param shouldInclude True if the library should only load from the [Directory] instances,
* false if the library should not load from the [Directory] instances.
* Represents the configuration for specific directories to filter to/from when loading music. TODO:
* Migrate to a combined "Include + Exclude" system that is more sensible.
* @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude]
* .
* @param shouldInclude True if the library should only load from the [Directory] instances, false
* if the library should not load from the [Directory] instances.
* @author Alexander Capehart (OxygenCobalt)
*/
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.
* @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
* not be obtained.
* @param fromFormat The mime type obtained by analyzing the file format. Null if could not be
* obtained.
* @author Alexander Capehart (OxygenCobalt)
*/
data class MimeType(val fromExtension: String, val fromFormat: String?) {
/**
* Resolve the mime type into a human-readable format name, such as "Ogg Vorbis".
* @param context [Context] required to obtain human-readable strings.
* @return A human-readable name for this mime type. Will first try [fromFormat],
* then falling back to [fromExtension], then falling back to the extension name,
* and then finally a placeholder "No Format" string.
* @return A human-readable name for this mime type. Will first try [fromFormat], then falling
* back to [fromExtension], then falling back to the extension name, and then finally a
* placeholder "No Format" string.
*/
fun resolveName(context: Context): String {
// We try our best to produce a more readable name for the common audio formats.
@ -193,8 +206,8 @@ data class MimeType(val fromExtension: String, val fromFormat: String?) {
} else {
// Fall back to the extension if we can't find a special name for this format.
MimeTypeMap.getSingleton().getExtensionFromMimeType(fromExtension)?.uppercase()
// Fall back to a placeholder if even that fails.
?: context.getString(R.string.def_codec)
// Fall back to a placeholder if even that fails.
?: context.getString(R.string.def_codec)
}
}
}

View file

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

View file

@ -43,10 +43,10 @@ import org.oxycblt.auxio.util.logW
/**
* Core music loading state class.
*
* This class provides low-level access into the exact state of the music loading process.
* **This class should not be used in most cases.** It is highly volatile and provides far
* more information than is usually needed. Use [MusicStore] instead if you do not need to
* work with the exact music loading state.
* This class provides low-level access into the exact state of the music loading process. **This
* class should not be used in most cases.** It is highly volatile and provides far more information
* than is usually needed. Use [MusicStore] instead if you do not need to work with the exact music
* loading state.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@ -61,9 +61,9 @@ class Indexer private constructor() {
get() = indexingState != null
/**
* Whether this instance has not completed a loading process and is not currently
* loading music. This often occurs early in an app's lifecycle, and consumers should
* try to avoid showing any state when this flag is true.
* Whether this instance has not completed a loading process and is not currently loading music.
* This often occurs early in an app's lifecycle, and consumers should try to avoid showing any
* state when this flag is true.
*/
val isIndeterminate: Boolean
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
* to the current music loading state. There can be only one [Callback] at a time.
* Will invoke all [Callback] methods to initialize the instance with the current state.
* Register the [Callback] for this instance. This can be used to receive rapid-fire updates to
* the current music loading state. There can be only one [Callback] at a time. Will invoke all
* [Callback] methods to initialize the instance with the current state.
* @param callback The [Callback] to add.
*/
@Synchronized
@ -125,10 +125,9 @@ class Indexer private constructor() {
}
/**
* Unregister a [Callback] from this instance, preventing it from recieving any further
* updates.
* @param callback The [Callback] to unregister. Must be the current [Callback]. Does
* nothing if invoked by another [Callback] implementation.
* Unregister a [Callback] from this instance, preventing it from recieving any further updates.
* @param callback The [Callback] to unregister. Must be the current [Callback]. Does nothing if
* invoked by another [Callback] implementation.
* @see Callback
*/
@Synchronized
@ -145,12 +144,12 @@ class Indexer private constructor() {
* 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.
* @param context [Context] required to load music.
* @param withCache Whether to use the cache or not when loading. If false, the cache will
* still be written, but no cache entries will be loaded into the new library.
* @param withCache Whether to use the cache or not when loading. If false, the cache will still
* be written, but no cache entries will be loaded into the new library.
*/
suspend fun index(context: Context, withCache: Boolean) {
if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
PackageManager.PERMISSION_DENIED) {
PackageManager.PERMISSION_DENIED) {
// No permissions, signal that we can't do anything.
emitCompletion(Response.NoPerms)
return
@ -186,9 +185,9 @@ class Indexer private constructor() {
}
/**
* Request that the music library should be reloaded. This should be used by components that
* do not manage the indexing process in order to signal that the [Controller] should call
* [index] eventually.
* Request that the music library should be reloaded. This should be used by components that do
* not manage the indexing process in order to signal that the [Controller] should call [index]
* eventually.
* @param withCache Whether to use the cache when loading music. Does nothing if there is no
* [Controller].
*/
@ -199,8 +198,8 @@ class Indexer private constructor() {
}
/**
* Reset the current loading state to signal that the instance is not loading. This should
* be called by [Controller] after it's indexing co-routine was cancelled.
* Reset the current loading state to signal that the instance is not loading. This should be
* called by [Controller] after it's indexing co-routine was cancelled.
*/
@Synchronized
fun reset() {
@ -211,19 +210,20 @@ class Indexer private constructor() {
/**
* Internal implementation of the music loading process.
* @param context [Context] required to load music.
* @param withCache Whether to use the cache or not when loading. If false, the cache will
* still be written, but no cache entries will be loaded into the new library.
* @param withCache Whether to use the cache or not when loading. If false, the cache will still
* 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.
*/
private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? {
// Create the chain of extractors. Each extractor builds on the previous and
// enables version-specific features in order to create the best possible music
// experience.
val cacheDatabase = if (withCache) {
ReadWriteCacheExtractor(context)
} else {
WriteOnlyCacheExtractor(context)
}
val cacheDatabase =
if (withCache) {
ReadWriteCacheExtractor(context)
} else {
WriteOnlyCacheExtractor(context)
}
val mediaStoreExtractor =
when {
@ -255,11 +255,11 @@ class Indexer private constructor() {
/**
* Load a list of [Song]s from the device.
* @param metadataExtractor The completed [MetadataExtractor] instance to use to load
* [Song.Raw] instances.
* @param metadataExtractor The completed [MetadataExtractor] instance to use to load [Song.Raw]
* instances.
* @param settings [Settings] required to create [Song] instances.
* @return A possibly empty list of [Song]s. These [Song]s will be incomplete and
* must be linked with parent [Album], [Artist], and [Genre] items in order to be usable.
* @return A possibly empty list of [Song]s. These [Song]s will be incomplete and must be linked
* with parent [Album], [Artist], and [Genre] items in order to be usable.
*/
private suspend fun buildSongs(
metadataExtractor: MetadataExtractor,
@ -301,10 +301,10 @@ class Indexer private constructor() {
/**
* 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
* respective [Album]s when created.
* @return A non-empty list of [Album]s. These [Album]s will be incomplete and
* must be linked with parent [Artist] instances in order to be usable.
* @param songs The [Song]s to build [Album]s from. These will be linked with their respective
* [Album]s when created.
* @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked
* with parent [Artist] instances in order to be usable.
*/
private fun buildAlbums(songs: List<Song>): List<Album> {
// 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
* as they group into [Artist] instances much differently, with [Song]s being grouped
* primarily by 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
* the creation of one or more [Artist] instances. These will be linked with their
* respective [Artist]s when created.
* @param albums The [Album]s to build [Artist]s from. One [Album] can result in
* the creation of one or more [Artist] instances. These will be linked with their
* respective [Artist]s when created.
* @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined
* groupings of [Song]s and [Album]s.
* Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required as
* they group into [Artist] instances much differently, with [Song]s being grouped primarily by
* 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 the creation of
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
* created.
* @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
* created.
* @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings
* of [Song]s and [Album]s.
*/
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,
@ -353,9 +353,9 @@ class Indexer private constructor() {
/**
* Group up [Song]s into [Genre] instances.
* @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in
* the creation of one or more [Genre] instances. These will be linked with their
* respective [Genre]s when created.
* @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of
* one or more [Genre] instances. These will be linked with their respective [Genre]s when
* created.
* @return A non-empty list of [Genre]s.
*/
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
* loading process to external code. Assumes that the callee has already checked if they
* have not been canceled and thus have the ability to emit a new state.
* loading process to external code. Assumes that the callee has already checked if they have
* 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.
*/
@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
* loading process to external code. Will check if the callee has not been canceled and thus
* has the ability to emit a new state
* loading process to external code. Will check if the callee has not been canceled and thus has
* the ability to emit a new state
* @param response The new [Response] to emit, representing the outcome of the music loading
* process.
*/
@ -439,8 +439,7 @@ class Indexer private constructor() {
*/
sealed class Indexing {
/**
* Music loading is occurring, but no definite estimate can be put on the current
* progress.
* Music loading is occurring, but no definite estimate can be put on the current progress.
*/
object Indeterminate : Indexing()
@ -477,8 +476,8 @@ class Indexer private constructor() {
* 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.
* Otherwise, [MusicStore.Callback] is highly recommended due to it's updates only
* consisting of the [MusicStore.Library].
* Otherwise, [MusicStore.Callback] is highly recommended due to it's updates only consisting of
* the [MusicStore.Library].
*/
interface Callback {
/**
@ -493,13 +492,13 @@ class Indexer private constructor() {
}
/**
* Context that runs the music loading process. Implementations should be capable of
* running the background for long periods of time without android killing the process.
* Context that runs the music loading process. Implementations should be capable of running the
* background for long periods of time without android killing the process.
*/
interface Controller : Callback {
/**
* Called when a new music loading process was requested. Implementations should
* forward this to [index].
* Called when a new music loading process was requested. Implementations should forward
* this to [index].
* @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.
* @see index
@ -511,9 +510,8 @@ class Indexer private constructor() {
@Volatile private var INSTANCE: Indexer? = null
/**
* A version-compatible identifier for the read external storage permission required
* by the system to load audio.
* TODO: Move elsewhere.
* A version-compatible identifier for the read external storage permission required by the
* system to load audio. TODO: Move elsewhere.
*/
val PERMISSION_READ_AUDIO =
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.IntegerTable
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.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
* the music library for changes.
* A static [ForegroundServiceNotification] that signals to the user that the app is currently
* monitoring the music library for changes.
* @author Alexander Capehart (OxygenCobalt)
*/
class ObservingNotification(context: Context) : ForegroundServiceNotification(context, INDEXER_CHANNEL) {
class ObservingNotification(context: Context) :
ForegroundServiceNotification(context, INDEXER_CHANNEL) {
init {
setSmallIcon(R.drawable.ic_indexer_24)
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.storage.contentResolverSafe
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ForegroundManager
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD
/**
* 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
* it could complete if ran anywhere else. So, this [Service] manages the music loading process
* as an instance of [Indexer.Controller].
* Loading music is a time-consuming process that would likely be killed by the system before it
* could complete if ran anywhere else. So, this [Service] manages the music loading process as an
* instance of [Indexer.Controller].
*
* This [Service] also handles automatic rescanning, as that is a similarly long-running
* background operation that would be unsuitable elsewhere in the app.
* This [Service] also handles automatic rescanning, as that is a similarly long-running background
* operation that would be unsuitable elsewhere in the app.
*
* TODO: Unify with PlaybackService as part of the service independence project
*
@ -121,8 +121,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
indexer.reset()
}
// Start a new music loading job on a co-routine.
currentIndexJob = indexScope.launch {
indexer.index(this@IndexerService, withCache) }
currentIndexJob = indexScope.launch { indexer.index(this@IndexerService, withCache) }
}
override fun onIndexerStateChanged(state: Indexer.State?) {
@ -165,8 +164,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
// --- INTERNAL ---
/**
* Update the current state to "Active", in which the service signals that music
* loading is on-going.
* Update the current state to "Active", in which the service signals that music loading is
* on-going.
* @param state The current music loading state.
*/
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
* that it's currently monitoring the music library for changes.
* Update the current state to "Idle", in which it either does nothing or signals that it's
* currently monitoring the music library for changes.
*/
private fun updateIdleSession() {
if (settings.shouldBeObserving) {
@ -208,9 +207,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
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() {
// Avoid unnecessary acquire calls.
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() {
// Avoid unnecessary release calls.
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
* 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())
init {

View file

@ -25,9 +25,9 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately
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.Song
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately
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.
* @author Alexander Capehart (OxygenCobalt)
*/
class QueueAdapter(private val listener: Listener) :
RecyclerView.Adapter<QueueSongViewHolder>() {
class QueueAdapter(private val listener: Listener) : RecyclerView.Adapter<QueueSongViewHolder>() {
private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK)
// 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
@ -72,8 +71,8 @@ class QueueAdapter(private val listener: Listener) :
}
/**
* Synchronously update the list with new items. This is exceedingly slow for large diffs,
* so only use it for trivial updates.
* Synchronously update the list with new items. This is exceedingly slow for large diffs, so
* only use it for trivial updates.
* @param newList The new [Song]s for the adapter to display.
*/
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,
* so only use it for trivial updates.
* Replace the list with a new list. This is exceedingly slow for large diffs, so only use it
* for trivial updates.
* @param newList The new [Song]s for the adapter to display.
*/
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
* as playing and any previous items as played.
* Set the position of the currently playing item in the queue. This will mark the item as
* playing and any previous items as played.
* @param index The position of the currently playing item in the queue.
* @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 {
/**
* 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) :
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
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
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 =
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
@ -174,9 +165,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
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
get() = binding.songAlbumCover.isEnabled
set(value) {
@ -205,9 +194,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
*/
@SuppressLint("ClickableViewAccessibility")
fun bind(song: Song, listener: QueueAdapter.Listener) {
binding.body.setOnClickListener {
listener.onClick(this)
}
binding.body.setOnClickListener { listener.onClick(this) }
binding.songAlbumCover.bind(song)
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
/**
* A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in the queue
* UI, such as an animation when lifting items.
* A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in the queue UI,
* such as an animation when lifting items.
* @author Alexander Capehart (OxygenCobalt)
*/
class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHelper.Callback() {
@ -73,7 +73,8 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
holder.itemView
.animate()
.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 {
bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt()
}
@ -114,7 +115,8 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
holder.itemView
.animate()
.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 {
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.music.Song
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.collectImmediately
import org.oxycblt.auxio.util.logD

View file

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

View file

@ -25,7 +25,7 @@ import kotlin.math.abs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
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.R
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.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.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.shared.ForegroundManager
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.widgets.WidgetComponent
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
/**
* Called when window insets are being applied to the [View] this [BaseBottomSheetBehavior]
* is linked to.
* Called when window insets are being applied to the [View] this [BaseBottomSheetBehavior] is
* linked to.
* @param child The child view recieving the [WindowInsets].
* @param insets The [WindowInsets] to apply.
* @return The (possibly modified) [WindowInsets].

View file

@ -60,7 +60,7 @@ class SearchAdapter(private val listener: SelectableListListener) :
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = differ.currentList[position]) {
when (val item = differ.currentList[position]) {
is Song -> (holder as SongViewHolder).bind(item, listener)
is Album -> (holder as AlbumViewHolder).bind(item, listener)
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
@ -72,8 +72,8 @@ class SearchAdapter(private val listener: SelectableListListener) :
override fun isItemFullWidth(position: Int) = differ.currentList[position] is Header
/**
* Asynchronously update the list with new items. Assumes that the list only contains
* supported data..
* Asynchronously update the list with new items. Assumes that the list only contains supported
* data..
* @param newList The new [Item]s for the adapter to display.
* @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() {
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.list.Header
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.MusicMode
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.context
@ -129,10 +125,12 @@ class SearchViewModel(application: Application) :
}
if (filterMode == null || filterMode == MusicMode.SONGS) {
library.songs.searchListImpl(query) { q, song -> song.path.name.contains(q) }?.let {
results.add(Header(R.string.lbl_songs))
results.addAll(sort.songs(it))
}
library.songs
.searchListImpl(query) { q, song -> song.path.name.contains(q) }
?.let {
results.add(Header(R.string.lbl_songs))
results.addAll(sort.songs(it))
}
}
// Handle if we were canceled while searching.
@ -141,41 +139,43 @@ class SearchViewModel(application: Application) :
/**
* Search a given [Music] list.
* @param query The query to search for. The routine will compare this query to the names
* of each object in the list and
* @param query The query to search for. The routine will compare this query to the names of
* each object in the list and
* @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
* result quality.
* initially. This can be used to compare against additional attributes to improve search result
* quality.
*/
private inline fun <T : Music> List<T>.searchListImpl(
query: String,
fallback: (String, T) -> Boolean = { _, _ -> false }
) = filter {
// See if the plain resolved name matches the query. This works for most situations.
val name = it.resolveName(context)
if (name.contains(query, ignoreCase = true)) {
return@filter true
}
) =
filter {
// See if the plain resolved name matches the query. This works for most situations.
val name = it.resolveName(context)
if (name.contains(query, ignoreCase = true)) {
return@filter true
}
// See if the sort name matches. This can sometimes be helpful as certain libraries
// will tag sort names to have a alphabetized version of the title.
val sortName = it.rawSortName
if (sortName != null && sortName.contains(query, ignoreCase = true)) {
return@filter true
}
// See if the sort name matches. This can sometimes be helpful as certain libraries
// will tag sort names to have a alphabetized version of the title.
val sortName = it.rawSortName
if (sortName != null && sortName.contains(query, ignoreCase = true)) {
return@filter true
}
// As a last-ditch effort, see if the normalized name matches. This will replace
// any non-alphabetical characters with their alphabetical representations, which
// could make it match the query.
val normalizedName = NORMALIZATION_SANITIZE_REGEX.replace(
Normalizer.normalize(name, Normalizer.Form.NFKD), "")
if (normalizedName.contains(query, ignoreCase = true)) {
return@filter true
}
// As a last-ditch effort, see if the normalized name matches. This will replace
// any non-alphabetical characters with their alphabetical representations, which
// could make it match the query.
val normalizedName =
NORMALIZATION_SANITIZE_REGEX.replace(
Normalizer.normalize(name, Normalizer.Form.NFKD), "")
if (normalizedName.contains(query, ignoreCase = true)) {
return@filter true
}
fallback(query, it)
}
.ifEmpty { null }
fallback(query, it)
}
.ifEmpty { null }
/**
* Returns the ID of the filter option to currently highlight.
@ -212,7 +212,6 @@ class SearchViewModel(application: Application) :
search(lastQuery)
}
companion object {
/**
* 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/>.
*/
package org.oxycblt.auxio.shared
package org.oxycblt.auxio.service
import android.app.Service
import androidx.core.app.ServiceCompat
@ -31,17 +31,15 @@ import org.oxycblt.auxio.util.logD
class ForegroundManager(private val service: Service) {
private var isForeground = false
/**
* Release this instance.
*/
/** Release this instance. */
fun release() {
tryStopForeground()
}
/**
* Try to enter a foreground state.
* @param notification The [ForegroundServiceNotification] to show in order to signal the foreground
* state.
* @param notification The [ForegroundServiceNotification] to show in order to signal the
* foreground state.
* @return true if the state was changed, false otherwise
* @see Service.startForeground
*/

View file

@ -15,7 +15,7 @@
* 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 androidx.annotation.StringRes
@ -51,14 +51,11 @@ abstract class ForegroundServiceNotification(context: Context, info: ChannelInfo
*/
abstract val code: Int
/**
* Post this notification using [NotificationManagerCompat].
*/
/** Post this notification using [NotificationManagerCompat]. */
fun post() {
// This is safe to call without the POST_NOTIFICATIONS permission, as it's a foreground
// notification.
@Suppress("MissingPermission")
notificationManager.notify(code, build())
@Suppress("MissingPermission") 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.music.MusicViewModel
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.logD
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.replaygain.ReplayGainMode
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.unlikelyToBeNull
/**
* A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings.
* Object mutability
* A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. Object
* mutability
* @author Alexander Capehart (OxygenCobalt)
*/
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
* data loss depending on the feasibility of a migration.
* Migrate any settings from an old version into their modern counterparts. This can cause data
* loss depending on the feasibility of a migration.
*/
fun migrate() {
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]
* was originally attached.
* Release this instance and any callbacks held by it. This is not needed if no [Callback] was
* originally attached.
*/
fun release() {
inner.unregisterOnSharedPreferenceChangeListener(this)
@ -164,9 +164,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
unlikelyToBeNull(callback).onSettingChanged(key)
}
/**
* TODO: Remove this
*/
/** TODO: Remove this */
interface Callback {
fun onSettingChanged(key: String)
}
@ -264,8 +262,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
?: MusicMode.SONGS
/**
* What MusicParent item to play from when a Song is played from the detail view.
* Will be null if configured to play from the currently shown item.
* What MusicParent item to play from when a Song is played from the detail view. Will be null
* if configured to play from the currently shown item.
*/
val detailPlaybackMode: MusicMode?
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
* multi-value tags.
* A string of characters representing the desired separator characters to denote multi-value
* tags.
*/
var musicSeparators: String?
// Differ from convention and store a string of separator characters instead of an int
@ -358,7 +356,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
}
}
/** The Song [Sort] mode used in the Home UI. */
/** The Song [Sort] mode used in the Home UI. */
var libSongSort: Sort
get() =
Sort.fromIntCode(
@ -371,7 +369,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
}
}
/** The Album [Sort] mode used in the Home UI. */
/** The Album [Sort] mode used in the Home UI. */
var libAlbumSort: Sort
get() =
Sort.fromIntCode(
@ -384,7 +382,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
}
}
/** The Artist [Sort] mode used in the Home UI. */
/** The Artist [Sort] mode used in the Home UI. */
var libArtistSort: Sort
get() =
Sort.fromIntCode(
@ -397,7 +395,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
}
}
/** The Genre [Sort] mode used in the Home UI. */
/** The Genre [Sort] mode used in the Home UI. */
var libGenreSort: Sort
get() =
Sort.fromIntCode(
@ -410,7 +408,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
}
}
/** The [Sort] mode used in the Album Detail UI. */
/** The [Sort] mode used in the Album Detail UI. */
var detailAlbumSort: Sort
get() {
var sort =
@ -433,7 +431,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
}
}
/** The [Sort] mode used in the Artist Detail UI. */
/** The [Sort] mode used in the Artist Detail UI. */
var detailArtistSort: Sort
get() =
Sort.fromIntCode(
@ -446,7 +444,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
}
}
/** The [Sort] mode used in the Genre Detail UI. */
/** The [Sort] mode used in the Genre Detail UI. */
var detailGenreSort: Sort
get() =
Sort.fromIntCode(

View file

@ -23,7 +23,7 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.transition.MaterialFadeThrough
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.

View file

@ -114,8 +114,8 @@ constructor(
/**
* Get the index of the current value.
* @return The index of the current value within [values], or -1 if the [IntListPreference]
* is not set.
* @return The index of the current value within [values], or -1 if the [IntListPreference] is
* not set.
*/
fun getValueIndex(): Int {
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> {
override fun provideSummary(preference: IntListPreference): CharSequence {
val index = getValueIndex()

View file

@ -124,8 +124,8 @@ class PreferenceFragment : PreferenceFragmentCompat() {
context.getString(R.string.set_key_wipe_state) -> {
playbackModel.wipePlaybackState { wiped ->
if (wiped) {
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
this.context?.showToast(R.string.lbl_state_wiped)
} else {
this.context?.showToast(R.string.err_did_not_wipe)
@ -135,8 +135,8 @@ class PreferenceFragment : PreferenceFragmentCompat() {
context.getString(R.string.set_key_restore_state) ->
playbackModel.tryRestorePlaybackState { restored ->
if (restored) {
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
this.context?.showToast(R.string.lbl_state_restored)
} else {
this.context?.showToast(R.string.err_did_not_restore)

View file

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

View file

@ -15,7 +15,7 @@
* 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.util.AttributeSet
@ -35,8 +35,8 @@ import org.oxycblt.auxio.util.coordinatorLayoutBehavior
* 1. Lift state failing to update when list data changes.
* 2. Expansion causing jumping in [RecyclerView] instances.
*
* Note: This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what
* scrolling view to use. Failure to specify this will result in the layout not working.
* Note: This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what scrolling
* view to use. Failure to specify this will result in the layout not working.
*
* 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
* jumping around.
* @param recycler [RecyclerView] to expand with, or null if one is currently unavailable.
* TODO: Is it possible to use liftOnScrollTargetViewId to avoid the [RecyclerView] argument?
* @param recycler [RecyclerView] to expand with, or null if one is currently unavailable. TODO:
* Is it possible to use liftOnScrollTargetViewId to avoid the [RecyclerView] argument?
*/
fun expandWithRecycler(recycler: RecyclerView?) {
setExpanded(true)
@ -108,13 +108,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/**
* An [AppBarLayout.OnOffsetChangedListener] that will automatically move the given
* [RecyclerView] as the [AppBarLayout] expands. Should be added right when the view
* is expanding. Will be removed automatically.
* [RecyclerView] as the [AppBarLayout] expands. Should be added right when the view is
* expanding. Will be removed automatically.
* @param recycler [RecyclerView] to scroll with the [AppBarLayout].
*/
private class ExpansionHackListener(private val recycler: RecyclerView) :
OnOffsetChangedListener {
private val offsetAnimationMaxEndTime = (AnimationUtils.currentAnimationTimeMillis() +
private val offsetAnimationMaxEndTime =
(AnimationUtils.currentAnimationTimeMillis() +
APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION)
private var currentVerticalOffset: Int? = null
@ -123,8 +124,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
AnimationUtils.currentAnimationTimeMillis() > offsetAnimationMaxEndTime) {
// AppBarLayout crashes with IndexOutOfBoundsException when a non-last listener
// removes itself, so we have to do the removal asynchronously.
appBarLayout.postOnAnimation {
appBarLayout.removeOnOffsetChangedListener(this) }
appBarLayout.postOnAnimation { appBarLayout.removeOnOffsetChangedListener(this) }
}
// 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 {
/**
* @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
}
}

View file

@ -15,7 +15,7 @@
* 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.navigation.NavDirections
@ -25,14 +25,11 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.util.logD
/**
* A [ViewModel] that handles complicated navigation functionality.
*/
/** A [ViewModel] that handles complicated navigation functionality. */
class NavigationViewModel : ViewModel() {
private val _mainNavigationAction = MutableStateFlow<MainNavigationAction?>(null)
/**
* Flag for navigation within the main navigation graph. Only intended for use by
* MainFragment.
* Flag for navigation within the main navigation graph. Only intended for use by MainFragment.
*/
val mainNavigationAction: StateFlow<MainNavigationAction?>
get() = _mainNavigationAction
@ -47,17 +44,17 @@ class NavigationViewModel : ViewModel() {
private val _exploreNavigationArtists = MutableStateFlow<List<Artist>?>(null)
/**
* Variation of [exploreNavigationItem] for situations where the choice of [Artist]
* to navigate to is ambiguous. Only intended for use by MainFragment, as the resolved
* choice will eventually be assigned to [exploreNavigationItem].
* Variation of [exploreNavigationItem] for situations where the choice of [Artist] to navigate
* to is ambiguous. Only intended for use by MainFragment, as the resolved choice will
* eventually be assigned to [exploreNavigationItem].
*/
val exploreNavigationArtists: StateFlow<List<Artist>?>
get() = _exploreNavigationArtists
/**
* 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.
* Will do nothing if already navigating.
* navigation graph to trigger navigation in the higher-level main navigation graph. Will do
* nothing if already navigating.
* @param action The [MainNavigationAction] to perform.
*/
fun mainNavigateTo(action: MainNavigationAction) {
@ -81,8 +78,7 @@ class NavigationViewModel : ViewModel() {
/**
* Navigate to a given [Music] item. Will do nothing if already navigating.
* @param item The [Music] to navigate to.
* TODO: Extend to song properties???
* @param item The [Music] to navigate to. TODO: Extend to song properties???
*/
fun exploreNavigateTo(item: Music) {
if (_exploreNavigationItem.value != null) {
@ -96,8 +92,8 @@ class NavigationViewModel : ViewModel() {
/**
* 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
* user will be prompted with a choice on which [Artist] to navigate to.
* @param artists The [Artist]s to navigate to. In the case of multiple artists, the user will
* be prompted with a choice on which [Artist] to navigate to.
*/
fun exploreNavigateTo(artists: List<Artist>) {
if (_exploreNavigationArtists.value != null) {
@ -114,7 +110,7 @@ class NavigationViewModel : ViewModel() {
}
/**
* Mark that the navigation process within the explore navigation graph (initiated by
* Mark that the navigation process within the explore navigation graph (initiated by
* [exploreNavigateTo]) was completed.
*/
fun finishExploreNavigation() {
@ -126,8 +122,8 @@ class NavigationViewModel : ViewModel() {
/**
* 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
* in the app, including outside the main navigation graph.
* [NavigationViewModel] to initiate navigation in the main navigation graph from anywhere in the
* app, including outside the main navigation graph.
* @author Alexander Capehart (OxygenCobalt)
*/
sealed class MainNavigationAction {

View file

@ -15,7 +15,7 @@
* 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.view.LayoutInflater
@ -86,8 +86,8 @@ abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
}
/**
* Delegate to automatically create and destroy an object derived from the [ViewBinding].
* TODO: Phase this out, it's really dumb
* Delegate to automatically create and destroy an object derived from the [ViewBinding]. TODO:
* Phase this out, it's really dumb
* @param create Block to create the object from the [ViewBinding].
*/
fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
@ -140,9 +140,7 @@ abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
logD("Fragment destroyed")
}
/**
* Internal implementation of [lifecycleObject].
*/
/** Internal implementation of [lifecycleObject]. */
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
fun populate(binding: VB) {
data = create(binding)

View file

@ -15,7 +15,7 @@
* 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.view.LayoutInflater
@ -76,8 +76,8 @@ abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
}
/**
* Delegate to automatically create and destroy an object derived from the [ViewBinding].
* TODO: Phase this out, it's really dumb
* Delegate to automatically create and destroy an object derived from the [ViewBinding]. TODO:
* Phase this out, it's really dumb
* @param create Block to create the object from the [ViewBinding].
*/
fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
@ -121,9 +121,7 @@ abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
logD("Fragment destroyed")
}
/**
* Internal implementation of [lifecycleObject].
*/
/** Internal implementation of [lifecycleObject]. */
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
fun populate(binding: VB) {
data = create(binding)

View file

@ -15,7 +15,7 @@
* 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 org.oxycblt.auxio.R
@ -120,8 +120,7 @@ class Accent private constructor(val index: Int) : Item {
val theme: Int
get() = ACCENT_THEMES[index]
/**
* The black theme resource for this accent. Identical to [theme], but with a black
* background.
* The black theme resource for this accent. Identical to [theme], but with a black background.
*/
val blackTheme: Int
get() = ACCENT_BLACK_THEMES[index]
@ -137,20 +136,18 @@ class Accent private constructor(val index: Int) : Item {
/**
* Create a new instance.
* @param index The unique number for this particular accent.
* @return A new [Accent] with the specified [index]. If [index] is not within the
* range of valid accents, [index] will be [DEFAULT] instead.
* @return A new [Accent] with the specified [index]. If [index] is not within the range of
* valid accents, [index] will be [DEFAULT] instead.
*/
fun from(index: Int): Accent {
if (index !in 0 until MAX ) {
if (index !in 0 until MAX) {
logW("Accent is out of bounds [idx: $index]")
return Accent(DEFAULT)
}
return Accent(index)
}
/**
* The default accent.
*/
/** The default accent. */
val DEFAULT =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Use dynamic coloring on devices that support it.
@ -160,9 +157,7 @@ class Accent private constructor(val index: Int) : Item {
5
}
/**
* The amount of valid accents.
*/
/** The amount of valid accents. */
val MAX =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ACCENT_THEMES.size

View file

@ -15,7 +15,7 @@
* 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.ViewGroup

View file

@ -15,7 +15,7 @@
* 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.view.LayoutInflater
@ -26,7 +26,7 @@ import org.oxycblt.auxio.databinding.DialogAccentBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.Item
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.logD
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].
* @author Alexander Capehart (OxygenCobalt)
*/
class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>(), ClickableListListener {
class AccentCustomizeDialog :
ViewBindingDialogFragment<DialogAccentBinding>(), ClickableListListener {
private var accentAdapter = AccentAdapter(this)
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/>.
*/
package org.oxycblt.auxio.settings.accent
package org.oxycblt.auxio.ui.accent
import android.content.Context
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
* space in the [RecyclerView].
* Derived from this StackOverflow answer: https://stackoverflow.com/a/30256880/14143986
* space in the [RecyclerView]. Derived from this StackOverflow answer:
* https://stackoverflow.com/a/30256880/14143986
*/
class AccentGridLayoutManager(
context: Context,

View file

@ -47,17 +47,13 @@ import org.oxycblt.auxio.MainActivity
val Context.inflater: LayoutInflater
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
get() =
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
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
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()
}
/**
* 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 =
PendingIntent.getActivity(
this,

View file

@ -14,7 +14,7 @@
* 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.util
import android.content.Context
@ -50,11 +50,10 @@ import kotlinx.coroutines.launch
* Get if this [View] contains the given [PointF], with optional leeway.
* @param x The x 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.
* This can be used to extend the range where a point is considered "contained"
* by the [View] beyond it's actual size.
* @return true if the [PointF] is contained by the view, false otherwise.
* Adapted from AndroidFastScroll: https://github.com/zhanghai/AndroidFastScroll
* @param minTouchTargetSize A minimum size to use when checking the value. This can be used to
* extend the range where a point is considered "contained" by the [View] beyond it's actual size.
* @return true if the [PointF] is contained by the view, false otherwise. Adapted from
* AndroidFastScroll: https://github.com/zhanghai/AndroidFastScroll
*/
fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) =
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 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 minTouchTargetSize The minimum size to use when checking if the value is
* in range.
* @param minTouchTargetSize The minimum size to use when checking if the value is in range.
*/
private fun isUnderImpl(
position: Float,
@ -98,27 +96,21 @@ private fun isUnderImpl(
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
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
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
get() = root.context
/**
* Compute if this [RecyclerView] can scroll through their items, or if the items can all fit on
* one screen.
* Compute if this [RecyclerView] can scroll through their items, or if the items can all fit on one
* screen.
*/
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
* launching, the initializing call will occur ~100ms after draw time. If this is not desirable,
* use [collectImmediately].
* launching, the initializing call will occur ~100ms after draw time. If this is not desirable, use
* [collectImmediately].
* @param stateFlow The [StateFlow] to collect.
* @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
* immediately run an initializing call to ensure the UI is set up before draw-time. Note
* that this will result in two initializing calls.
* immediately run an initializing call to ensure the UI is set up before draw-time. Note that this
* will result in two initializing calls.
* @param stateFlow The [StateFlow] to collect.
* @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
* with the same block.
* Like [collectImmediately], but with two [StateFlow] instances that are collected with the same
* block.
* @param a The first [StateFlow] to collect.
* @param b The second [StateFlow] to collect.
* @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
* with the same block.
* Like [collectImmediately], but with three [StateFlow] instances that are collected with the same
* block.
* @param a The first [StateFlow] to collect.
* @param b The second [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].
* This should always been used when launching [Fragment] co-routines was it will not result
* in unexpected behavior.
* Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State]. This
* should always been used when launching [Fragment] co-routines was it will not result in
* unexpected behavior.
* @param state The [Lifecycle.State] to launch the co-routine in.
* @param block The block to run in the co-routine.
* @see repeatOnLifecycle
@ -208,40 +200,36 @@ private fun Fragment.launch(
/**
* An extension to [viewModels] that automatically provides an
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel]
* is used.
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used.
*/
inline fun <reified T : AndroidViewModel> Fragment.androidViewModels() =
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) }
/**
* An extension to [viewModels] that automatically provides an
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel]
* is used. Note that this implementation is for an [AppCompatActivity], and thus
* makes this functionally equivalent in scope to [androidActivityViewModels].
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used. Note
* that this implementation is for an [AppCompatActivity], and thus makes this functionally
* equivalent in scope to [androidActivityViewModels].
*/
inline fun <reified T : AndroidViewModel> AppCompatActivity.androidViewModels() =
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(application) }
/**
* An extension to [activityViewModels] that automatically provides an
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel]
* is used.
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used.
*/
inline fun <reified T : AndroidViewModel> Fragment.androidActivityViewModels() =
activityViewModels<T> {
ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
}
/**
* The [Context] provided to an [AndroidViewModel].
*/
/** The [Context] provided to an [AndroidViewModel]. */
inline val AndroidViewModel.context: Context
get() = getApplication()
/**
* Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor]
* is loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor]
* Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor] is
* loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor]
* resources.
* @param tableName The name of the table to query all columns in.
* @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)
/**
* Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner
* This can be used to prevent [View] elements from intersecting with the navigation bars.
* Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner This
* can be used to prevent [View] elements from intersecting with the navigation bars.
*/
val WindowInsets.systemBarInsetsCompat: Insets
get() =
@ -266,9 +254,9 @@ val WindowInsets.systemBarInsetsCompat: Insets
/**
* 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
* their extended gesture hit-boxes. Note that "System Bar" insets will be used if the system
* does not provide gesture insets.
* This can be used to prevent [View] elements from intersecting with the navigation bars and their
* extended gesture hit-boxes. Note that "System Bar" insets will be used if the system does not
* provide gesture insets.
*/
val WindowInsets.systemGestureInsetsCompat: Insets
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
/**
* Lazily set up a reflected field. Automatically handles visibility changes.
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
* Lazily set up a reflected field. Automatically handles visibility changes. Adapted from Material
* Files: https://github.com/zhanghai/MaterialFiles
* @param clazz The [KClass] to reflect into.
* @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 }
}
/**
* Lazily set up a reflected method. Automatically handles visibility changes.
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
* Lazily set up a reflected method. Automatically handles visibility changes. Adapted from Material
* Files: https://github.com/zhanghai/MaterialFiles
* @param clazz The [KClass] to reflect into.
* @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
* functions that don't necessarily require suspend, but still want to ensure that they
* are being called with a co-routine.
* Assert that the execution is currently on a background thread. This is helpful for functions that
* don't necessarily require suspend, but still want to ensure that they are being called with a
* co-routine.
* @throws IllegalStateException If the execution is not on a background thread.
*/
fun requireBackgroundThread() {

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