music: split display name fix across versions
When having to fall back to an alternative displayName field, use the version-specific RELATIVE_PATH and DATA fields when necessary.
This commit is contained in:
parent
dd00c70488
commit
fb3c32b14c
6 changed files with 55 additions and 27 deletions
|
@ -2,8 +2,11 @@
|
||||||
|
|
||||||
## dev [v2.3.2, v2.4.0, or v3.0.0]
|
## dev [v2.3.2, v2.4.0, or v3.0.0]
|
||||||
|
|
||||||
|
#### What's New
|
||||||
|
- Folders on external drives can now be excluded on Android Q+ [#134]
|
||||||
|
|
||||||
#### What's Improved
|
#### What's Improved
|
||||||
- Genre parsing now handles multiple integer values and cover/remix indicators
|
- Genre parsing now handles multiple integer values and cover/remix indicators (May wipe playback state)
|
||||||
|
|
||||||
#### Dev/Meta
|
#### Dev/Meta
|
||||||
- New translations [Fjuro -> Czech]
|
- New translations [Fjuro -> Czech]
|
||||||
|
|
|
@ -21,7 +21,10 @@ import androidx.lifecycle.ViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
/** A ViewModel representing the current music indexing state. */
|
/**
|
||||||
|
* A ViewModel representing the current music indexing state.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
class IndexerViewModel : ViewModel(), Indexer.Callback {
|
class IndexerViewModel : ViewModel(), Indexer.Callback {
|
||||||
private val indexer = Indexer.getInstance()
|
private val indexer = Indexer.getInstance()
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.provider.MediaStore
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.database.getIntOrNull
|
import androidx.core.database.getIntOrNull
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
|
import java.io.File
|
||||||
import org.oxycblt.auxio.music.Indexer
|
import org.oxycblt.auxio.music.Indexer
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.albumCoverUri
|
import org.oxycblt.auxio.music.albumCoverUri
|
||||||
|
@ -104,14 +105,13 @@ import org.oxycblt.auxio.util.logW
|
||||||
abstract class MediaStoreBackend : Indexer.Backend {
|
abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
private var idIndex = -1
|
private var idIndex = -1
|
||||||
private var titleIndex = -1
|
private var titleIndex = -1
|
||||||
private var fileIndex = -1
|
private var displayNameIndex = -1
|
||||||
private var durationIndex = -1
|
private var durationIndex = -1
|
||||||
private var yearIndex = -1
|
private var yearIndex = -1
|
||||||
private var albumIndex = -1
|
private var albumIndex = -1
|
||||||
private var albumIdIndex = -1
|
private var albumIdIndex = -1
|
||||||
private var artistIndex = -1
|
private var artistIndex = -1
|
||||||
private var albumArtistIndex = -1
|
private var albumArtistIndex = -1
|
||||||
private var dataIndex = -1
|
|
||||||
|
|
||||||
override fun query(context: Context): Cursor {
|
override fun query(context: Context): Cursor {
|
||||||
val settingsManager = SettingsManager.getInstance()
|
val settingsManager = SettingsManager.getInstance()
|
||||||
|
@ -193,14 +193,14 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
// We need to initialize the cursor indices.
|
// We need to initialize the cursor indices.
|
||||||
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
||||||
titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
|
titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
|
||||||
fileIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
displayNameIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
||||||
durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
||||||
yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
||||||
albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
||||||
albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID)
|
albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID)
|
||||||
artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)
|
artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)
|
||||||
albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
|
albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
|
||||||
dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val audio = Audio()
|
val audio = Audio()
|
||||||
|
@ -212,12 +212,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
// from the android system. Once again though, OEM issues get in our way and
|
// from the android system. Once again though, OEM issues get in our way and
|
||||||
// this field isn't available on some platforms. In that case, see if we can
|
// this field isn't available on some platforms. In that case, see if we can
|
||||||
// grok a file name from the DATA field.
|
// grok a file name from the DATA field.
|
||||||
audio.displayName =
|
audio.displayName = cursor.getStringOrNull(displayNameIndex)
|
||||||
cursor.getStringOrNull(fileIndex)
|
|
||||||
?: cursor
|
|
||||||
.getStringOrNull(dataIndex)
|
|
||||||
?.substringAfterLast('/', MediaStore.UNKNOWN_STRING)
|
|
||||||
?: MediaStore.UNKNOWN_STRING
|
|
||||||
|
|
||||||
audio.duration = cursor.getLong(durationIndex)
|
audio.duration = cursor.getLong(durationIndex)
|
||||||
audio.year = cursor.getIntOrNull(yearIndex)
|
audio.year = cursor.getIntOrNull(yearIndex)
|
||||||
|
@ -317,8 +312,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
MediaStore.Audio.AudioColumns.ALBUM,
|
MediaStore.Audio.AudioColumns.ALBUM,
|
||||||
MediaStore.Audio.AudioColumns.ALBUM_ID,
|
MediaStore.Audio.AudioColumns.ALBUM_ID,
|
||||||
MediaStore.Audio.AudioColumns.ARTIST,
|
MediaStore.Audio.AudioColumns.ARTIST,
|
||||||
AUDIO_COLUMN_ALBUM_ARTIST,
|
AUDIO_COLUMN_ALBUM_ARTIST)
|
||||||
MediaStore.Audio.AudioColumns.DATA)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base selector that works across all versions of android. Does not exclude
|
* The base selector that works across all versions of android. Does not exclude
|
||||||
|
@ -334,9 +328,12 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
*/
|
*/
|
||||||
class Api21MediaStoreBackend : MediaStoreBackend() {
|
class Api21MediaStoreBackend : MediaStoreBackend() {
|
||||||
private var trackIndex = -1
|
private var trackIndex = -1
|
||||||
|
private var dataIndex = -1
|
||||||
|
|
||||||
override val projection: Array<String>
|
override val projection: Array<String>
|
||||||
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
|
get() =
|
||||||
|
super.projection +
|
||||||
|
arrayOf(MediaStore.Audio.AudioColumns.TRACK, MediaStore.Audio.AudioColumns.DATA)
|
||||||
|
|
||||||
override fun buildExcludedSelector(dirs: List<ExcludedDirectory>): Selector {
|
override fun buildExcludedSelector(dirs: List<ExcludedDirectory>): Selector {
|
||||||
val base = Environment.getExternalStorageDirectory().absolutePath
|
val base = Environment.getExternalStorageDirectory().absolutePath
|
||||||
|
@ -363,6 +360,7 @@ class Api21MediaStoreBackend : MediaStoreBackend() {
|
||||||
// Initialize the TRACK index if we have not already.
|
// Initialize the TRACK index if we have not already.
|
||||||
if (trackIndex == -1) {
|
if (trackIndex == -1) {
|
||||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||||
|
dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACK is formatted as DTTT where D is the disc number and T is the track number.
|
// TRACK is formatted as DTTT where D is the disc number and T is the track number.
|
||||||
|
@ -379,6 +377,14 @@ class Api21MediaStoreBackend : MediaStoreBackend() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (audio.displayName == null) {
|
||||||
|
audio.displayName =
|
||||||
|
cursor
|
||||||
|
.getStringOrNull(dataIndex)
|
||||||
|
?.substringAfterLast(File.separatorChar, MediaStore.UNKNOWN_STRING)
|
||||||
|
?: MediaStore.UNKNOWN_STRING
|
||||||
|
}
|
||||||
|
|
||||||
return audio
|
return audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -390,12 +396,10 @@ class Api21MediaStoreBackend : MediaStoreBackend() {
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
open class Api29MediaStoreBackend : MediaStoreBackend() {
|
open class Api29MediaStoreBackend : MediaStoreBackend() {
|
||||||
|
private var relativePathIndex = -1
|
||||||
|
|
||||||
override val projection: Array<String>
|
override val projection: Array<String>
|
||||||
get() =
|
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.RELATIVE_PATH)
|
||||||
super.projection +
|
|
||||||
arrayOf(
|
|
||||||
MediaStore.Audio.AudioColumns.VOLUME_NAME,
|
|
||||||
MediaStore.Audio.AudioColumns.RELATIVE_PATH)
|
|
||||||
|
|
||||||
override fun buildExcludedSelector(dirs: List<ExcludedDirectory>): Selector {
|
override fun buildExcludedSelector(dirs: List<ExcludedDirectory>): Selector {
|
||||||
var selector = BASE_SELECTOR
|
var selector = BASE_SELECTOR
|
||||||
|
@ -422,6 +426,25 @@ open class Api29MediaStoreBackend : MediaStoreBackend() {
|
||||||
|
|
||||||
return Selector(selector, args)
|
return Selector(selector, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun buildAudio(context: Context, cursor: Cursor): Audio {
|
||||||
|
val audio = super.buildAudio(context, cursor)
|
||||||
|
|
||||||
|
if (relativePathIndex != -1) {
|
||||||
|
relativePathIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audio.displayName == null) {
|
||||||
|
audio.displayName =
|
||||||
|
cursor
|
||||||
|
.getStringOrNull(relativePathIndex)
|
||||||
|
?.substringAfterLast(File.separatorChar, MediaStore.UNKNOWN_STRING)
|
||||||
|
?: MediaStore.UNKNOWN_STRING
|
||||||
|
}
|
||||||
|
|
||||||
|
return audio
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.oxycblt.auxio.music.excluded
|
package org.oxycblt.auxio.music.excluded
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import java.io.File
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
data class ExcludedDirectory(val volume: Volume, val relativePath: String) {
|
data class ExcludedDirectory(val volume: Volume, val relativePath: String) {
|
||||||
|
@ -44,10 +45,9 @@ data class ExcludedDirectory(val volume: Volume, val relativePath: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val VOLUME_SEPARATOR = ':'
|
|
||||||
|
|
||||||
fun fromString(dir: String): ExcludedDirectory? {
|
fun fromString(dir: String): ExcludedDirectory? {
|
||||||
val split = dir.split(VOLUME_SEPARATOR, limit = 2)
|
val split = dir.split(File.pathSeparator, limit = 2)
|
||||||
|
|
||||||
val volume = Volume.fromString(split.getOrNull(0) ?: return null)
|
val volume = Volume.fromString(split.getOrNull(0) ?: return null)
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && volume is Volume.Secondary) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && volume is Volume.Secondary) {
|
||||||
logW("Cannot use secondary volumes below API 29")
|
logW("Cannot use secondary volumes below API 29")
|
||||||
|
@ -55,6 +55,7 @@ data class ExcludedDirectory(val volume: Volume, val relativePath: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
val relativePath = split.getOrNull(1) ?: return null
|
val relativePath = split.getOrNull(1) ?: return null
|
||||||
|
|
||||||
return ExcludedDirectory(volume, relativePath)
|
return ExcludedDirectory(volume, relativePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,9 +139,6 @@ class PlaybackService :
|
||||||
// --- PLAYBACKSTATEMANAGER SETUP ---
|
// --- PLAYBACKSTATEMANAGER SETUP ---
|
||||||
|
|
||||||
playbackManager.addCallback(this)
|
playbackManager.addCallback(this)
|
||||||
|
|
||||||
// --- SETTINGSMANAGER SETUP ---
|
|
||||||
|
|
||||||
settingsManager.addCallback(this)
|
settingsManager.addCallback(this)
|
||||||
|
|
||||||
logD("Service created")
|
logD("Service created")
|
||||||
|
|
|
@ -24,6 +24,7 @@ import android.database.sqlite.SQLiteOpenHelper
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import java.io.File
|
||||||
import org.oxycblt.auxio.music.excluded.ExcludedDirectory
|
import org.oxycblt.auxio.music.excluded.ExcludedDirectory
|
||||||
import org.oxycblt.auxio.ui.accent.Accent
|
import org.oxycblt.auxio.ui.accent.Accent
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -90,7 +91,7 @@ fun handleAccentCompat(prefs: SharedPreferences): Accent {
|
||||||
*/
|
*/
|
||||||
fun handleExcludedCompat(context: Context): List<ExcludedDirectory> {
|
fun handleExcludedCompat(context: Context): List<ExcludedDirectory> {
|
||||||
val db = LegacyExcludedDatabase(context)
|
val db = LegacyExcludedDatabase(context)
|
||||||
val primaryPrefix = Environment.getExternalStorageDirectory().absolutePath + '/'
|
val primaryPrefix = Environment.getExternalStorageDirectory().absolutePath + File.separatorChar
|
||||||
return db.readPaths().map { path ->
|
return db.readPaths().map { path ->
|
||||||
val relativePath = path.removePrefix(primaryPrefix)
|
val relativePath = path.removePrefix(primaryPrefix)
|
||||||
ExcludedDirectory(ExcludedDirectory.Volume.Primary, relativePath)
|
ExcludedDirectory(ExcludedDirectory.Volume.Primary, relativePath)
|
||||||
|
|
Loading…
Reference in a new issue