music: refactor backends into extractors
Refactor all Backend instances into a new package called extractor and a new structure called "Layer". Layers are no longer generalized into an interface. Instead, they build on eachother in order to produce a correct output of raw songs. One of these layers is a stub class to eventually implement caching. This changeset also phases out the "Ignore MediaStore tags" setting, as it is no longer needed.
This commit is contained in:
parent
2e71342e1c
commit
28d28287fe
35 changed files with 239 additions and 336 deletions
|
@ -11,8 +11,12 @@
|
|||
audio focus was lost
|
||||
- Fixed issue where the app would crash if a song menu in the genre UI was opened
|
||||
|
||||
#### What's Changed
|
||||
- Ignore MediaStore tags is now on by default
|
||||
|
||||
#### Dev/Meta
|
||||
- Completed migration to reactive playback system
|
||||
- Refactor music backends into a unified chain of extractors
|
||||
|
||||
## 2.6.3
|
||||
|
||||
|
|
|
@ -83,6 +83,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
|||
return item is Header || item is SortHeader
|
||||
}
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
protected val differ = AsyncListDiffer(this, diffCallback)
|
||||
|
||||
override val currentList: List<Item>
|
||||
|
|
|
@ -39,9 +39,6 @@ import org.oxycblt.auxio.util.inflater
|
|||
*/
|
||||
class GenreDetailAdapter(private val listener: Listener) :
|
||||
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
|
||||
private var currentSong: Song? = null
|
||||
private var isPlaying = false
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
is Genre -> GenreDetailViewHolder.VIEW_TYPE
|
||||
|
|
|
@ -57,7 +57,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
AppCompatImageView(context, attrs, defStyleAttr) {
|
||||
private val settings = Settings(context)
|
||||
|
||||
var cornerRadius = 0f
|
||||
private var cornerRadius = 0f
|
||||
set(value) {
|
||||
field = value
|
||||
(background as? MaterialShapeDrawable)?.let { bg ->
|
||||
|
|
|
@ -23,7 +23,6 @@ import android.content.Context
|
|||
import android.os.Parcelable
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
import kotlin.experimental.and
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.reflect.KClass
|
||||
|
@ -143,7 +142,7 @@ sealed class MusicParent : Music() {
|
|||
* A song.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class Song constructor(private val raw: Raw) : Music() {
|
||||
class Song constructor(raw: Raw) : Music() {
|
||||
override val uid: UID
|
||||
|
||||
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
|
||||
|
@ -279,6 +278,7 @@ class Song constructor(private val raw: Raw) : Music() {
|
|||
var formatMimeType: String? = null,
|
||||
var size: Long? = null,
|
||||
var dateAdded: Long? = null,
|
||||
var dateModified: Long? = null,
|
||||
var durationMs: Long? = null,
|
||||
var track: Int? = null,
|
||||
var disc: Int? = null,
|
||||
|
@ -473,7 +473,7 @@ fun MessageDigest.update(date: Date?) {
|
|||
update(date.toString().toByteArray())
|
||||
}
|
||||
|
||||
// Note: All methods regarding integer bytemucking must be little-endian
|
||||
// Note: All methods regarding integer byte-mucking must be little-endian
|
||||
|
||||
/**
|
||||
* Update the digest using the little-endian byte representation of a byte, or do not update if
|
||||
|
|
|
@ -24,7 +24,6 @@ import android.database.Cursor
|
|||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.text.format.DateUtils
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import java.util.UUID
|
||||
|
|
|
@ -18,11 +18,7 @@
|
|||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.storage.StorageManager
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package org.oxycblt.auxio.music.extractor
|
||||
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
/**
|
||||
* TODO: Stub class, not implemented yet
|
||||
*/
|
||||
class CacheLayer {
|
||||
fun init() {
|
||||
// STUB: Add cache database
|
||||
}
|
||||
|
||||
fun finalize(rawSongs: List<Song.Raw>) {
|
||||
// STUB: Add cache database
|
||||
}
|
||||
|
||||
fun maybePopulateCachedRaw(raw: Song.Raw) = false
|
||||
}
|
|
@ -1,21 +1,4 @@
|
|||
/*
|
||||
* 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.system
|
||||
package org.oxycblt.auxio.music.extractor
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
|
@ -26,12 +9,18 @@ import android.provider.MediaStore
|
|||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.database.getIntOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
import java.io.File
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.Date
|
||||
import org.oxycblt.auxio.music.Directory
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.directoryCompat
|
||||
import org.oxycblt.auxio.music.mediaStoreVolumeNameCompat
|
||||
import org.oxycblt.auxio.music.queryCursor
|
||||
import org.oxycblt.auxio.music.storageVolumesCompat
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.contentResolverSafe
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import java.io.File
|
||||
|
||||
/*
|
||||
* This file acts as the base for most the black magic required to get a remotely sensible music
|
||||
|
@ -91,33 +80,41 @@ import org.oxycblt.auxio.util.logD
|
|||
* I wish I was born in the neolithic.
|
||||
*/
|
||||
|
||||
// TODO: Move duration util to MusicUtil
|
||||
|
||||
/**
|
||||
* Represents a [Indexer.Backend] that loads music from the media database ([MediaStore]). This is
|
||||
* not a fully-featured class by itself, and it's API-specific derivatives should be used instead.
|
||||
* The layer that loads music from the MediaStore database. This is an intermediate step in
|
||||
* the music loading process.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend {
|
||||
abstract class MediaStoreLayer(private val context: Context, private val cacheLayer: CacheLayer) {
|
||||
private var cursor: Cursor? = null
|
||||
|
||||
private var idIndex = -1
|
||||
private var titleIndex = -1
|
||||
private var displayNameIndex = -1
|
||||
private var mimeTypeIndex = -1
|
||||
private var sizeIndex = -1
|
||||
private var dateAddedIndex = -1
|
||||
private var dateModifiedIndex = -1
|
||||
private var durationIndex = -1
|
||||
private var yearIndex = -1
|
||||
private var albumIndex = -1
|
||||
private var albumIdIndex = -1
|
||||
private var artistIndex = -1
|
||||
private var albumArtistIndex = -1
|
||||
|
||||
|
||||
private val settings = Settings(context)
|
||||
protected val volumes = mutableListOf<StorageVolume>()
|
||||
|
||||
override fun query(): Cursor {
|
||||
private val _volumes = mutableListOf<StorageVolume>()
|
||||
protected val volumes: List<StorageVolume> get() = _volumes
|
||||
|
||||
/**
|
||||
* Initialize this instance by making a query over the media database.
|
||||
*/
|
||||
open fun init(): Cursor {
|
||||
cacheLayer.init()
|
||||
|
||||
val storageManager = context.getSystemServiceCompat(StorageManager::class)
|
||||
volumes.addAll(storageManager.storageVolumesCompat)
|
||||
_volumes.addAll(storageManager.storageVolumesCompat)
|
||||
val dirs = settings.getMusicDirs(storageManager)
|
||||
|
||||
val args = mutableListOf<String>()
|
||||
|
@ -152,73 +149,70 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend
|
|||
|
||||
logD("Starting query [proj: ${projection.toList()}, selector: $selector, args: $args]")
|
||||
|
||||
return requireNotNull(
|
||||
val cursor = requireNotNull(
|
||||
context.contentResolverSafe.queryCursor(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
selector,
|
||||
args.toTypedArray())) { "Content resolver failure: No Cursor returned" }
|
||||
.also { cursor = it }
|
||||
|
||||
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
||||
titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
|
||||
displayNameIndex =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
||||
mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE)
|
||||
sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE)
|
||||
dateAddedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_ADDED)
|
||||
dateModifiedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_MODIFIED)
|
||||
durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
||||
yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
||||
albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
||||
albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID)
|
||||
artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)
|
||||
albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
|
||||
|
||||
return cursor
|
||||
}
|
||||
|
||||
override fun buildSongs(
|
||||
cursor: Cursor,
|
||||
emitIndexing: (Indexer.Indexing) -> Unit
|
||||
): List<Song> {
|
||||
val rawSongs = mutableListOf<Song.Raw>()
|
||||
while (cursor.moveToNext()) {
|
||||
rawSongs.add(buildRawSong(context, cursor))
|
||||
if (cursor.position % 50 == 0) {
|
||||
// Only check for a cancellation every 50 songs or so (~20ms).
|
||||
// While this seems redundant, each call to emitIndexing checks for a
|
||||
// cancellation of the co-routine this loading task is running on.
|
||||
emitIndexing(Indexer.Indexing.Indeterminate)
|
||||
}
|
||||
/**
|
||||
* Finalize this instance by closing the cursor and finalizing the cache.
|
||||
*/
|
||||
fun finalize(rawSongs: List<Song.Raw>) {
|
||||
cursor?.close()
|
||||
cursor = null
|
||||
|
||||
cacheLayer.finalize(rawSongs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate a [raw] with whatever the next value in the cursor is.
|
||||
*
|
||||
* This returns true if the song could be restored from cache, false if metadata had to be
|
||||
* re-extracted, and null if the cursor is exhausted.
|
||||
*/
|
||||
fun populateRaw(raw: Song.Raw): Boolean? {
|
||||
val cursor = requireNotNull(cursor) { "MediaStoreLayer is not properly initialized" }
|
||||
if (!cursor.moveToNext()) {
|
||||
logD("Cursor is exhausted")
|
||||
return null
|
||||
}
|
||||
|
||||
// The raw song is not actually complete at this point, as we cannot obtain a genre
|
||||
// through a song query. Instead, we have to do the hack where we iterate through
|
||||
// every genre and assign it's name to raw songs that match it's child IDs.
|
||||
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)
|
||||
// Populate the minimum required fields to maybe obtain a cache entry.
|
||||
raw.mediaStoreId = cursor.getLong(idIndex)
|
||||
raw.dateAdded = cursor.getLong(dateAddedIndex)
|
||||
raw.dateModified = cursor.getLong(dateAddedIndex)
|
||||
|
||||
while (genreCursor.moveToNext()) {
|
||||
// Genre names could theoretically be anything, including null for some reason.
|
||||
// Null values are junk and should be ignored, but since we cannot assume the
|
||||
// format a genre was derived from, we have to treat them like they are ID3
|
||||
// genres, even when they might not be.
|
||||
val id = genreCursor.getLong(idIndex)
|
||||
val name = (genreCursor.getStringOrNull(nameIndex) ?: continue)
|
||||
.parseId3GenreNames(settings)
|
||||
|
||||
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()) {
|
||||
val songId = cursor.getLong(songIdIndex)
|
||||
rawSongs
|
||||
.find { it.mediaStoreId == songId }
|
||||
?.let { song -> song.genreNames = name }
|
||||
|
||||
if (cursor.position % 50 == 0) {
|
||||
// Only check for a cancellation every 50 songs or so (~20ms).
|
||||
emitIndexing(Indexer.Indexing.Indeterminate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a cancellation every time we finish a genre too, in the case that
|
||||
// the genre has <50 songs.
|
||||
emitIndexing(Indexer.Indexing.Indeterminate)
|
||||
if (cacheLayer.maybePopulateCachedRaw(raw)) {
|
||||
// We found a valid cache entry, no need to build it.
|
||||
logD("Found cached raw: ${raw.name}")
|
||||
return true
|
||||
}
|
||||
|
||||
return rawSongs.map { Song(it) }
|
||||
buildRaw(cursor, raw)
|
||||
|
||||
// We had to freshly make this raw, return false
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -235,6 +229,7 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend
|
|||
MediaStore.Audio.AudioColumns.MIME_TYPE,
|
||||
MediaStore.Audio.AudioColumns.SIZE,
|
||||
MediaStore.Audio.AudioColumns.DATE_ADDED,
|
||||
MediaStore.Audio.AudioColumns.DATE_MODIFIED,
|
||||
MediaStore.Audio.AudioColumns.DURATION,
|
||||
MediaStore.Audio.AudioColumns.YEAR,
|
||||
MediaStore.Audio.AudioColumns.ALBUM,
|
||||
|
@ -250,32 +245,11 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend
|
|||
* obtain an upstream [Song.Raw] first, and then populate it with version-specific fields
|
||||
* outlined in [projection].
|
||||
*/
|
||||
open fun buildRawSong(context: Context, cursor: Cursor): Song.Raw {
|
||||
// Initialize our cursor indices if we haven't already.
|
||||
if (idIndex == -1) {
|
||||
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
||||
titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
|
||||
displayNameIndex =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
||||
mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE)
|
||||
sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE)
|
||||
dateAddedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_ADDED)
|
||||
durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
||||
yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
||||
albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
||||
albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID)
|
||||
artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)
|
||||
albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
|
||||
}
|
||||
|
||||
val raw = Song.Raw()
|
||||
|
||||
raw.mediaStoreId = cursor.getLong(idIndex)
|
||||
protected open fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
||||
raw.name = cursor.getString(titleIndex)
|
||||
|
||||
raw.extensionMimeType = cursor.getString(mimeTypeIndex)
|
||||
raw.size = cursor.getLong(sizeIndex)
|
||||
raw.dateAdded = cursor.getLong(dateAddedIndex)
|
||||
|
||||
// Try to use the DISPLAY_NAME field to obtain a (probably sane) file name
|
||||
// from the android system.
|
||||
|
@ -294,11 +268,12 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend
|
|||
// Android does not make a non-existent artist tag null, it instead fills it in
|
||||
// as <unknown>, which makes absolutely no sense given how other fields default
|
||||
// to null if they are not present. If this field is <unknown>, null it so that
|
||||
// it's easier to handle later. While we can't natively parse multi-value tags,
|
||||
// from MediaStore itself, we can still parse by user-defined separators.
|
||||
// it's easier to handle later.
|
||||
raw.artistNames =
|
||||
cursor.getString(artistIndex).run {
|
||||
if (this != MediaStore.UNKNOWN_STRING) {
|
||||
// While we can't natively parse multi-value tags,
|
||||
// from MediaStore itself, we can still parse by user-defined separators.
|
||||
maybeParseSeparators(settings)
|
||||
} else {
|
||||
null
|
||||
|
@ -307,8 +282,6 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend
|
|||
|
||||
// The album artist field is nullable and never has placeholder values.
|
||||
raw.albumArtistNames = cursor.getStringOrNull(albumArtistIndex)?.maybeParseSeparators(settings)
|
||||
|
||||
return raw
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -321,12 +294,6 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend
|
|||
@Suppress("InlinedApi")
|
||||
private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST
|
||||
|
||||
/**
|
||||
* External has existed since at least API 21, but no constant existed for it until API 29.
|
||||
* This constant is safe to use.
|
||||
*/
|
||||
@Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
|
||||
|
||||
/**
|
||||
* The base selector that works across all versions of android. Does not exclude
|
||||
* directories.
|
||||
|
@ -336,17 +303,26 @@ abstract class MediaStoreBackend(private val context: Context) : Indexer.Backend
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Note: The separation between version-specific backends may not be the cleanest. To preserve
|
||||
// speed, we only want to add redundancy on known issues, not with possible issues.
|
||||
|
||||
/**
|
||||
* A [MediaStoreBackend] that completes the music loading process in a way compatible from
|
||||
* A [MediaStoreLayer] that completes the music loading process in a way compatible from
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class Api21MediaStoreBackend(context: Context) : MediaStoreBackend(context) {
|
||||
class Api21MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
|
||||
MediaStoreLayer(context, cacheLayer) {
|
||||
private var trackIndex = -1
|
||||
private var dataIndex = -1
|
||||
|
||||
override fun init(): Cursor {
|
||||
val cursor = super.init()
|
||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||
dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
|
||||
return cursor
|
||||
}
|
||||
|
||||
override val projection: Array<String>
|
||||
get() =
|
||||
super.projection +
|
||||
|
@ -361,15 +337,10 @@ class Api21MediaStoreBackend(context: Context) : MediaStoreBackend(context) {
|
|||
return true
|
||||
}
|
||||
|
||||
override fun buildRawSong(context: Context, cursor: Cursor): Song.Raw {
|
||||
val raw = super.buildRawSong(context, cursor)
|
||||
|
||||
// Initialize our indices if we have not already.
|
||||
if (trackIndex == -1) {
|
||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||
dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
|
||||
}
|
||||
|
||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
||||
super.buildRaw(cursor, raw)
|
||||
|
||||
// DATA is equivalent to the absolute path of the file.
|
||||
val data = cursor.getString(dataIndex)
|
||||
|
||||
// On some OEM devices below API 29, DISPLAY_NAME may not be present. I assume
|
||||
|
@ -397,21 +368,29 @@ class Api21MediaStoreBackend(context: Context) : MediaStoreBackend(context) {
|
|||
rawTrack.unpackTrackNo()?.let { raw.track = it }
|
||||
rawTrack.unpackDiscNo()?.let { raw.disc = it }
|
||||
}
|
||||
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [MediaStoreBackend] that selects directories and builds paths using the modern volume fields
|
||||
* A [MediaStoreLayer] that selects directories and builds paths using the modern volume fields
|
||||
* available from API 29 onwards.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
open class BaseApi29MediaStoreBackend(context: Context) : MediaStoreBackend(context) {
|
||||
open class BaseApi29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) : MediaStoreLayer(context, cacheLayer) {
|
||||
private var volumeIndex = -1
|
||||
private var relativePathIndex = -1
|
||||
|
||||
override fun init(): Cursor {
|
||||
val cursor = super.init()
|
||||
|
||||
volumeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME)
|
||||
relativePathIndex =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH)
|
||||
|
||||
return cursor
|
||||
}
|
||||
|
||||
override val projection: Array<String>
|
||||
get() =
|
||||
super.projection +
|
||||
|
@ -431,14 +410,8 @@ open class BaseApi29MediaStoreBackend(context: Context) : MediaStoreBackend(cont
|
|||
return true
|
||||
}
|
||||
|
||||
override fun buildRawSong(context: Context, cursor: Cursor): Song.Raw {
|
||||
val raw = super.buildRawSong(context, cursor)
|
||||
|
||||
if (volumeIndex == -1) {
|
||||
volumeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME)
|
||||
relativePathIndex =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH)
|
||||
}
|
||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
||||
super.buildRaw(cursor, raw)
|
||||
|
||||
val volumeName = cursor.getString(volumeIndex)
|
||||
val relativePath = cursor.getString(relativePathIndex)
|
||||
|
@ -449,30 +422,30 @@ open class BaseApi29MediaStoreBackend(context: Context) : MediaStoreBackend(cont
|
|||
if (volume != null) {
|
||||
raw.directory = Directory.from(volume, relativePath)
|
||||
}
|
||||
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [MediaStoreBackend] that completes the music loading process in a way compatible with at least
|
||||
* A [MediaStoreLayer] that completes the music loading process in a way compatible with at least
|
||||
* API 29.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
open class Api29MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend(context) {
|
||||
open class Api29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) : BaseApi29MediaStoreLayer(context, cacheLayer) {
|
||||
private var trackIndex = -1
|
||||
|
||||
override fun init(): Cursor {
|
||||
val cursor = super.init()
|
||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||
return cursor
|
||||
}
|
||||
|
||||
override val projection: Array<String>
|
||||
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
|
||||
|
||||
override fun buildRawSong(context: Context, cursor: Cursor): Song.Raw {
|
||||
val raw = super.buildRawSong(context, cursor)
|
||||
|
||||
if (trackIndex == -1) {
|
||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||
}
|
||||
|
||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
||||
super.buildRaw(cursor, raw)
|
||||
|
||||
// This backend is volume-aware, but does not support the modern track fields.
|
||||
// Use the old field instead.
|
||||
val rawTrack = cursor.getIntOrNull(trackIndex)
|
||||
|
@ -480,21 +453,26 @@ open class Api29MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend
|
|||
rawTrack.unpackTrackNo()?.let { raw.track = it }
|
||||
rawTrack.unpackDiscNo()?.let { raw.disc = it }
|
||||
}
|
||||
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [MediaStoreBackend] that completes the music loading process in a way compatible with at least
|
||||
* A [MediaStoreLayer] that completes the music loading process in a way compatible with at least
|
||||
* API 30.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
class Api30MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend(context) {
|
||||
class Api30MediaStoreLayer(context: Context, cacheLayer: CacheLayer) : BaseApi29MediaStoreLayer(context, cacheLayer) {
|
||||
private var trackIndex: Int = -1
|
||||
private var discIndex: Int = -1
|
||||
|
||||
override fun init(): Cursor {
|
||||
val cursor = super.init()
|
||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
|
||||
discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
||||
return cursor
|
||||
}
|
||||
|
||||
override val projection: Array<String>
|
||||
get() =
|
||||
super.projection +
|
||||
|
@ -502,14 +480,8 @@ class Api30MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend(cont
|
|||
MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER,
|
||||
MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
||||
|
||||
override fun buildRawSong(context: Context, cursor: Cursor): Song.Raw {
|
||||
val raw = super.buildRawSong(context, cursor)
|
||||
|
||||
// Populate our indices if we have not already.
|
||||
if (trackIndex == -1) {
|
||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
|
||||
discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
||||
}
|
||||
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
||||
super.buildRaw(cursor, raw)
|
||||
|
||||
// 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
|
||||
|
@ -517,7 +489,5 @@ class Api30MediaStoreBackend(context: Context) : BaseApi29MediaStoreBackend(cont
|
|||
// total, as we have no use for it.
|
||||
cursor.getStringOrNull(trackIndex)?.parsePositionNum()?.let { raw.track = it }
|
||||
cursor.getStringOrNull(discIndex)?.parsePositionNum()?.let { raw.disc = it }
|
||||
|
||||
return raw
|
||||
}
|
||||
}
|
|
@ -1,39 +1,22 @@
|
|||
/*
|
||||
* 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.system
|
||||
package org.oxycblt.auxio.music.extractor
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
import com.google.android.exoplayer2.MetadataRetriever
|
||||
import com.google.android.exoplayer2.metadata.Metadata
|
||||
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
|
||||
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
|
||||
import org.oxycblt.auxio.music.Date
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.audioUri
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import com.google.android.exoplayer2.metadata.Metadata
|
||||
import org.oxycblt.auxio.music.Date
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
|
||||
/**
|
||||
* A [Indexer.Backend] that leverages ExoPlayer's metadata retrieval system to index metadata.
|
||||
* The layer that leverages ExoPlayer's metadata retrieval system to index metadata.
|
||||
*
|
||||
* Normally, leveraging ExoPlayer's metadata system would be a terrible idea, as it is horrifically
|
||||
* slow. However, if we parallelize it, we can get similar throughput to other metadata extractors,
|
||||
|
@ -45,29 +28,28 @@ import org.oxycblt.auxio.util.logW
|
|||
*
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ExoPlayerBackend(private val context: Context, private val inner: MediaStoreBackend) : Indexer.Backend {
|
||||
class MetadataLayer(private val context: Context, private val mediaStoreLayer: MediaStoreLayer) {
|
||||
private val settings = Settings(context)
|
||||
private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
|
||||
|
||||
// No need to implement our own query logic, as this backend is still reliant on
|
||||
// MediaStore.
|
||||
override fun query() = inner.query()
|
||||
/**
|
||||
* Initialize the sub-layers that this layer relies on.
|
||||
*/
|
||||
fun init() = mediaStoreLayer.init().count
|
||||
|
||||
override fun buildSongs(
|
||||
cursor: Cursor,
|
||||
emitIndexing: (Indexer.Indexing) -> Unit
|
||||
): List<Song> {
|
||||
// Metadata retrieval with ExoPlayer is asynchronous, so a callback may at any point
|
||||
// add a completed song to the list. To prevent a crash in that case, we use the
|
||||
// concurrent counterpart to a typical mutable list.
|
||||
val songs = mutableListOf<Song>()
|
||||
val total = cursor.count
|
||||
/**
|
||||
* Finalize the sub-layers that this layer relies on.
|
||||
*/
|
||||
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreLayer.finalize(rawSongs)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
// Note: This call to buildRawSong does not populate the genre field. This is
|
||||
// because indexing genres is quite slow with MediaStore, and so keeping the
|
||||
// field blank on unsupported ExoPlayer formats ends up being preferable.
|
||||
val raw = inner.buildRawSong(context, cursor)
|
||||
fun parse(emit: (Song.Raw) -> Unit) {
|
||||
while (true) {
|
||||
val raw = Song.Raw()
|
||||
if (mediaStoreLayer.populateRaw(raw) ?: break) {
|
||||
// No need to extract metadata that was successfully restored from the cache
|
||||
emit(raw)
|
||||
continue
|
||||
}
|
||||
|
||||
// Spin until there is an open slot we can insert a task in. Note that we do
|
||||
// not add callbacks to our new tasks, as Future callbacks run on a different
|
||||
|
@ -78,10 +60,9 @@ class ExoPlayerBackend(private val context: Context, private val inner: MediaSto
|
|||
val task = taskPool[i]
|
||||
|
||||
if (task != null) {
|
||||
val song = task.get()
|
||||
if (song != null) {
|
||||
songs.add(song)
|
||||
emitIndexing(Indexer.Indexing.Songs(songs.size, total))
|
||||
val finishedRaw = task.get()
|
||||
if (finishedRaw != null) {
|
||||
emit(finishedRaw)
|
||||
taskPool[i] = Task(context, settings, raw)
|
||||
break@spin
|
||||
}
|
||||
|
@ -99,19 +80,17 @@ class ExoPlayerBackend(private val context: Context, private val inner: MediaSto
|
|||
val task = taskPool[i]
|
||||
|
||||
if (task != null) {
|
||||
val song = task.get() ?: continue@spin
|
||||
songs.add(song)
|
||||
emitIndexing(Indexer.Indexing.Songs(songs.size, total))
|
||||
val finishedRaw = task.get() ?: continue@spin
|
||||
emit(finishedRaw)
|
||||
taskPool[i] = null
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return songs
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
/** The amount of tasks this backend can run efficiently at once. */
|
||||
private const val TASK_CAPACITY = 8
|
||||
|
@ -132,7 +111,7 @@ class Task(context: Context, private val settings: Settings, private val raw: So
|
|||
* Get the song that this task is trying to complete. If the task is still busy, this will
|
||||
* return null. Otherwise, it will return a song.
|
||||
*/
|
||||
fun get(): Song? {
|
||||
fun get(): Song.Raw? {
|
||||
if (!future.isDone) {
|
||||
return null
|
||||
}
|
||||
|
@ -148,7 +127,7 @@ class Task(context: Context, private val settings: Settings, private val raw: So
|
|||
|
||||
if (format == null) {
|
||||
logD("Nothing could be extracted for ${raw.name}")
|
||||
return Song(raw)
|
||||
return raw
|
||||
}
|
||||
|
||||
// Populate the format mime type if we have one.
|
||||
|
@ -161,7 +140,7 @@ class Task(context: Context, private val settings: Settings, private val raw: So
|
|||
logD("No metadata could be extracted for ${raw.name}")
|
||||
}
|
||||
|
||||
return Song(raw)
|
||||
return raw
|
||||
}
|
||||
|
||||
private fun completeRawSong(metadata: Metadata) {
|
|
@ -1,4 +1,4 @@
|
|||
package org.oxycblt.auxio.music.system
|
||||
package org.oxycblt.auxio.music.extractor
|
||||
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import org.oxycblt.auxio.music.Date
|
|
@ -20,7 +20,6 @@ package org.oxycblt.auxio.music.system
|
|||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.database.Cursor
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
@ -28,13 +27,16 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.music.extractor.Api21MediaStoreLayer
|
||||
import org.oxycblt.auxio.music.extractor.Api29MediaStoreLayer
|
||||
import org.oxycblt.auxio.music.extractor.Api30MediaStoreLayer
|
||||
import org.oxycblt.auxio.music.extractor.CacheLayer
|
||||
import org.oxycblt.auxio.music.extractor.MetadataLayer
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.TaskGuard
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import org.oxycblt.auxio.util.requireBackgroundThread
|
||||
|
||||
/**
|
||||
* Auxio's media indexer.
|
||||
|
@ -43,13 +45,13 @@ import org.oxycblt.auxio.util.requireBackgroundThread
|
|||
* (and hacky garbage) in order to produce the best possible experience. It is split into three
|
||||
* distinct steps:
|
||||
*
|
||||
* 1. Finding a [Backend] to use and then querying the media database with it.
|
||||
* 2. Using the [Backend] and the media data to create songs
|
||||
* 1. Creating the chain of layers to extract metadata with
|
||||
* 2. Running the chain process
|
||||
* 3. Using the songs to build the library, which primarily involves linking up all data objects
|
||||
* with their corresponding parents/children.
|
||||
*
|
||||
* This class in particular handles 3 primarily. For the code that handles 1 and 2, see the
|
||||
* [Backend] implementations.
|
||||
* layer implementations.
|
||||
*
|
||||
* This class also fulfills the role of maintaining the current music loading state, which seems
|
||||
* like a job for [MusicStore] but in practice is only really leveraged by the components that
|
||||
|
@ -194,27 +196,23 @@ class Indexer {
|
|||
private fun indexImpl(context: Context, handle: Long): MusicStore.Library? {
|
||||
emitIndexing(Indexing.Indeterminate, handle)
|
||||
|
||||
// Since we have different needs for each version, we determine a "Backend" to use
|
||||
// when loading music and then leverage that to create the initial song list.
|
||||
// This is technically dependency injection. Except it doesn't increase your compile
|
||||
// times by 3x. Isn't that nice.
|
||||
// Create the chain of layers. Each layer builds on the previous layer and
|
||||
// enables version-specific features in order to create the best possible music
|
||||
// experience. This is technically dependency injection. Except it doesn't increase
|
||||
// your compile times by 3x. Isn't that nice.
|
||||
|
||||
val mediaStoreBackend =
|
||||
val cacheLayer = CacheLayer()
|
||||
|
||||
val mediaStoreLayer =
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreBackend(context)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreBackend(context)
|
||||
else -> Api21MediaStoreBackend(context)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreLayer(context, cacheLayer)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreLayer(context, cacheLayer)
|
||||
else -> Api21MediaStoreLayer(context, cacheLayer)
|
||||
}
|
||||
|
||||
val settings = Settings(context)
|
||||
val backend =
|
||||
if (settings.useQualityTags) {
|
||||
ExoPlayerBackend(context, mediaStoreBackend)
|
||||
} else {
|
||||
mediaStoreBackend
|
||||
}
|
||||
val metadataLayer = MetadataLayer(context, mediaStoreLayer)
|
||||
|
||||
val songs = buildSongs(backend, handle)
|
||||
val songs = buildSongs(metadataLayer, handle)
|
||||
if (songs.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
@ -236,32 +234,37 @@ class Indexer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Does the initial query over the song database using [backend]. The songs returned by this
|
||||
* Does the initial query over the song database using [metadataLayer]. The songs returned by this
|
||||
* function are **not** well-formed. The companion [buildAlbums], [buildArtists], and
|
||||
* [buildGenres] functions must be called with the returned list so that all songs are properly
|
||||
* linked up.
|
||||
*/
|
||||
private fun buildSongs(backend: Backend, handle: Long): List<Song> {
|
||||
private fun buildSongs(metadataLayer: MetadataLayer, handle: Long): List<Song> {
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
var songs =
|
||||
backend.query().use { cursor ->
|
||||
logD(
|
||||
"Successfully queried media database " +
|
||||
"in ${System.currentTimeMillis() - start}ms")
|
||||
// Initialize the extractor chain. This also nets us the projected total
|
||||
// that we can show when loading.
|
||||
val total = metadataLayer.init()
|
||||
|
||||
backend.buildSongs(cursor) { emitIndexing(it, handle) }
|
||||
}
|
||||
// Note: We use a set here so we can eliminate effective duplicates of
|
||||
// songs (by UID).
|
||||
val songs = mutableSetOf<Song>()
|
||||
val rawSongs = mutableListOf<Song.Raw>()
|
||||
|
||||
// Deduplicate songs to prevent (most) deformed music clones
|
||||
songs = songs.distinctBy { it.uid }.toMutableList()
|
||||
metadataLayer.parse { raw ->
|
||||
songs.add(Song(raw))
|
||||
rawSongs.add(raw)
|
||||
emitIndexing(Indexing.Songs(songs.size, total), handle)
|
||||
}
|
||||
|
||||
// Ensure that sorting order is consistent so that grouping is also consistent.
|
||||
Sort(Sort.Mode.ByName, true).songsInPlace(songs)
|
||||
metadataLayer.finalize(rawSongs)
|
||||
|
||||
val sorted = Sort(Sort.Mode.ByName, true).songs(songs)
|
||||
|
||||
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
|
||||
|
||||
return songs
|
||||
// Ensure that sorting order is consistent so that grouping is also consistent.
|
||||
return sorted
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -420,18 +423,6 @@ class Indexer {
|
|||
fun onStartIndexing()
|
||||
}
|
||||
|
||||
/** Represents a backend that metadata can be extracted from. */
|
||||
interface Backend {
|
||||
/** Query the media database for a basic cursor. */
|
||||
fun query(): Cursor
|
||||
|
||||
/** Create a list of songs from the [Cursor] queried in [query]. */
|
||||
fun buildSongs(
|
||||
cursor: Cursor,
|
||||
emitIndexing: (Indexing) -> Unit
|
||||
): List<Song>
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile private var INSTANCE: Indexer? = null
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ class IndexingNotification(private val context: Context) :
|
|||
}
|
||||
is Indexer.Indexing.Songs -> {
|
||||
// Only update the notification every 50 songs to prevent excessive updates.
|
||||
// TODO: Use a timeout instead to handle rapid-fire updates w/o rate limiting
|
||||
if (indexing.current % 50 == 0) {
|
||||
logD("Updating state to $indexing")
|
||||
setContentText(
|
||||
|
|
|
@ -218,8 +218,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
override fun onSettingChanged(key: String) {
|
||||
when (key) {
|
||||
getString(R.string.set_key_music_dirs),
|
||||
getString(R.string.set_key_music_dirs_include),
|
||||
getString(R.string.set_key_quality_tags) -> onStartIndexing()
|
||||
getString(R.string.set_key_music_dirs_include)-> onStartIndexing()
|
||||
getString(R.string.set_key_observing) -> {
|
||||
if (!indexer.isIndexing) {
|
||||
updateIdleSession()
|
||||
|
|
|
@ -20,7 +20,6 @@ package org.oxycblt.auxio.playback
|
|||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import kotlin.math.max
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
|
||||
|
|
|
@ -479,7 +479,6 @@ class PlaybackService :
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val POS_POLL_INTERVAL = 100L
|
||||
private const val REWIND_THRESHOLD = 3000L
|
||||
|
||||
const val ACTION_INC_REPEAT_MODE = BuildConfig.APPLICATION_ID + ".action.LOOP"
|
||||
|
|
|
@ -189,10 +189,6 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
|||
val pauseOnRepeat: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false)
|
||||
|
||||
/** Whether to parse metadata directly with ExoPlayer. */
|
||||
val useQualityTags: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_quality_tags), false)
|
||||
|
||||
/** Whether to be actively watching for changes in the music library. */
|
||||
val shouldBeObserving: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_observing), false)
|
||||
|
|
|
@ -45,7 +45,7 @@ interface MenuItemListener : ItemClickListener {
|
|||
}
|
||||
|
||||
/**
|
||||
* Like [AsyncListDiffer], but synchronous. This may seem like it would be inefficient, but in
|
||||
* Like AsyncListDiffer, but synchronous. This may seem like it would be inefficient, but in
|
||||
* practice Auxio's lists tend to be small enough to the point where this does not matter, and
|
||||
* situations that would be inefficient are ruled out with [replaceList].
|
||||
*/
|
||||
|
|
|
@ -18,13 +18,11 @@
|
|||
package org.oxycblt.auxio.util
|
||||
|
||||
import android.os.Looper
|
||||
import android.text.format.DateUtils
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.util.concurrent.CancellationException
|
||||
import kotlin.reflect.KClass
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import java.util.*
|
||||
|
||||
/** Assert that we are on a background thread. */
|
||||
fun requireBackgroundThread() {
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.24" android:color="?attr/colorOnPrimary" />
|
||||
</selector>
|
|
@ -203,8 +203,6 @@
|
|||
<string name="lbl_bitrate">Přenosová rychlost</string>
|
||||
<string name="lbl_sample_rate">Vzorkovací frekvence</string>
|
||||
<string name="set_dirs">Hudební složky</string>
|
||||
<string name="set_quality_tags">Ignorovat štítky MediaStore</string>
|
||||
<string name="set_quality_tags_desc">Zvýší kvalitu štítků, ale má za následek delší načítací čas (experimentální)</string>
|
||||
<string name="set_restore_state">Obnovit stav přehrávání</string>
|
||||
<string name="lbl_state_restored">Stav obnoven</string>
|
||||
<string name="set_restore_desc">Obnovit dříve uložený stav přehrávání (pokud existuje)</string>
|
||||
|
|
|
@ -196,8 +196,6 @@
|
|||
<string name="lbl_format">Format</string>
|
||||
<string name="lbl_size">Größe</string>
|
||||
<string name="lbl_bitrate">Bitrate</string>
|
||||
<string name="set_quality_tags">MediaStore-Tags ignorieren</string>
|
||||
<string name="set_quality_tags_desc">Erhöht Tag-Qualität, benötigt aber längere Ladezeiten (Experimentell)</string>
|
||||
<string name="lbl_observing">Überwachen der Musikbibliothek</string>
|
||||
<string name="lbl_indexing">Musik wird geladen</string>
|
||||
<string name="lng_observing">Musikbibliothek wird auf Änderungen überwacht…</string>
|
||||
|
|
|
@ -188,10 +188,8 @@
|
|||
<string name="set_restore_state">Reestablecer el estado de reproducción</string>
|
||||
<string name="set_restore_desc">Reestablecer el estado de reproducción guardado previamente (si existe)</string>
|
||||
<string name="set_dirs">Carpetas de música</string>
|
||||
<string name="set_quality_tags">Ignorar etiquetas MediaStore</string>
|
||||
<string name="set_dirs_desc">Gestionar de dónde se cargará la música</string>
|
||||
<string name="set_dirs_mode_include_desc">La música<b>solo</b> se cargará de las carpetas que añadas.</string>
|
||||
<string name="set_quality_tags_desc">Incrementa la calidad de las etiquetas, pero resulra en tiempos de carga mayores (Experimental)</string>
|
||||
<string name="def_codec">Formato desconocido</string>
|
||||
<string name="clr_dynamic">Dinámico</string>
|
||||
<string name="fmt_disc_no">Disco %d</string>
|
||||
|
|
|
@ -103,8 +103,6 @@
|
|||
<string name="set_dirs_mode_exclude_desc">Glazba se <b>neće</b> učitati iz dodanih mapa.</string>
|
||||
<string name="set_dirs_mode_include">Uključi</string>
|
||||
<string name="set_dirs_mode_include_desc">Glazba će se učitati <b>samo</b> iz dodanih mapa.</string>
|
||||
<string name="set_quality_tags">Zanemari MediaStore oznake</string>
|
||||
<string name="set_quality_tags_desc">Poboljšava kvalitetu oznaka, no može produljiti vrijeme učitavanja (Eksperimentalno)</string>
|
||||
<string name="set_observing">Automatsko ponovno učitavanje</string>
|
||||
<string name="set_observing_desc">Ponovo učitaj svoju zbirku glazbe čim se dogode promjene (eksperimentalno)</string>
|
||||
<string name="err_no_music">Nijedan zvučni zapis nije pronađen</string>
|
||||
|
|
|
@ -204,8 +204,6 @@
|
|||
<string name="lbl_state_restored">Stato ripristinato</string>
|
||||
<string name="lbl_sort_date_added">Data aggiunta</string>
|
||||
<string name="set_observing">Ricaricamento automatico</string>
|
||||
<string name="set_quality_tags">Ignora tags MediaStore</string>
|
||||
<string name="set_quality_tags_desc">Migliora qualità dei tag, ma potrebbe richiedere tempi di carimento più lunghi (sperimentale)</string>
|
||||
<string name="set_restore_state">Ripristina stato riproduzione</string>
|
||||
<string name="set_restore_desc">Ripristina lo stato di riproduzione precedentemente salvato (se disponibile)</string>
|
||||
<string name="err_did_not_restore">Impossibile ripristinare lo stato</string>
|
||||
|
|
|
@ -168,8 +168,6 @@
|
|||
<string name="lng_search_library">Ieškokite savo bibliotekoje…</string>
|
||||
<string name="lbl_equalizer">Ekvalaizeris</string>
|
||||
<string name="set_dirs_mode">Režimas</string>
|
||||
<string name="set_quality_tags">Ignoruoti „MediaStore“ žymas</string>
|
||||
<string name="set_quality_tags_desc">Padidėja žymų kokybė, tačiau dėl to pailgėja krovimo laikas (Eksperimentinis)</string>
|
||||
<string name="set_observing">Automatinis krovimas</string>
|
||||
<string name="err_no_music">Muzikos nerasta</string>
|
||||
</resources>
|
|
@ -170,8 +170,6 @@
|
|||
<string name="set_playback_mode_none">Afspelen vanaf getoond item</string>
|
||||
<string name="set_restore_state">Afspeelstatus herstellen</string>
|
||||
<string name="set_restore_desc">Herstel de eerder opgeslagen afspeelstatus (indien aanwezig)</string>
|
||||
<string name="set_quality_tags">MediaStore tags negeren</string>
|
||||
<string name="set_quality_tags_desc">Verhoogt tag-kwaliteit, maar vereist langere laadtijden</string>
|
||||
<string name="err_did_not_restore">Geen staat kan hersteld worden</string>
|
||||
<string name="desc_clear_queue_item">Verwijder dit wachtrij liedje</string>
|
||||
<string name="desc_queue_handle">Verplaats dit wachtrij liedje</string>
|
||||
|
|
|
@ -193,9 +193,7 @@
|
|||
<string name="set_lib_tabs">Abas da biblioteca</string>
|
||||
<string name="lbl_genre">Gênero</string>
|
||||
<string name="set_playback_mode_artist">Reproduzir do artista</string>
|
||||
<string name="set_quality_tags_desc">Aumenta a qualidade da tag, mas resulta em tempos de carregamento mais longos (Experimental)</string>
|
||||
<string name="set_restore_desc">Restaurar o estado de reprodução salvo anteriormente (se houver)</string>
|
||||
<string name="set_quality_tags">Ignorar tags do MediaStore</string>
|
||||
<string name="set_pre_amp_with">Ajuste com tags</string>
|
||||
<string name="lbl_state_wiped">Estado liberado</string>
|
||||
<string name="lbl_state_restored">Estado restaurado</string>
|
||||
|
|
|
@ -215,10 +215,8 @@
|
|||
<string name="set_observing_desc">Перезагружать библиотеку при каждом изменении (экспериментально)</string>
|
||||
<string name="fmt_db_neg">-%.1f дБ</string>
|
||||
<string name="fmt_lib_genre_count">Жанров загружено: %d</string>
|
||||
<string name="set_quality_tags">Игнорировать теги MediaStore</string>
|
||||
<string name="set_restore_desc">Восстановить предыдущее состояние воспроизведения (если есть)</string>
|
||||
<string name="set_dirs_mode">Режим</string>
|
||||
<string name="set_quality_tags_desc">Улучшает качество тегов, но приводит к долгому времени загрузки (экспериментально)</string>
|
||||
<string name="set_dirs_mode_include_desc">Музыка будет загружена <b>только</b> из указанных папок.</string>
|
||||
<string name="err_did_not_restore">Невозможно восстановить состояние воспроизведения</string>
|
||||
<string name="def_track">Нет номера трека</string>
|
||||
|
|
|
@ -193,8 +193,6 @@
|
|||
<string name="set_restore_desc">Önceden kaydedilmiş oynatma durumunu geri getirir (varsa)</string>
|
||||
<string name="set_round_mode">Yuvarlak mod</string>
|
||||
<string name="set_restore_state">Oynatma durumunu eski haline getir</string>
|
||||
<string name="set_quality_tags">MediaStore etiketlerini yoksay</string>
|
||||
<string name="set_quality_tags_desc">Etiket kalitesini artırır, ancak daha uzun yükleme süreleri gerektirir</string>
|
||||
<string name="err_did_not_restore">Hiçbir durum geri getirelemedi</string>
|
||||
<string name="set_repeat_pause">Tekrarda duraklat</string>
|
||||
<string name="lbl_indexing">Müzik yükleniyor</string>
|
||||
|
|
|
@ -200,7 +200,6 @@
|
|||
<string name="err_did_not_restore">没有可以恢复的状态</string>
|
||||
<string name="set_restore_state">恢复播放状态</string>
|
||||
<string name="set_restore_desc">恢复此前保存的播放状态(如果有)</string>
|
||||
<string name="set_quality_tags_desc">提高标签质量,但会导致加载时间变长(实验性)</string>
|
||||
<string name="set_observing">自动重载中</string>
|
||||
<string name="set_observing_desc">发生更改时自动重新加载您的曲库(实验性)</string>
|
||||
<string name="desc_queue_bar">打开队列</string>
|
||||
|
@ -209,7 +208,6 @@
|
|||
<string name="lng_observing">正在监测您的曲库以查找更改…</string>
|
||||
<string name="lbl_state_wiped">已清除状态</string>
|
||||
<string name="lbl_state_restored">已恢复状态</string>
|
||||
<string name="set_quality_tags">忽略 MediaStore 标签</string>
|
||||
<string name="lbl_eps">EP 专辑</string>
|
||||
<string name="lbl_ep">EP 专辑</string>
|
||||
<string name="lbl_singles">单曲</string>
|
||||
|
|
|
@ -34,7 +34,6 @@
|
|||
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
|
||||
<string name="set_key_separators" translatable="false">auxio_separators</string>
|
||||
<string name="set_key_observing" translatable="false">auxio_observing</string>
|
||||
<string name="set_key_quality_tags" translatable="false">auxio_quality_tags</string>
|
||||
|
||||
<string name="set_key_search_filter" translatable="false">KEY_SEARCH_FILTER</string>
|
||||
|
||||
|
|
|
@ -224,8 +224,6 @@
|
|||
<!-- Restrict music loading to selected folders -->
|
||||
<string name="set_dirs_mode_include">Include</string>
|
||||
<string name="set_dirs_mode_include_desc">Music will <b>only</b> be loaded from the folders you add.</string>
|
||||
<string name="set_quality_tags">Ignore MediaStore tags</string>
|
||||
<string name="set_quality_tags_desc">Increases tag quality, but results in longer loading times (Experimental)</string>
|
||||
<string name="set_observing">Automatic reloading</string>
|
||||
<string name="set_observing_desc">Reload your music library whenever it changes (Experimental)</string>
|
||||
|
||||
|
|
|
@ -70,15 +70,6 @@
|
|||
<item name="useLargeIcon">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Auxio.ItemLayout" parent="">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:background">?attr/selectableItemBackground</item>
|
||||
<item name="android:padding">@dimen/spacing_medium</item>
|
||||
<item name="android:clickable">true</item>
|
||||
<item name="android:focusable">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Auxio.RecyclerView.Linear" parent="">
|
||||
<item name="layoutManager">androidx.recyclerview.widget.LinearLayoutManager</item>
|
||||
</style>
|
||||
|
|
|
@ -124,10 +124,6 @@
|
|||
app:summary="@string/set_repeat_pause_desc"
|
||||
app:title="@string/set_repeat_pause" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/set_content">
|
||||
|
||||
<Preference
|
||||
app:key="@string/set_key_save_state"
|
||||
app:summary="@string/set_save_desc"
|
||||
|
@ -143,6 +139,10 @@
|
|||
app:summary="@string/set_restore_desc"
|
||||
app:title="@string/set_restore_state" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/set_content">
|
||||
|
||||
<Preference
|
||||
app:key="@string/set_key_reindex"
|
||||
app:summary="@string/set_reindex_desc"
|
||||
|
@ -153,12 +153,6 @@
|
|||
app:summary="@string/set_dirs_desc"
|
||||
app:title="@string/set_dirs" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:defaultValue="false"
|
||||
app:key="@string/set_key_quality_tags"
|
||||
app:summary="@string/set_quality_tags_desc"
|
||||
app:title="@string/set_quality_tags" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:defaultValue="false"
|
||||
app:key="@string/set_key_observing"
|
||||
|
|
Loading…
Reference in a new issue