diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 359afd9c3..285e94c2c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -30,8 +30,8 @@ import kotlinx.parcelize.Parcelize import org.oxycblt.auxio.image.extractor.Cover import org.oxycblt.auxio.image.extractor.ParentCover import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.music.fs.MimeType -import org.oxycblt.auxio.music.fs.Path +import org.oxycblt.auxio.music.stack.fs.MimeType +import org.oxycblt.auxio.music.stack.fs.Path import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Name diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 1de4de180..5db58c9f7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.yield import org.oxycblt.auxio.music.cache.CacheRepository import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.RawSong -import org.oxycblt.auxio.music.fs.MediaStoreExtractor import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.music.metadata.TagExtractor 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 2b11b885a..f7993a881 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -24,7 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.music.dirs.MusicDirectories -import org.oxycblt.auxio.music.fs.DocumentPathFactory +import org.oxycblt.auxio.music.stack.fs.DocumentPathFactory import org.oxycblt.auxio.settings.Settings import timber.log.Timber as L 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 f3c8b47e1..86aa56e5f 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 @@ -31,9 +31,9 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.fs.Path -import org.oxycblt.auxio.music.fs.contentResolverSafe -import org.oxycblt.auxio.music.fs.useQuery +import org.oxycblt.auxio.music.stack.fs.Path +import org.oxycblt.auxio.music.stack.fs.contentResolverSafe +import org.oxycblt.auxio.music.stack.fs.useQuery import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.util.forEachWithTimeout 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 1a90b4cd7..a00450b8f 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 @@ -28,10 +28,10 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.fs.MimeType -import org.oxycblt.auxio.music.fs.toAlbumCoverUri -import org.oxycblt.auxio.music.fs.toAudioUri -import org.oxycblt.auxio.music.fs.toSongCoverUri +import org.oxycblt.auxio.music.stack.fs.MimeType +import org.oxycblt.auxio.music.stack.fs.toAlbumCoverUri +import org.oxycblt.auxio.music.stack.fs.toAudioUri +import org.oxycblt.auxio.music.stack.fs.toSongCoverUri import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Name diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryAdapter.kt index 1b9cae7c7..f824082cb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryAdapter.kt @@ -23,7 +23,7 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.ItemMusicDirBinding import org.oxycblt.auxio.list.recycler.DialogRecyclerView -import org.oxycblt.auxio.music.fs.Path +import org.oxycblt.auxio.music.stack.fs.Path import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater import timber.log.Timber as L diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirectories.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirectories.kt index a4082cbf7..d71c81a8f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirectories.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirectories.kt @@ -18,7 +18,7 @@ package org.oxycblt.auxio.music.dirs -import org.oxycblt.auxio.music.fs.Path +import org.oxycblt.auxio.music.stack.fs.Path /** * Represents the configuration for specific directories to filter to/from when loading music. diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt index fd433ba57..f37cfc340 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt @@ -33,8 +33,8 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.music.MusicSettings -import org.oxycblt.auxio.music.fs.DocumentPathFactory -import org.oxycblt.auxio.music.fs.Path +import org.oxycblt.auxio.music.stack.fs.DocumentPathFactory +import org.oxycblt.auxio.music.stack.fs.Path import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.showToast import timber.log.Timber as L diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt index 1ba167f47..debe8be5f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt @@ -23,10 +23,10 @@ import android.net.Uri import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.music.Playlist -import org.oxycblt.auxio.music.fs.Components -import org.oxycblt.auxio.music.fs.DocumentPathFactory -import org.oxycblt.auxio.music.fs.Path -import org.oxycblt.auxio.music.fs.contentResolverSafe +import org.oxycblt.auxio.music.stack.fs.Components +import org.oxycblt.auxio.music.stack.fs.DocumentPathFactory +import org.oxycblt.auxio.music.stack.fs.Path +import org.oxycblt.auxio.music.stack.fs.contentResolverSafe import timber.log.Timber as L /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt index 717f43619..3b8543f74 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt @@ -27,10 +27,10 @@ import java.io.InputStreamReader import java.io.OutputStream import javax.inject.Inject import org.oxycblt.auxio.music.Playlist -import org.oxycblt.auxio.music.fs.Components -import org.oxycblt.auxio.music.fs.Path -import org.oxycblt.auxio.music.fs.Volume -import org.oxycblt.auxio.music.fs.VolumeManager +import org.oxycblt.auxio.music.stack.fs.Components +import org.oxycblt.auxio.music.stack.fs.Path +import org.oxycblt.auxio.music.stack.fs.Volume +import org.oxycblt.auxio.music.stack.fs.VolumeManager import org.oxycblt.auxio.music.metadata.correctWhitespace import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.unlikelyToBeNull 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 deleted file mode 100644 index d2dc884eb..000000000 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ /dev/null @@ -1,440 +0,0 @@ -/* - * Copyright (c) 2022 Auxio Project - * MediaStoreExtractor.kt is part of Auxio. - * - * 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 . - */ - -package org.oxycblt.auxio.music.fs - -import android.content.Context -import android.database.Cursor -import android.os.Build -import android.provider.MediaStore -import androidx.core.database.getIntOrNull -import androidx.core.database.getStringOrNull -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.yield -import org.oxycblt.auxio.music.cache.Cache -import org.oxycblt.auxio.music.device.RawSong -import org.oxycblt.auxio.music.dirs.MusicDirectories -import org.oxycblt.auxio.music.info.Date -import org.oxycblt.auxio.music.metadata.parseId3v2PositionField -import org.oxycblt.auxio.music.metadata.transformPositionField -import org.oxycblt.auxio.util.sendWithTimeout -import timber.log.Timber as L - -/** - * 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 other extractors. Solely relying on this is not recommended, as it often produces bad - * metadata. - * - * @author Alexander Capehart (OxygenCobalt) - */ -interface MediaStoreExtractor { - /** - * Query the media database. - * - * @param constraints Configuration parameter to restrict what music should be ignored when - * querying. - * @return A new [Query] returned from the media database. - */ - suspend fun query(constraints: Constraints): Query - - /** - * Consume the [Cursor] loaded after [query]. - * - * @param query The [Query] to consume. - * @param cache A [Cache] used to avoid extracting metadata for cached songs, or null if no - * [Cache] was available. - * @param incompleteSongs A channel where songs that could not be retrieved from the [Cache] - * should be sent to. - * @param completeSongs A channel where completed songs should be sent to. - */ - suspend fun consume( - query: Query, - cache: Cache?, - incompleteSongs: Channel, - completeSongs: Channel - ) - - /** 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): Boolean - - fun populateTags(rawSong: RawSong) - } - - data class Constraints(val excludeNonMusic: Boolean, val musicDirs: MusicDirectories) - - companion object { - /** - * Create a framework-backed instance. - * - * @param context [Context] required. - * @param pathInterpreterFactory A [MediaStorePathInterpreter.Factory] to use. - * @return A new [MediaStoreExtractor] that will work best on the device's API level. - */ - fun from( - context: Context, - pathInterpreterFactory: MediaStorePathInterpreter.Factory - ): MediaStoreExtractor { - val tagInterpreterFactory = - when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30TagInterpreter.Factory() - else -> Api21TagInterpreter.Factory() - } - - return MediaStoreExtractorImpl(context, pathInterpreterFactory, tagInterpreterFactory) - } - } -} - -private class MediaStoreExtractorImpl( - private val context: Context, - private val mediaStorePathInterpreterFactory: MediaStorePathInterpreter.Factory, - private val tagInterpreterFactory: TagInterpreter.Factory -) : MediaStoreExtractor { - override suspend fun query( - constraints: MediaStoreExtractor.Constraints - ): MediaStoreExtractor.Query { - val start = System.currentTimeMillis() - - val projection = - BASE_PROJECTION + - mediaStorePathInterpreterFactory.projection + - tagInterpreterFactory.projection - var uniSelector = BASE_SELECTOR - var uniArgs = listOf() - - // Filter out audio that is not music, if enabled. - if (constraints.excludeNonMusic) { - L.d("Excluding non-music") - uniSelector += " AND ${MediaStore.Audio.AudioColumns.IS_MUSIC}=1" - } - - // Set up the projection to follow the music directory configuration. - if (constraints.musicDirs.dirs.isNotEmpty()) { - val pathSelector = - mediaStorePathInterpreterFactory.createSelector(constraints.musicDirs.dirs) - if (pathSelector != null) { - L.d("Must select for directories") - uniSelector += " AND " - if (!constraints.musicDirs.shouldInclude) { - L.d("Excluding directories in selector") - // Without a NOT, the query will be restricted to the specified paths, resulting - // in the "Include" mode. With a NOT, the specified paths will not be included, - // resulting in the "Exclude" mode. - uniSelector += "NOT " - } - uniSelector += " (${pathSelector.template})" - uniArgs = pathSelector.args - } - } - - // Now we can actually query MediaStore. - L.d( - "Starting song query [proj=${projection.toList()}, selector=$uniSelector, args=$uniArgs]") - val cursor = - context.contentResolverSafe.safeQuery( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, - projection, - uniSelector, - uniArgs.toTypedArray()) - L.d("Successfully queried for ${cursor.count} songs") - - val genreNamesMap = mutableMapOf() - - // Since we can't obtain the genre tag from a song query, we must construct our own - // equivalent from genre database queries. Theoretically, this isn't needed since - // MetadataLayer will fill this in for us, but I'd imagine there are some obscure - // formats where genre support is only really covered by this, so we are forced to - // bite the O(n^2) complexity here. - context.contentResolverSafe.useQuery( - MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, - arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)) { genreCursor -> - val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) - val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) - - while (genreCursor.moveToNext()) { - val id = genreCursor.getLong(idIndex) - val name = genreCursor.getStringOrNull(nameIndex) ?: continue - - context.contentResolverSafe.useQuery( - MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id), - arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor -> - val songIdIndex = - cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID) - - while (cursor.moveToNext()) { - // Assume that a song can't inhabit multiple genre entries, as I - // doubt MediaStore is actually aware that songs can have multiple - // genres. - genreNamesMap[cursor.getLong(songIdIndex)] = name - } - } - } - } - - L.d("Read ${genreNamesMap.values.distinct().size} genres from MediaStore") - L.d("Finished initialization in ${System.currentTimeMillis() - start}ms") - return QueryImpl( - cursor, - mediaStorePathInterpreterFactory.wrap(cursor), - tagInterpreterFactory.wrap(cursor), - genreNamesMap) - } - - override suspend fun consume( - query: MediaStoreExtractor.Query, - cache: Cache?, - incompleteSongs: Channel, - completeSongs: Channel - ) { - while (query.moveToNext()) { - val rawSong = RawSong() - if (!query.populateFileInfo(rawSong)) { - continue - } - if (cache?.populate(rawSong) == true) { - completeSongs.sendWithTimeout(rawSong) - } else { - query.populateTags(rawSong) - incompleteSongs.sendWithTimeout(rawSong) - } - yield() - } - // Free the cursor and signal that no more incomplete songs will be produced by - // this extractor. - query.close() - } - - class QueryImpl( - private val cursor: Cursor, - private val mediaStorePathInterpreter: MediaStorePathInterpreter, - private val tagInterpreter: TagInterpreter, - private val genreNamesMap: Map - ) : MediaStoreExtractor.Query { - private val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID) - private val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE) - private val mimeTypeIndex = - cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE) - private val sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE) - private val dateAddedIndex = - cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_ADDED) - private val dateModifiedIndex = - cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_MODIFIED) - private val durationIndex = - cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION) - private val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR) - private val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM) - private val albumIdIndex = - cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID) - private val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST) - private val albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST) - - override val projectedTotal = cursor.count - - override fun moveToNext() = cursor.moveToNext() - - override fun close() = cursor.close() - - override fun populateFileInfo(rawSong: RawSong): Boolean { - rawSong.mediaStoreId = cursor.getLong(idIndex) - rawSong.dateAdded = cursor.getLong(dateAddedIndex) - rawSong.dateModified = cursor.getLong(dateModifiedIndex) - rawSong.extensionMimeType = cursor.getString(mimeTypeIndex) - rawSong.albumMediaStoreId = cursor.getLong(albumIdIndex) - rawSong.path = mediaStorePathInterpreter.extract() ?: return false - return true - } - - override fun populateTags(rawSong: RawSong) { - // Song title - rawSong.name = cursor.getString(titleIndex) - // Size (in bytes) - rawSong.size = cursor.getLong(sizeIndex) - // Duration (in milliseconds) - rawSong.durationMs = cursor.getLong(durationIndex) - // MediaStore only exposes the year value of a file. This is actually worse than it - // seems, as it means that it will not read ID3v2 TDRC tags or Vorbis DATE comments. - // This is one of the major weaknesses of using MediaStore, hence the redundancy layers. - rawSong.date = cursor.getStringOrNull(yearIndex)?.let(Date::from) - // A non-existent album name should theoretically be the name of the folder it contained - // in, but in practice it is more often "0" (as in /storage/emulated/0), even when it - // the file is not actually in the root internal storage directory. We can't do - // anything to fix this, really. We also can't really filter it out, since how can we - // know when it corresponds to the folder and not, say, Low Roar's breakout album "0"? - // Also, on some devices it's literally just null. To maintain behavior sanity just - // replicate the majority behavior described prior. - rawSong.albumName = - cursor.getStringOrNull(albumIndex) - ?: requireNotNull(rawSong.path?.name) { "Invalid raw: No path" } - // Android does not make a non-existent artist tag null, it instead fills it in - // as , which makes absolutely no sense given how other columns default - // to null if they are not present. If this column is such, null it so that - // it's easier to handle later. - val artist = cursor.getString(artistIndex) - if (artist != MediaStore.UNKNOWN_STRING) { - rawSong.artistNames = listOf(artist) - } - // The album artist column is nullable and never has placeholder values. - cursor.getStringOrNull(albumArtistIndex)?.let { rawSong.albumArtistNames = listOf(it) } - // Get the genre value we had to query for in initialization - genreNamesMap[rawSong.mediaStoreId]?.let { rawSong.genreNames = listOf(it) } - // Get version/device-specific tags - tagInterpreter.populate(rawSong) - } - } - - companion object { - /** - * The album artist of a song. This column has existed since at least API 21, but until API - * 30 it was an undocumented extension for Google Play Music. This column will work on all - * versions that Auxio supports. - */ - @Suppress("InlinedApi") - 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. - */ - @Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL - - /** - * The base selector that works across all versions of android. Does not exclude - * directories. - */ - private const val BASE_SELECTOR = "NOT ${MediaStore.Audio.Media.SIZE}=0" - - /** The base projection that works across all versions of android. */ - private val BASE_PROJECTION = - arrayOf( - // These columns are guaranteed to work on all versions of android - MediaStore.Audio.AudioColumns._ID, - MediaStore.Audio.AudioColumns.DATE_ADDED, - MediaStore.Audio.AudioColumns.DATE_MODIFIED, - MediaStore.Audio.AudioColumns.SIZE, - MediaStore.Audio.AudioColumns.DURATION, - MediaStore.Audio.AudioColumns.MIME_TYPE, - MediaStore.Audio.AudioColumns.TITLE, - MediaStore.Audio.AudioColumns.YEAR, - MediaStore.Audio.AudioColumns.ALBUM, - MediaStore.Audio.AudioColumns.ALBUM_ID, - MediaStore.Audio.AudioColumns.ARTIST, - AUDIO_COLUMN_ALBUM_ARTIST) - } -} - -/** - * Wrapper around a [Cursor] that interprets certain tags on a per-API basis. - * - * @author Alexander Capehart (OxygenCobalt) - */ -private sealed interface TagInterpreter { - /** - * Populate the [RawSong] with version-specific tags. - * - * @param rawSong The [RawSong] to populate. - */ - fun populate(rawSong: RawSong) - - interface Factory { - /** The columns that must be added to a query to support this interpreter. */ - val projection: Array - - /** - * Wrap a [Cursor] with this interpreter. This cursor should be the result of a query - * containing the columns specified by [projection]. - * - * @param cursor The [Cursor] to wrap. - * @return A new [TagInterpreter] that will work best on the device's API level. - */ - fun wrap(cursor: Cursor): TagInterpreter - } -} - -private class Api21TagInterpreter(private val cursor: Cursor) : TagInterpreter { - private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) - - override fun populate(rawSong: RawSong) { - // See unpackTrackNo/unpackDiscNo for an explanation - // of how this column is set up. - val rawTrack = cursor.getIntOrNull(trackIndex) - if (rawTrack != null) { - rawTrack.unpackTrackNo()?.let { rawSong.track = it } - rawTrack.unpackDiscNo()?.let { rawSong.disc = it } - } - } - - class Factory : TagInterpreter.Factory { - override val projection: Array - get() = arrayOf(MediaStore.Audio.AudioColumns.TRACK) - - override fun wrap(cursor: Cursor): TagInterpreter = Api21TagInterpreter(cursor) - } - - /** - * 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. - */ - private fun Int.unpackTrackNo() = transformPositionField(mod(1000), null) - - /** - * 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. - */ - private fun Int.unpackDiscNo() = transformPositionField(div(1000), null) -} - -private class Api30TagInterpreter(private val cursor: Cursor) : TagInterpreter { - private val trackIndex = - cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER) - private val discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER) - - override fun populate(rawSong: RawSong) { - // Both CD_TRACK_NUMBER and DISC_NUMBER tend to be formatted as they are in - // the tag itself, which is to say that it is formatted as NN/TT tracks, where - // N is the number and T is the total. Parse the number while ignoring the - // total, as we have no use for it. - cursor.getStringOrNull(trackIndex)?.parseId3v2PositionField()?.let { rawSong.track = it } - cursor.getStringOrNull(discIndex)?.parseId3v2PositionField()?.let { rawSong.disc = it } - } - - class Factory : TagInterpreter.Factory { - override val projection: Array - get() = - arrayOf( - MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER, - MediaStore.Audio.AudioColumns.DISC_NUMBER) - - override fun wrap(cursor: Cursor): TagInterpreter = Api30TagInterpreter(cursor) - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt index 6dec4e22d..bd9f1856d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt @@ -24,7 +24,7 @@ import android.media.MediaFormat import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.fs.MimeType +import org.oxycblt.auxio.music.stack.fs.MimeType import timber.log.Timber as L /** @@ -119,6 +119,7 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. return AudioProperties( bitrate, sampleRate, - MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType)) + MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType) + ) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt index 3d102fb61..8e3b3066f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt @@ -27,7 +27,6 @@ import androidx.media3.common.Timeline import androidx.media3.common.util.Clock import androidx.media3.common.util.HandlerWrapper import androidx.media3.exoplayer.LoadingInfo -import androidx.media3.exoplayer.MetadataRetriever import androidx.media3.exoplayer.analytics.PlayerId import androidx.media3.exoplayer.source.MediaPeriod import androidx.media3.exoplayer.source.MediaSource @@ -40,7 +39,7 @@ import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.yield import org.oxycblt.auxio.music.device.RawSong -import org.oxycblt.auxio.music.fs.toAudioUri +import org.oxycblt.auxio.music.stack.fs.toAudioUri import org.oxycblt.auxio.util.forEachWithTimeout import org.oxycblt.auxio.util.sendWithTimeout import timber.log.Timber as L diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt b/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt index a07df70d9..ce6cb38b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt @@ -27,7 +27,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicSettings -import org.oxycblt.auxio.music.fs.contentResolverSafe +import org.oxycblt.auxio.music.stack.fs.contentResolverSafe import timber.log.Timber as L /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt index 2b90ad811..090d8634c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt @@ -31,9 +31,6 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flow -import org.oxycblt.auxio.music.fs.Components -import org.oxycblt.auxio.music.fs.contentResolverSafe -import org.oxycblt.auxio.music.fs.useQuery interface DeviceFiles { fun explore(uris: Flow): Flow diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/DocumentPathFactory.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DocumentPathFactory.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/fs/DocumentPathFactory.kt rename to app/src/main/java/org/oxycblt/auxio/music/stack/fs/DocumentPathFactory.kt index eb977a1fe..c0afe432f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/DocumentPathFactory.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DocumentPathFactory.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.fs +package org.oxycblt.auxio.music.stack.fs import android.content.ContentUris import android.content.Context diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/Fs.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt rename to app/src/main/java/org/oxycblt/auxio/music/stack/fs/Fs.kt index 47f0a3de4..28f60d251 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/Fs.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.fs +package org.oxycblt.auxio.music.stack.fs import android.content.Context import android.media.MediaFormat @@ -221,7 +221,7 @@ class VolumeManagerImpl @Inject constructor(private val storageManager: StorageM get() = storageVolume.mediaStoreVolumeNameCompat override val components - get() = storageVolume.directoryCompat?.let(Components::parseUnix) + get() = storageVolume.directoryCompat?.let(Components.Companion::parseUnix) override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context) } @@ -234,7 +234,7 @@ class VolumeManagerImpl @Inject constructor(private val storageManager: StorageM get() = storageVolume.mediaStoreVolumeNameCompat override val components - get() = storageVolume.directoryCompat?.let(Components::parseUnix) + get() = storageVolume.directoryCompat?.let(Components.Companion::parseUnix) override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/FsModule.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/FsModule.kt similarity index 86% rename from app/src/main/java/org/oxycblt/auxio/music/fs/FsModule.kt rename to app/src/main/java/org/oxycblt/auxio/music/stack/fs/FsModule.kt index 46a200562..c6cebda61 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/FsModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/FsModule.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.fs +package org.oxycblt.auxio.music.stack.fs import android.content.ContentResolver import android.content.Context @@ -36,12 +36,6 @@ class FsModule { fun volumeManager(@ApplicationContext context: Context): VolumeManager = VolumeManagerImpl(context.getSystemServiceCompat(StorageManager::class)) - @Provides - fun mediaStoreExtractor( - @ApplicationContext context: Context, - mediaStorePathInterpreterFactory: MediaStorePathInterpreter.Factory - ) = MediaStoreExtractor.from(context, mediaStorePathInterpreterFactory) - @Provides fun mediaStorePathInterpreterFactory( volumeManager: VolumeManager diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/MediaStorePathInterpreter.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt rename to app/src/main/java/org/oxycblt/auxio/music/stack/fs/MediaStorePathInterpreter.kt index 50793e8e1..0097b2f39 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/MediaStorePathInterpreter.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.fs +package org.oxycblt.auxio.music.stack.fs import android.database.Cursor import android.os.Build diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/StorageUtil.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt rename to app/src/main/java/org/oxycblt/auxio/music/stack/fs/StorageUtil.kt index eceab8b14..7f28fa260 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/StorageUtil.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.fs +package org.oxycblt.auxio.music.stack.fs import android.annotation.SuppressLint import android.content.ContentResolver diff --git a/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt b/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt index e89c8d241..9e3649022 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt @@ -31,8 +31,8 @@ import org.oxycblt.auxio.music.device.ArtistImpl import org.oxycblt.auxio.music.device.DeviceLibraryImpl import org.oxycblt.auxio.music.device.GenreImpl import org.oxycblt.auxio.music.device.SongImpl -import org.oxycblt.auxio.music.fs.Components -import org.oxycblt.auxio.music.fs.Path +import org.oxycblt.auxio.music.stack.fs.Components +import org.oxycblt.auxio.music.stack.fs.Path class DeviceLibraryTest {