accent: move back to ui
Move the accent module back into the ui module, where it's more consistent.
This commit is contained in:
parent
ac137d4cc8
commit
9d283fc6e4
110 changed files with 1159 additions and 1168 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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>() {
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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?) =
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
*/
|
|
@ -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())
|
||||
}
|
||||
|
||||
/**
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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) }
|
||||
|
|
@ -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,
|
|
@ -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,
|
||||
|
|
|
@ -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() =
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue