diff --git a/app/build.gradle b/app/build.gradle index e85547c49..f088245c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,8 @@ plugins { id "kotlin-parcelize" id "dagger.hilt.android.plugin" id "kotlin-kapt" - id 'org.jetbrains.kotlin.android' + id "com.google.devtools.ksp" + id "org.jetbrains.kotlin.android" } android { @@ -118,7 +119,9 @@ dependencies { // Database def room_version = '2.6.0-alpha02' implementation "androidx.room:room-runtime:$room_version" - kapt "androidx.room:room-compiler:$room_version" + // I have no clue why, but using KSP breaks the playlist database definition. + //noinspection KaptUsageInsteadOfKsp + ksp "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" // --- THIRD PARTY --- diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 422a02764..ea618078c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -102,6 +102,7 @@ constructor( /** The current list data derived from [currentAlbum]. */ val albumList: StateFlow> get() = _albumList + private val _albumInstructions = MutableEvent() /** Instructions for updating [albumList] in the UI. */ val albumInstructions: Event diff --git a/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt index f4654ace4..e8546a133 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt @@ -106,6 +106,7 @@ sealed interface ArtistShowChoices { class FromSong(val song: Song) : ArtistShowChoices { override val uid = song.uid override val choices = song.artists + override fun sanitize(newLibrary: DeviceLibrary) = newLibrary.findSong(uid)?.let { FromSong(it) } } @@ -116,6 +117,7 @@ sealed interface ArtistShowChoices { data class FromAlbum(val album: Album) : ArtistShowChoices { override val uid = album.uid override val choices = album.artists + override fun sanitize(newLibrary: DeviceLibrary) = newLibrary.findAlbum(uid)?.let { FromAlbum(it) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt index 02303a566..cb2343219 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt @@ -41,6 +41,7 @@ class ArtistDetailHeaderAdapter(private val listener: Listener) : DetailHeaderAdapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ArtistDetailHeaderViewHolder.from(parent) + override fun onBindHeader(holder: ArtistDetailHeaderViewHolder, parent: Artist) = holder.bind(parent, listener) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt index 247875432..4afabb6c3 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt @@ -30,7 +30,9 @@ import org.oxycblt.auxio.util.logD abstract class DetailHeaderAdapter : RecyclerView.Adapter() { private var currentParent: T? = null + final override fun getItemCount() = 1 + final override fun onBindViewHolder(holder: VH, position: Int) = onBindHeader(holder, requireNotNull(currentParent)) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt index 06c5be29b..5b1de107b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt @@ -211,6 +211,7 @@ private constructor(private val binding: ItemEditableSongBinding) : PlaylistDetailListAdapter.ViewHolder { override val enabled: Boolean get() = binding.songDragHandle.isVisible + override val root = binding.root override val body = binding.body override val delete = binding.background diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index abd3bf15a..834b25fe6 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -622,6 +622,7 @@ class HomeFragment : lifecycleOwner: LifecycleOwner ) : FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle) { override fun getItemCount() = tabs.size + override fun createFragment(position: Int): Fragment = when (tabs[position]) { MusicType.SONGS -> SongListFragment() diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index ac16e2e8b..a89b73b3d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -59,6 +59,7 @@ constructor( /** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */ val songsList: StateFlow> get() = _songsList + private val _songsInstructions = MutableEvent() /** Instructions for how to update [songsList] in the UI. */ val songsInstructions: Event @@ -68,6 +69,7 @@ constructor( /** A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view. */ val albumsList: StateFlow> get() = _albumsLists + private val _albumsInstructions = MutableEvent() /** Instructions for how to update [albumsList] in the UI. */ val albumsInstructions: Event @@ -80,6 +82,7 @@ constructor( */ val artistsList: MutableStateFlow> get() = _artistsList + private val _artistsInstructions = MutableEvent() /** Instructions for how to update [artistsList] in the UI. */ val artistsInstructions: Event @@ -89,6 +92,7 @@ constructor( /** A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view. */ val genresList: StateFlow> get() = _genresList + private val _genresInstructions = MutableEvent() /** Instructions for how to update [genresList] in the UI. */ val genresInstructions: Event @@ -98,6 +102,7 @@ constructor( /** A list of [Playlist]s, sorted by the preferred [Sort], to be shown in the home view. */ val playlistsList: StateFlow> get() = _playlistsList + private val _playlistsInstructions = MutableEvent() /** Instructions for how to update [genresList] in the UI. */ val playlistsInstructions: Event @@ -289,5 +294,6 @@ constructor( sealed interface Outer { object Settings : Outer + object About : Outer } diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt index 620ac018f..ae546d137 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt @@ -123,8 +123,11 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) } override fun isAutoMirrored(): Boolean = true + override fun setAlpha(alpha: Int) {} + override fun setColorFilter(colorFilter: ColorFilter?) {} + override fun getOpacity(): Int = PixelFormat.TRANSLUCENT private fun updatePath() { diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt index aa71b89f1..736b5ba30 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt @@ -42,7 +42,9 @@ class TabAdapter(private val listener: EditClickListListener) : private set override fun getItemCount() = tabs.size + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.from(parent) + override fun onBindViewHolder(holder: TabViewHolder, position: Int) { holder.bind(tabs[position], listener) } diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index d8d9b8d64..de00d7c9e 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -88,6 +88,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr val playingDrawable: AnimationDrawable, val pausedDrawable: Drawable ) + private val playbackIndicator: PlaybackIndicator? private val selectionBadge: ImageView? @@ -105,6 +106,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr val desc: String, @DrawableRes val errorRes: Int ) + private var currentCover: Cover? = null init { diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt index a32b8a0f5..115a118a0 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt @@ -225,6 +225,7 @@ sealed interface Menu { @get:MenuRes val res: Int /** A [Parcel] version of this instance that can be used as a navigation argument. */ val parcel: Parcel + sealed interface Parcel : Parcelable /** Navigate to a [Song] menu dialog. */ @@ -257,6 +258,7 @@ sealed interface Menu { class ForAlbum(@MenuRes override val res: Int, val album: Album) : Menu { override val parcel get() = Parcel(res, album.uid) + @Parcelize data class Parcel(val res: Int, val albumUid: Music.UID) : Menu.Parcel } @@ -264,6 +266,7 @@ sealed interface Menu { class ForArtist(@MenuRes override val res: Int, val artist: Artist) : Menu { override val parcel get() = Parcel(res, artist.uid) + @Parcelize data class Parcel(val res: Int, val artistUid: Music.UID) : Menu.Parcel } @@ -271,6 +274,7 @@ sealed interface Menu { class ForGenre(@MenuRes override val res: Int, val genre: Genre) : Menu { override val parcel get() = Parcel(res, genre.uid) + @Parcelize data class Parcel(val res: Int, val genreUid: Music.UID) : Menu.Parcel } @@ -278,6 +282,7 @@ sealed interface Menu { class ForPlaylist(@MenuRes override val res: Int, val playlist: Playlist) : Menu { override val parcel get() = Parcel(res, playlist.uid) + @Parcelize data class Parcel(val res: Int, val playlistUid: Music.UID) : Menu.Parcel } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt index 977c367c4..3b7d8d476 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt @@ -37,6 +37,7 @@ abstract class FlexibleListAdapter( diffCallback: DiffUtil.ItemCallback ) : RecyclerView.Adapter() { @Suppress("LeakingThis") private val differ = FlexibleListDiffer(this, diffCallback) + final override fun getItemCount() = differ.currentList.size /** The current list stored by the adapter's differ instance. */ val currentList: List @@ -118,6 +119,7 @@ private class FlexibleListDiffer( private class MainThreadExecutor : Executor { val mHandler = Handler(Looper.getMainLooper()) + override fun execute(command: Runnable) { mHandler.post(command) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt index e875dd04b..b7e9d3b0d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt @@ -28,5 +28,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) interface MusicModule { @Singleton @Binds fun repository(musicRepository: MusicRepositoryImpl): MusicRepository + @Binds fun settings(musicSettingsImpl: MusicSettingsImpl): MusicSettings } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index 4274ef4e8..b2685fc43 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -176,6 +176,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context apply() } } + override var albumSongSort: Sort get() { var sort = diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt index b1a19d52a..d28547239 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt @@ -40,7 +40,9 @@ abstract class CacheDatabase : RoomDatabase() { @Dao interface CachedSongsDao { @Query("SELECT * FROM CachedSong") suspend fun readSongs(): List + @Query("DELETE FROM CachedSong") suspend fun nukeSongs() + @Insert suspend fun insertSongs(songs: List) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt index 0b91c65b3..441a9a7fa 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt @@ -100,6 +100,7 @@ private class CacheImpl(cachedSongs: List) : Cache { } override var invalidated = false + override fun populate(rawSong: RawSong): Boolean { // 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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 622fd8652..cd75ba578 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -266,14 +266,19 @@ class DeviceLibraryImpl( // All other music is built from songs, so comparison only needs to check songs. override fun equals(other: Any?) = other is DeviceLibrary && other.songs == songs + override fun hashCode() = songs.hashCode() + override fun toString() = "DeviceLibrary(songs=${songs.size}, albums=${albums.size}, " + "artists=${artists.size}, genres=${genres.size})" override fun findSong(uid: Music.UID): Song? = songUidMap[uid] + override fun findAlbum(uid: Music.UID): Album? = albumUidMap[uid] + override fun findArtist(uid: Music.UID): Artist? = artistUidMap[uid] + override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid] override fun findSongForUri(context: Context, uri: Uri) = diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index e568b8f16..14497e909 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -102,8 +102,10 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son private val hashCode = 31 * uid.hashCode() + rawSong.hashCode() override fun hashCode() = hashCode + override fun equals(other: Any?) = other is SongImpl && uid == other.uid && rawSong == other.rawSong + override fun toString() = "Song(uid=$uid, name=$name)" private val artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings) @@ -313,8 +315,10 @@ class AlbumImpl( } override fun hashCode() = hashCode + override fun equals(other: Any?) = other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs + override fun toString() = "Album(uid=$uid, name=$name)" /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index 11a357f45..76e62e897 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -74,9 +74,13 @@ interface MediaStoreExtractor { /** A black-box interface representing a query from the media database. */ interface Query { val projectedTotal: Int + fun moveToNext(): Boolean + fun close() + fun populateFileInfo(rawSong: RawSong) + fun populateTags(rawSong: RawSong) } @@ -285,7 +289,9 @@ private abstract class BaseMediaStoreExtractor( private val albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST) final override val projectedTotal = cursor.count + final override fun moveToNext() = cursor.moveToNext() + final override fun close() = cursor.close() override fun populateFileInfo(rawSong: RawSong) { @@ -524,6 +530,7 @@ private class Api29MediaStoreExtractor(context: Context, musicSettings: MusicSet storageManager: StorageManager ) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) { private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) + override fun populateTags(rawSong: RawSong) { super.populateTags(rawSong) // This extractor is volume-aware, but does not support the modern track columns. diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt index 783b1356a..4eb8969f4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt @@ -74,8 +74,11 @@ class Date private constructor(private val tokens: List) : Comparable } override fun equals(other: Any?) = other is Date && compareTo(other) == 0 + override fun hashCode() = tokens.hashCode() + override fun toString() = StringBuilder().appendDate().toString() + override fun compareTo(other: Date): Int { for (i in 0 until max(tokens.size, other.tokens.size)) { val ai = tokens.getOrNull(i) diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt index 52b7ab646..2c8fd360b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt @@ -29,6 +29,8 @@ import org.oxycblt.auxio.list.Item class Disc(val number: Int, val name: String?) : Item, Comparable { // We don't want to group discs by differing subtitles, so only compare by the number override fun equals(other: Any?) = other is Disc && number == other.number + override fun hashCode() = number.hashCode() + override fun compareTo(other: Disc) = number.compareTo(other.number) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt index 03f3a33f6..acef9dbfc 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt @@ -132,7 +132,9 @@ sealed interface Name : Comparable { */ data class Unknown(@StringRes val stringRes: Int) : Name { override val thumb = "?" + override fun resolve(context: Context) = context.getString(stringRes) + override fun compareTo(other: Name) = when (other) { // Unknown names do not need any direct comparison right now. diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt index 96685a746..1f42df505 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt @@ -27,6 +27,8 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) interface MetadataModule { @Binds fun tagExtractor(extractor: TagExtractorImpl): TagExtractor + @Binds fun tagWorkerFactory(factory: TagWorkerFactoryImpl): TagWorker.Factory + @Binds fun audioPropertiesFactory(factory: AudioPropertiesFactoryImpl): AudioProperties.Factory } diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt index 19cf12401..ffe7a5174 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt @@ -42,7 +42,9 @@ private constructor( override fun equals(other: Any?) = other is PlaylistImpl && uid == other.uid && name == other.name && songs == other.songs + override fun hashCode() = hashCode + override fun toString() = "Playlist(uid=$uid, name=$name)" /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index 5e0e7ca55..06de6d64f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -171,7 +171,9 @@ private class UserLibraryImpl( private val musicSettings: MusicSettings ) : MutableUserLibrary { override fun hashCode() = playlistMap.hashCode() + override fun equals(other: Any?) = other is UserLibraryImpl && other.playlistMap == playlistMap + override fun toString() = "UserLibrary(playlists=${playlists.size})" override val playlists: Collection diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt index 9c46bbe78..21ba9cda4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt @@ -51,7 +51,7 @@ abstract class UserMusicDatabase : RoomDatabase() { * @author Alexander Capehart (OxygenCobalt) */ @Dao -interface PlaylistDao { +abstract class PlaylistDao() { /** * Read out all playlists stored in the database. * @@ -59,7 +59,7 @@ interface PlaylistDao { */ @Transaction @Query("SELECT * FROM PlaylistInfo") - suspend fun readRawPlaylists(): List + abstract suspend fun readRawPlaylists(): List /** * Create a new playlist. @@ -67,7 +67,7 @@ interface PlaylistDao { * @param rawPlaylist The [RawPlaylist] to create. */ @Transaction - suspend fun insertPlaylist(rawPlaylist: RawPlaylist) { + open suspend fun insertPlaylist(rawPlaylist: RawPlaylist) { insertInfo(rawPlaylist.playlistInfo) insertSongs(rawPlaylist.songs) insertRefs( @@ -83,7 +83,7 @@ interface PlaylistDao { * @param playlistInfo The new [PlaylistInfo] to store. */ @Transaction - suspend fun replacePlaylistInfo(playlistInfo: PlaylistInfo) { + open suspend fun replacePlaylistInfo(playlistInfo: PlaylistInfo) { deleteInfo(playlistInfo.playlistUid) insertInfo(playlistInfo) } @@ -94,7 +94,7 @@ interface PlaylistDao { * @param playlistUid The [Music.UID] of the playlist to delete. */ @Transaction - suspend fun deletePlaylist(playlistUid: Music.UID) { + open suspend fun deletePlaylist(playlistUid: Music.UID) { deleteInfo(playlistUid) deleteRefs(playlistUid) } @@ -106,7 +106,7 @@ interface PlaylistDao { * @param songs The [PlaylistSong] representing each song to put into the playlist. */ @Transaction - suspend fun insertPlaylistSongs(playlistUid: Music.UID, songs: List) { + open suspend fun insertPlaylistSongs(playlistUid: Music.UID, songs: List) { insertSongs(songs) insertRefs( songs.map { PlaylistSongCrossRef(playlistUid = playlistUid, songUid = it.songUid) }) @@ -120,7 +120,7 @@ interface PlaylistDao { * playlist. */ @Transaction - suspend fun replacePlaylistSongs(playlistUid: Music.UID, songs: List) { + open suspend fun replacePlaylistSongs(playlistUid: Music.UID, songs: List) { deleteRefs(playlistUid) insertSongs(songs) insertRefs( @@ -128,21 +128,22 @@ interface PlaylistDao { } /** Internal, do not use. */ - @Insert(onConflict = OnConflictStrategy.ABORT) suspend fun insertInfo(info: PlaylistInfo) + @Insert(onConflict = OnConflictStrategy.ABORT) + abstract suspend fun insertInfo(info: PlaylistInfo) /** Internal, do not use. */ @Query("DELETE FROM PlaylistInfo where playlistUid = :playlistUid") - suspend fun deleteInfo(playlistUid: Music.UID) + abstract suspend fun deleteInfo(playlistUid: Music.UID) /** Internal, do not use. */ @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertSongs(songs: List) + abstract suspend fun insertSongs(songs: List) /** Internal, do not use. */ @Insert(onConflict = OnConflictStrategy.ABORT) - suspend fun insertRefs(refs: List) + abstract suspend fun insertRefs(refs: List) /** Internal, do not use. */ @Query("DELETE FROM PlaylistSongCrossRef where playlistUid = :playlistUid") - suspend fun deleteRefs(playlistUid: Music.UID) + abstract suspend fun deleteRefs(playlistUid: Music.UID) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackModule.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackModule.kt index 36fb9a0ee..37e3eb4c2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackModule.kt @@ -32,5 +32,6 @@ interface PlaybackModule { @Singleton @Binds fun stateManager(playbackManager: PlaybackStateManagerImpl): PlaybackStateManager + @Binds fun settings(playbackSettings: PlaybackSettingsImpl): PlaybackSettings } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index cebba2850..9b7ad397b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -67,6 +67,7 @@ constructor( /** The currently playing song. */ val song: StateFlow get() = _song + private val _parent = MutableStateFlow(null) /** The [MusicParent] currently being played. Null if playback is occurring from all songs. */ val parent: StateFlow = _parent @@ -74,6 +75,7 @@ constructor( /** Whether playback is ongoing or paused. */ val isPlaying: StateFlow get() = _isPlaying + private val _positionDs = MutableStateFlow(0L) /** The current position, in deci-seconds (1/10th of a second). */ val positionDs: StateFlow @@ -83,6 +85,7 @@ constructor( /** The current [RepeatMode]. */ val repeatMode: StateFlow get() = _repeatMode + private val _isShuffled = MutableStateFlow(false) /** Whether the queue is shuffled or not. */ val isShuffled: StateFlow diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt index c5ebf9904..bc421b846 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt @@ -114,12 +114,14 @@ class MutableQueue : Queue { @Volatile override var index = -1 private set + override val currentSong: Song? get() = shuffledMapping .ifEmpty { orderedMapping.ifEmpty { null } } ?.getOrNull(index) ?.let(heap::get) + override val isShuffled: Boolean get() = shuffledMapping.isNotEmpty() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index 5c039ab8c..bd4ef874c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -22,7 +22,7 @@ import androidx.media3.common.C import androidx.media3.common.Format import androidx.media3.common.Player import androidx.media3.common.audio.AudioProcessor -import androidx.media3.exoplayer.audio.BaseAudioProcessor +import androidx.media3.common.audio.BaseAudioProcessor import java.nio.ByteBuffer import javax.inject.Inject import kotlin.math.pow @@ -81,6 +81,7 @@ constructor( applyReplayGain(queue.currentSong) } } + override fun onNewPlayback(queue: Queue, parent: MusicParent?) { logD("New playback started, updating playback information") applyReplayGain(queue.currentSong) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 388087653..870d7a84e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -309,15 +309,18 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Volatile override var parent: MusicParent? = null private set + @Volatile override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0) private set + @Volatile override var repeatMode = RepeatMode.NONE set(value) { field = value notifyRepeatModeChanged() } + override val currentAudioSessionId: Int? get() = internalPlayer?.audioSessionId diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchModule.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchModule.kt index 18c857ba4..fb8e7e063 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchModule.kt @@ -27,5 +27,6 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) interface SearchModule { @Binds fun engine(searchEngine: SearchEngineImpl): SearchEngine + @Binds fun settings(searchSettings: SearchSettingsImpl): SearchSettings } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreferenceDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreferenceDialog.kt index 2995b6353..67ad6b1f3 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreferenceDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreferenceDialog.kt @@ -35,6 +35,7 @@ import org.oxycblt.auxio.util.fixDoubleRipple class IntListPreferenceDialog : PreferenceDialogFragmentCompat() { private val listPreference: IntListPreference get() = (preference as IntListPreference) + private var pendingValueIndex = -1 override fun onStart() { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt index 9937d1eac..0299a6af8 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt @@ -76,6 +76,7 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: // Enable experimental settings that allow us to skip the half-expanded state. override fun shouldSkipHalfExpandedStateWhenDragging() = true + override fun shouldExpandOnUpwardDrag(dragDurationMillis: Long, yPositionPercentage: Float) = true diff --git a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt index c1e1a4a92..eb74d8f15 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt @@ -52,6 +52,7 @@ interface Event { */ class MutableEvent : Event { override val flow = MutableStateFlow(null) + override fun consume() = flow.value?.also { flow.value = null } /** diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index bb2e7b54f..e6f86736a 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -137,13 +137,18 @@ constructor( // Respond to all major song or player changes that will affect the widget override fun onIndexMoved(queue: Queue) = update() + override fun onQueueReordered(queue: Queue) = update() + override fun onNewPlayback(queue: Queue, parent: MusicParent?) = update() + override fun onStateChanged(state: InternalPlayer.State) = update() + override fun onRepeatChanged(repeatMode: RepeatMode) = update() // Respond to settings changes that will affect the widget override fun onRoundModeChanged() = update() + override fun onImageSettingsChanged() = update() /** diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt index 1c322e36a..32d8c0df2 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/FakeMusic.kt @@ -29,30 +29,43 @@ import org.oxycblt.auxio.music.info.ReleaseType open class FakeSong : Song { override val name: Name get() = throw NotImplementedError() + override val date: Date? get() = throw NotImplementedError() + override val dateAdded: Long get() = throw NotImplementedError() + override val disc: Disc? get() = throw NotImplementedError() + override val genres: List get() = throw NotImplementedError() + override val mimeType: MimeType get() = throw NotImplementedError() + override val track: Int? get() = throw NotImplementedError() + override val path: Path get() = throw NotImplementedError() + override val size: Long get() = throw NotImplementedError() + override val uri: Uri get() = throw NotImplementedError() + override val album: Album get() = throw NotImplementedError() + override val artists: List get() = throw NotImplementedError() + override val durationMs: Long get() = throw NotImplementedError() + override val uid: Music.UID get() = throw NotImplementedError() } @@ -60,20 +73,28 @@ open class FakeSong : Song { open class FakeAlbum : Album { override val name: Name get() = throw NotImplementedError() + override val coverUri: Uri get() = throw NotImplementedError() + override val dateAdded: Long get() = throw NotImplementedError() + override val dates: Date.Range? get() = throw NotImplementedError() + override val releaseType: ReleaseType get() = throw NotImplementedError() + override val artists: List get() = throw NotImplementedError() + override val durationMs: Long get() = throw NotImplementedError() + override val songs: List get() = throw NotImplementedError() + override val uid: Music.UID get() = throw NotImplementedError() } @@ -81,18 +102,25 @@ open class FakeAlbum : Album { open class FakeArtist : Artist { override val name: Name get() = throw NotImplementedError() + override val albums: List get() = throw NotImplementedError() + override val explicitAlbums: List get() = throw NotImplementedError() + override val implicitAlbums: List get() = throw NotImplementedError() + override val genres: List get() = throw NotImplementedError() + override val durationMs: Long get() = throw NotImplementedError() + override val songs: List get() = throw NotImplementedError() + override val uid: Music.UID get() = throw NotImplementedError() } @@ -100,12 +128,16 @@ open class FakeArtist : Artist { open class FakeGenre : Genre { override val name: Name get() = throw NotImplementedError() + override val artists: List get() = throw NotImplementedError() + override val durationMs: Long get() = throw NotImplementedError() + override val songs: List get() = throw NotImplementedError() + override val uid: Music.UID get() = throw NotImplementedError() } diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicRepository.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicRepository.kt index 4af3e64b3..8c79f0e9a 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicRepository.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicRepository.kt @@ -25,8 +25,10 @@ import org.oxycblt.auxio.music.user.UserLibrary open class FakeMusicRepository : MusicRepository { override val indexingState: IndexingState? get() = throw NotImplementedError() + override val deviceLibrary: DeviceLibrary? get() = throw NotImplementedError() + override val userLibrary: UserLibrary? get() = throw NotImplementedError() diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt index 66cd8e880..e1beae695 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt @@ -23,40 +23,54 @@ import org.oxycblt.auxio.music.fs.MusicDirectories open class FakeMusicSettings : MusicSettings { override fun registerListener(listener: MusicSettings.Listener) = throw NotImplementedError() + override fun unregisterListener(listener: MusicSettings.Listener) = throw NotImplementedError() + override var musicDirs: MusicDirectories get() = throw NotImplementedError() set(_) = throw NotImplementedError() + override val excludeNonMusic: Boolean get() = throw NotImplementedError() + override val shouldBeObserving: Boolean get() = throw NotImplementedError() + override var multiValueSeparators: String get() = throw NotImplementedError() set(_) = throw NotImplementedError() + override val intelligentSorting: Boolean get() = throw NotImplementedError() + override var songSort: Sort get() = throw NotImplementedError() set(_) = throw NotImplementedError() + override var albumSort: Sort get() = throw NotImplementedError() set(_) = throw NotImplementedError() + override var artistSort: Sort get() = throw NotImplementedError() set(_) = throw NotImplementedError() + override var genreSort: Sort get() = throw NotImplementedError() set(_) = throw NotImplementedError() + override var playlistSort: Sort get() = throw NotImplementedError() set(_) = throw NotImplementedError() + override var albumSongSort: Sort get() = throw NotImplementedError() set(_) = throw NotImplementedError() + override var artistSongSort: Sort get() = throw NotImplementedError() set(_) = throw NotImplementedError() + override var genreSongSort: Sort get() = throw NotImplementedError() set(_) = throw NotImplementedError() diff --git a/app/src/test/java/org/oxycblt/auxio/music/MusicViewModelTest.kt b/app/src/test/java/org/oxycblt/auxio/music/MusicViewModelTest.kt index 8ad02dbb6..b25c2c0b1 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/MusicViewModelTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/MusicViewModelTest.kt @@ -77,6 +77,7 @@ class MusicViewModelTest { updateListener?.onMusicChanges( MusicRepository.Changes(deviceLibrary = true, userLibrary = false)) } + override var indexingState: IndexingState? = null set(value) { field = value @@ -114,10 +115,13 @@ class MusicViewModelTest { private class TestDeviceLibrary : FakeDeviceLibrary() { override val songs: List get() = listOf(TestSong(), TestSong()) + override val albums: List get() = listOf(FakeAlbum(), FakeAlbum(), FakeAlbum()) + override val artists: List get() = listOf(FakeArtist(), FakeArtist(), FakeArtist(), FakeArtist()) + override val genres: List get() = listOf(FakeGenre()) } diff --git a/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt b/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt index d08e04615..dab0834a3 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/device/FakeDeviceLibrary.kt @@ -29,10 +29,13 @@ import org.oxycblt.auxio.music.Song open class FakeDeviceLibrary : DeviceLibrary { override val songs: List get() = throw NotImplementedError() + override val albums: List get() = throw NotImplementedError() + override val artists: List get() = throw NotImplementedError() + override val genres: List get() = throw NotImplementedError() diff --git a/build.gradle b/build.gradle index bcc63d310..75fa2ca64 100644 --- a/build.gradle +++ b/build.gradle @@ -2,33 +2,23 @@ buildscript { ext { kotlin_version = '1.9.0' navigation_version = "2.6.0" - hilt_version = '2.46.1' - } - - repositories { - google() - mavenCentral() + hilt_version = '2.47' } dependencies { - classpath 'com.android.tools.build:gradle:8.0.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version" - classpath "com.diffplug.spotless:spotless-plugin-gradle:6.18.0" + // Hilt isn't compatible with the new plugin syntax yet. classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files } } -allprojects { - repositories { - google() - mavenCentral() - } +plugins { + id "com.android.application" version "8.1.0" apply false + id "androidx.navigation.safeargs.kotlin" version "$navigation_version" apply false + id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false + id "com.google.devtools.ksp" version '1.9.0-1.0.12' apply false + id "com.diffplug.spotless" version "6.20.0" apply false } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2c3425d49..42e01c062 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionSha256Sum=a8451eeda314d0568b5340498b36edf147a8f0d692c5ff58082d477abe9146e4 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index c8be53d93..61caa7aa7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,22 @@ -include ':app' -rootProject.name = "Auxio" +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + gradle.ext.androidxMediaModulePrefix = 'media-' gradle.ext.androidxMediaProjectName = 'media-' -apply from: file("media/core_settings.gradle") \ No newline at end of file +apply from: file("media/core_settings.gradle") + +rootProject.name = "Auxio" +include ':app' \ No newline at end of file