music: add automatic rescanning

Add automatic rescanning, for real.

This is the culmination of 6 months of work to make Auxio respond to
music updates and to research the most versatile implementation of
such. Is it the best system? No. It's actually a bit messy by necessity
in order to prevent bugs. Does it work well? Yes.

This will not be enabled by default, as it does require a battery
draining foreground service and is generally not useful for most
circumstances.

So glad to be done with this.

Resolves #72.
This commit is contained in:
OxygenCobalt 2022-07-08 16:35:45 -06:00
parent 94f2d28936
commit 3a7768ad22
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
28 changed files with 224 additions and 88 deletions

View file

@ -3,8 +3,10 @@
## dev ## dev
#### What's New #### What's New
- Added direct metadata parsing, allowing more correct metadata at the cost of longer loading times - Massively overhauled how music is loaded [#72]:
- Auxio can now reload music without requiring a restart - Auxio can now reload music without requiring a restart
- Added a new option to reload music when device files change
- Added direct metadata parsing, allowing more correct metadata at the cost of longer loading times
- Added a shuffle shortcut - Added a shuffle shortcut
- Widgets now have a more sleek and consistent button layout - Widgets now have a more sleek and consistent button layout
- "Rounded album covers" is now "Round mode" - "Rounded album covers" is now "Round mode"

View file

@ -33,9 +33,9 @@ import org.oxycblt.auxio.detail.recycler.SortHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MimeType
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.system.MimeType
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.Header

View file

@ -46,10 +46,10 @@ import org.oxycblt.auxio.home.list.SongListFragment
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.IndexerViewModel
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.system.Indexer import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.music.system.IndexerViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
@ -313,7 +313,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
when (indexing) { when (indexing) {
is Indexer.Indexing.Indeterminate -> { is Indexer.Indexing.Indeterminate -> {
binding.homeIndexingStatus.textSafe = getString(R.string.lbl_indexing) binding.homeIndexingStatus.textSafe = getString(R.string.lbl_indexing_desc)
binding.homeIndexingProgress.isIndeterminate = true binding.homeIndexingProgress.isIndeterminate = true
} }
is Indexer.Indexing.Songs -> { is Indexer.Indexing.Songs -> {

View file

@ -15,15 +15,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.system package org.oxycblt.auxio.music
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.system.Indexer
/** /**
* A ViewModel representing the current music indexing state. * A ViewModel representing the current music indexing state.
* @author OxygenCobalt * @author OxygenCobalt
*
* TODO: Indeterminate state for Home + Settings
*/ */
class IndexerViewModel : ViewModel(), Indexer.Callback { class IndexerViewModel : ViewModel(), Indexer.Callback {
private val indexer = Indexer.getInstance() private val indexer = Indexer.getInstance()

View file

@ -23,8 +23,6 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.system.MimeType
import org.oxycblt.auxio.music.system.Path
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.system package org.oxycblt.auxio.music
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
@ -109,7 +109,7 @@ val StorageVolume.directoryCompat: String?
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
directory?.absolutePath directory?.absolutePath
} else { } else {
// Replicate getDirectory: getPath if mounted, null if not // Replicate API: getPath if mounted, null if not
when (stateCompat) { when (stateCompat) {
Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED,
Environment.MEDIA_MOUNTED_READ_ONLY -> Environment.MEDIA_MOUNTED_READ_ONLY ->

View file

@ -19,7 +19,7 @@ package org.oxycblt.auxio.music.dirs
import android.content.Context import android.content.Context
import org.oxycblt.auxio.databinding.ItemMusicDirBinding import org.oxycblt.auxio.databinding.ItemMusicDirBinding
import org.oxycblt.auxio.music.system.Directory import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.ui.recycler.BackingData import org.oxycblt.auxio.ui.recycler.BackingData
import org.oxycblt.auxio.ui.recycler.BindingViewHolder import org.oxycblt.auxio.ui.recycler.BindingViewHolder
import org.oxycblt.auxio.ui.recycler.MonoAdapter import org.oxycblt.auxio.ui.recycler.MonoAdapter

View file

@ -17,7 +17,7 @@
package org.oxycblt.auxio.music.dirs package org.oxycblt.auxio.music.dirs
import org.oxycblt.auxio.music.system.Directory import org.oxycblt.auxio.music.Directory
/** Represents a the configuration for the "Folder Management" setting */ /** Represents a the configuration for the "Folder Management" setting */
data class MusicDirs(val dirs: List<Directory>, val shouldInclude: Boolean) data class MusicDirs(val dirs: List<Directory>, val shouldInclude: Boolean)

View file

@ -28,7 +28,7 @@ import androidx.core.view.isVisible
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
import org.oxycblt.auxio.music.system.Directory import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.backend package org.oxycblt.auxio.music.system
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
@ -29,7 +29,6 @@ import org.oxycblt.auxio.music.audioUri
import org.oxycblt.auxio.music.id3GenreName import org.oxycblt.auxio.music.id3GenreName
import org.oxycblt.auxio.music.iso8601year import org.oxycblt.auxio.music.iso8601year
import org.oxycblt.auxio.music.plainTrackNo import org.oxycblt.auxio.music.plainTrackNo
import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.music.trackDiscNo import org.oxycblt.auxio.music.trackDiscNo
import org.oxycblt.auxio.music.year import org.oxycblt.auxio.music.year
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD

View file

@ -32,10 +32,6 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.backend.Api21MediaStoreBackend
import org.oxycblt.auxio.music.backend.Api29MediaStoreBackend
import org.oxycblt.auxio.music.backend.Api30MediaStoreBackend
import org.oxycblt.auxio.music.backend.ExoPlayerBackend
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -130,6 +126,10 @@ class Indexer {
this.callback = null this.callback = null
} }
/**
* Start the indexing process. This should be done by [Controller] in a background thread. When
* complete, a new completion state will be pushed to each callback.
*/
suspend fun index(context: Context) { suspend fun index(context: Context) {
requireBackgroundThread() requireBackgroundThread()
@ -190,7 +190,7 @@ class Indexer {
@Synchronized @Synchronized
private fun emitIndexing(indexing: Indexing?, generation: Long) { private fun emitIndexing(indexing: Indexing?, generation: Long) {
checkGenerationImpl(generation) checkGeneration(generation)
if (indexing == indexingState) { if (indexing == indexingState) {
// Ignore redundant states used when the backends just want to check for // Ignore redundant states used when the backends just want to check for
@ -211,7 +211,7 @@ class Indexer {
private suspend fun emitCompletion(response: Response, generation: Long) { private suspend fun emitCompletion(response: Response, generation: Long) {
synchronized(this) { synchronized(this) {
checkGenerationImpl(generation) checkGeneration(generation)
// Do not check for redundancy here, as we actually need to notify a switch // Do not check for redundancy here, as we actually need to notify a switch
// from Indexing -> Complete and not Indexing -> Indexing or Complete -> Complete. // from Indexing -> Complete and not Indexing -> Indexing or Complete -> Complete.
@ -230,7 +230,7 @@ class Indexer {
} }
} }
private fun checkGenerationImpl(generation: Long) { private fun checkGeneration(generation: Long) {
if (currentGeneration != generation) { if (currentGeneration != generation) {
// Not the running task anymore, cancel this co-routine. This allows a yield-like // Not the running task anymore, cancel this co-routine. This allows a yield-like
// behavior to be implemented in a far cheaper manner for each backend. // behavior to be implemented in a far cheaper manner for each backend.

View file

@ -30,7 +30,7 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newMainPendingIntent import org.oxycblt.auxio.util.newMainPendingIntent
/** The notification responsible for showing the indexer state. */ /** The notification responsible for showing the indexer state. */
class IndexerNotification(private val context: Context) : class IndexingNotification(private val context: Context) :
NotificationCompat.Builder(context, CHANNEL_ID) { NotificationCompat.Builder(context, CHANNEL_ID) {
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class) private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
@ -52,7 +52,7 @@ class IndexerNotification(private val context: Context) :
setContentIntent(context.newMainPendingIntent()) setContentIntent(context.newMainPendingIntent())
setVisibility(NotificationCompat.VISIBILITY_PUBLIC) setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
setContentTitle(context.getString(R.string.info_indexer_channel_name)) setContentTitle(context.getString(R.string.info_indexer_channel_name))
setContentText(context.getString(R.string.lbl_indexing)) setContentText(context.getString(R.string.lbl_indexing_desc))
setProgress(0, 0, true) setProgress(0, 0, true)
} }
@ -64,7 +64,7 @@ class IndexerNotification(private val context: Context) :
when (indexing) { when (indexing) {
is Indexer.Indexing.Indeterminate -> { is Indexer.Indexing.Indeterminate -> {
logD("Updating state to $indexing") logD("Updating state to $indexing")
setContentText(context.getString(R.string.lbl_indexing)) setContentText(context.getString(R.string.lbl_indexing_desc))
setProgress(0, 0, true) setProgress(0, 0, true)
return true return true
} }
@ -87,3 +87,37 @@ class IndexerNotification(private val context: Context) :
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER" const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
} }
} }
/** The notification responsible for showing the indexer state. */
class ObservingNotification(context: Context) : NotificationCompat.Builder(context, CHANNEL_ID) {
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
CHANNEL_ID,
context.getString(R.string.info_indexer_channel_name),
NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(channel)
}
setSmallIcon(R.drawable.ic_indexer_24)
setCategory(NotificationCompat.CATEGORY_SERVICE)
setShowWhen(false)
setSilent(true)
setContentIntent(context.newMainPendingIntent())
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
setContentTitle(context.getString(R.string.lbl_observing))
setContentText(context.getString(R.string.lbl_observing_desc))
}
fun renotify() {
notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
}
companion object {
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
}
}

View file

@ -17,9 +17,14 @@
package org.oxycblt.auxio.music.system package org.oxycblt.auxio.music.system
import android.app.Notification
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.database.ContentObserver
import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper
import android.provider.MediaStore
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import coil.imageLoader import coil.imageLoader
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -31,6 +36,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -43,8 +49,6 @@ import org.oxycblt.auxio.util.logD
* boilerplate you skip is not worth the insanity of androidx. * boilerplate you skip is not worth the insanity of androidx.
* *
* @author OxygenCobalt * @author OxygenCobalt
*
* TODO: Add file observing
*/ */
class IndexerService : Service(), Indexer.Controller, Settings.Callback { class IndexerService : Service(), Indexer.Controller, Settings.Callback {
private val indexer = Indexer.getInstance() private val indexer = Indexer.getInstance()
@ -52,18 +56,23 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
private val serviceJob = Job() private val serviceJob = Job()
private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO) private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO)
private lateinit var indexerContentObserver: SystemContentObserver
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private lateinit var settings: Settings private lateinit var settings: Settings
private var isForeground = false private var isForeground = false
private lateinit var notification: IndexerNotification private lateinit var indexingNotification: IndexingNotification
private lateinit var observingNotification: ObservingNotification
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notification = IndexerNotification(this)
settings = Settings(this, this) settings = Settings(this, this)
indexerContentObserver = SystemContentObserver()
indexingNotification = IndexingNotification(this)
observingNotification = ObservingNotification(this)
indexer.registerController(this) indexer.registerController(this)
if (musicStore.library == null && indexer.isIndeterminate) { if (musicStore.library == null && indexer.isIndeterminate) {
@ -81,12 +90,14 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
// cancelLast actually stops foreground for us as it updates the loading state to // De-initialize the components first to prevent stray reloading events
// null or completed.
indexer.cancelLast()
indexer.unregisterController(this)
serviceJob.cancel()
settings.release() settings.release()
indexerContentObserver.release()
indexer.unregisterController(this)
// Then cancel the other components.
indexer.cancelLast()
serviceJob.cancel()
} }
// --- CONTROLLER CALLBACKS --- // --- CONTROLLER CALLBACKS ---
@ -109,7 +120,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
val newLibrary = state.response.library val newLibrary = state.response.library
if (musicStore.library != null) { if (musicStore.library != null) {
// This is a new library to replace a pre-existing one. // This is a new library to replace an existing one.
// Wipe possibly-invalidated album covers // Wipe possibly-invalidated album covers
imageLoader.memoryCache?.clear() imageLoader.memoryCache?.clear()
@ -127,46 +138,115 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
// error, in practice that comes into conflict with the upcoming Android 13 // error, in practice that comes into conflict with the upcoming Android 13
// notification permission, and there is no point implementing permission // notification permission, and there is no point implementing permission
// on-boarding for such when it will only be used for this. // on-boarding for such when it will only be used for this.
stopForegroundSession() updateIdleSession()
} }
is Indexer.State.Indexing -> { is Indexer.State.Indexing -> {
// When loading, we want to enter the foreground state so that android does updateActiveSession(state.indexing)
// not shut off the loading process. Note that while we will always post the
// notification when initially starting, we will not update the notification
// unless it indicates that we have changed it.
val changed = notification.updateIndexingState(state.indexing)
if (!isForeground) {
logD("Starting foreground session")
startForeground(IntegerTable.INDEXER_NOTIFICATION_CODE, notification.build())
isForeground = true
} else if (changed) {
logD("Notification changed, re-posting notification")
notification.renotify()
}
} }
null -> { null -> {
// Null is the indeterminate state that occurs on app startup or after // Null is the indeterminate state that occurs on app startup or after
// the cancellation of a load, so in that case we want to stop foreground // the cancellation of a load, so in that case we want to stop foreground
// since (technically) nothing is loading. // since (technically) nothing is loading.
stopForegroundSession() updateIdleSession()
} }
} }
} }
// --- INTERNAL ---
private fun updateActiveSession(state: Indexer.Indexing) {
// When loading, we want to enter the foreground state so that android does
// not shut off the loading process. Note that while we will always post the
// notification when initially starting, we will not update the notification
// unless it indicates that we have changed it.
val changed = indexingNotification.updateIndexingState(state)
if (!tryStartForeground(indexingNotification.build()) && changed) {
logD("Notification changed, re-posting notification")
indexingNotification.renotify()
}
}
private fun updateIdleSession() {
if (settings.shouldBeObserving) {
// There are a few reasons why we stay in the foreground with automatic rescanning:
// 1. Newer versions of Android have become more and more restrictive regarding
// how a foreground service starts. Thus, it's best to go foreground now so that
// we can go foreground later.
// 2. If a non-foreground service is killed, the app will probably still be alive,
// and thus the music library will not be updated at all.
if (!tryStartForeground(observingNotification.build())) {
observingNotification.renotify()
}
} else {
tryStopForeground()
}
}
private fun tryStartForeground(notification: Notification): Boolean {
if (isForeground) {
return false
}
startForeground(IntegerTable.INDEXER_NOTIFICATION_CODE, notification)
isForeground = true
return true
}
private fun tryStopForeground() {
if (!isForeground) {
return
}
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
isForeground = false
}
// --- SETTING CALLBACKS --- // --- SETTING CALLBACKS ---
override fun onSettingChanged(key: String) { override fun onSettingChanged(key: String) {
if (key == getString(R.string.set_key_music_dirs) || when (key) {
key == getString(R.string.set_key_music_dirs_include) || getString(R.string.set_key_music_dirs),
key == getString(R.string.set_key_quality_tags)) { getString(R.string.set_key_music_dirs_include),
onStartIndexing() getString(R.string.set_key_quality_tags) -> onStartIndexing()
getString(R.string.set_key_observing) -> {
if (!indexer.isIndexing) {
updateIdleSession()
}
}
} }
} }
private fun stopForegroundSession() { /** Internal content observer intended to work with the automatic reloading framework. */
if (isForeground) { private inner class SystemContentObserver(
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) private val handler: Handler = Handler(Looper.getMainLooper())
isForeground = false ) : ContentObserver(handler), Runnable {
init {
contentResolverSafe.registerContentObserver(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, this)
}
fun release() {
contentResolverSafe.unregisterContentObserver(this)
}
override fun onChange(selfChange: Boolean) {
// Batch rapid-fire updates to the library into a single call to run after an
// arbitrary amount of time.
handler.removeCallbacks(this)
handler.postDelayed(this, REINDEX_DELAY)
}
override fun run() {
// Check here if we should even start a reindex. This is much less bug-prone than
// registering and de-registering this component as this setting changes.
if (settings.shouldBeObserving) {
onStartIndexing()
} }
} }
} }
companion object {
const val REINDEX_DELAY = 500L
}
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.backend package org.oxycblt.auxio.music.system
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
@ -27,20 +27,19 @@ 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 java.io.File
import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.music.MimeType
import org.oxycblt.auxio.music.Path
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.albumCoverUri import org.oxycblt.auxio.music.albumCoverUri
import org.oxycblt.auxio.music.audioUri import org.oxycblt.auxio.music.audioUri
import org.oxycblt.auxio.music.directoryCompat
import org.oxycblt.auxio.music.id3GenreName import org.oxycblt.auxio.music.id3GenreName
import org.oxycblt.auxio.music.mediaStoreVolumeNameCompat
import org.oxycblt.auxio.music.packedDiscNo import org.oxycblt.auxio.music.packedDiscNo
import org.oxycblt.auxio.music.packedTrackNo import org.oxycblt.auxio.music.packedTrackNo
import org.oxycblt.auxio.music.queryCursor import org.oxycblt.auxio.music.queryCursor
import org.oxycblt.auxio.music.system.Directory import org.oxycblt.auxio.music.storageVolumesCompat
import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.music.system.MimeType
import org.oxycblt.auxio.music.system.Path
import org.oxycblt.auxio.music.system.directoryCompat
import org.oxycblt.auxio.music.system.mediaStoreVolumeNameCompat
import org.oxycblt.auxio.music.system.storageVolumesCompat
import org.oxycblt.auxio.music.trackDiscNo import org.oxycblt.auxio.music.trackDiscNo
import org.oxycblt.auxio.music.useQuery import org.oxycblt.auxio.music.useQuery
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
@ -182,6 +181,8 @@ abstract class MediaStoreBackend : Indexer.Backend {
audios.add(buildAudio(context, cursor)) audios.add(buildAudio(context, cursor))
if (cursor.position % 50 == 0) { if (cursor.position % 50 == 0) {
// Only check for a cancellation every 50 songs or so (~20ms). // 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) emitIndexing(Indexer.Indexing.Indeterminate)
} }
} }
@ -462,8 +463,8 @@ class Api21MediaStoreBackend : MediaStoreBackend() {
} }
/** /**
* A [MediaStoreBackend] that selects directories and builds paths using the modern volume * A [MediaStoreBackend] that selects directories and builds paths using the modern volume fields
* primitives available from API 29 onwards. * available from API 29 onwards.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
@ -503,7 +504,7 @@ open class VolumeAwareMediaStoreBackend : MediaStoreBackend() {
val relativePath = cursor.getString(relativePathIndex) val relativePath = cursor.getString(relativePathIndex)
// Find the StorageVolume whose MediaStore name corresponds to this song. // Find the StorageVolume whose MediaStore name corresponds to this song.
// This is what we use for the Directory. // This is what we use for the Directory's volume.
val volume = volumes.find { it.mediaStoreVolumeNameCompat == volumeName } val volume = volumes.find { it.mediaStoreVolumeNameCompat == volumeName }
if (volume != null) { if (volume != null) {
audio.dir = Directory(volume, relativePath.removeSuffix(File.separator)) audio.dir = Directory(volume, relativePath.removeSuffix(File.separator))
@ -532,7 +533,7 @@ open class Api29MediaStoreBackend : VolumeAwareMediaStoreBackend() {
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
} }
// This backend is volume-aware, but does not support the modern track primitives. // This backend is volume-aware, but does not support the modern track fields.
// Use the packed utilities instead. // Use the packed utilities instead.
val rawTrack = cursor.getIntOrNull(trackIndex) val rawTrack = cursor.getIntOrNull(trackIndex)
if (rawTrack != null) { if (rawTrack != null) {

View file

@ -25,8 +25,8 @@ import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.music.dirs.MusicDirs import org.oxycblt.auxio.music.dirs.MusicDirs
import org.oxycblt.auxio.music.system.Directory
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
@ -185,6 +185,10 @@ class Settings(private val context: Context, private val callback: Callback? = n
val pauseOnRepeat: Boolean val pauseOnRepeat: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false) get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), 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)
/** Whether to parse metadata directly with ExoPlayer. */ /** Whether to parse metadata directly with ExoPlayer. */
val useQualityTags: Boolean val useQualityTags: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_quality_tags), false) get() = inner.getBoolean(context.getString(R.string.set_key_quality_tags), false)

View file

@ -27,10 +27,10 @@ import android.util.Log
import androidx.core.content.edit import androidx.core.content.edit
import java.io.File import java.io.File
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.system.Directory import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.music.system.directoryCompat import org.oxycblt.auxio.music.directoryCompat
import org.oxycblt.auxio.music.system.isInternalCompat import org.oxycblt.auxio.music.isInternalCompat
import org.oxycblt.auxio.music.system.storageVolumesCompat import org.oxycblt.auxio.music.storageVolumesCompat
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
import org.oxycblt.auxio.util.queryAll import org.oxycblt.auxio.util.queryAll

View file

@ -31,8 +31,8 @@ import androidx.recyclerview.widget.RecyclerView
import coil.Coil import coil.Coil
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.TabCustomizeDialog import org.oxycblt.auxio.home.tabs.TabCustomizeDialog
import org.oxycblt.auxio.music.IndexerViewModel
import org.oxycblt.auxio.music.dirs.MusicDirsDialog import org.oxycblt.auxio.music.dirs.MusicDirsDialog
import org.oxycblt.auxio.music.system.IndexerViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.replaygain.PreAmpCustomizeDialog import org.oxycblt.auxio.playback.replaygain.PreAmpCustomizeDialog
import org.oxycblt.auxio.settings.ui.IntListPreference import org.oxycblt.auxio.settings.ui.IntListPreference

View file

@ -6,7 +6,7 @@
<string name="info_indexer_channel_name">Načítání hudby</string> <string name="info_indexer_channel_name">Načítání hudby</string>
<string name="info_widget_desc">Zobrazení a ovládání přehrávání hudby</string> <string name="info_widget_desc">Zobrazení a ovládání přehrávání hudby</string>
<!-- Label Namespace | Static Labels --> <!-- Label Namespace | Static Labels -->
<string name="lbl_indexing">Načítání vaší hudební knihovny…</string> <string name="lbl_indexing_desc">Načítání vaší hudební knihovny…</string>
<string name="lbl_retry">Zkusit znovu</string> <string name="lbl_retry">Zkusit znovu</string>
<string name="lbl_grant">Udělit</string> <string name="lbl_grant">Udělit</string>
<string name="lbl_genres">Žánry</string> <string name="lbl_genres">Žánry</string>

View file

@ -192,7 +192,7 @@
<string name="lbl_shuffle_shortcut_short">Mischen</string> <string name="lbl_shuffle_shortcut_short">Mischen</string>
<string name="lbl_shuffle_shortcut_long">Alle mischen</string> <string name="lbl_shuffle_shortcut_long">Alle mischen</string>
<string name="info_indexer_channel_name">Musik wird geladen</string> <string name="info_indexer_channel_name">Musik wird geladen</string>
<string name="lbl_indexing">Lade deine Musikbibliothek…</string> <string name="lbl_indexing_desc">Lade deine Musikbibliothek…</string>
<string name="lbl_sample_rate">Abtastrate</string> <string name="lbl_sample_rate">Abtastrate</string>
<string name="lbl_song_detail">Zeige Eigenschaften an</string> <string name="lbl_song_detail">Zeige Eigenschaften an</string>
<string name="lbl_props">Lied-Eigenschaften</string> <string name="lbl_props">Lied-Eigenschaften</string>

View file

@ -101,7 +101,7 @@
<string name="info_playback_channel_name">Lecture de musique</string> <string name="info_playback_channel_name">Lecture de musique</string>
<string name="info_indexer_channel_name">Chargement de musique</string> <string name="info_indexer_channel_name">Chargement de musique</string>
<string name="info_widget_desc">Afficher et contrôler la lecture de la musique</string> <string name="info_widget_desc">Afficher et contrôler la lecture de la musique</string>
<string name="lbl_indexing">Chargement de votre bibliothèque musicale…</string> <string name="lbl_indexing_desc">Chargement de votre bibliothèque musicale…</string>
<string name="lbl_sort_name">Nom</string> <string name="lbl_sort_name">Nom</string>
<string name="lbl_sort_artist">Artiste</string> <string name="lbl_sort_artist">Artiste</string>
<string name="lbl_sort_album">Album</string> <string name="lbl_sort_album">Album</string>

View file

@ -73,7 +73,7 @@
<string name="lbl_size">Ukuran</string> <string name="lbl_size">Ukuran</string>
<string name="lbl_sample_rate">Tingkat sampel</string> <string name="lbl_sample_rate">Tingkat sampel</string>
<string name="set_lib_tabs">Tab Pustaka</string> <string name="set_lib_tabs">Tab Pustaka</string>
<string name="lbl_indexing">Memuat perpustakaan musik Anda…</string> <string name="lbl_indexing_desc">Memuat perpustakaan musik Anda…</string>
<string name="lbl_sort_name">Nama</string> <string name="lbl_sort_name">Nama</string>
<string name="lbl_sort_artist">Artis</string> <string name="lbl_sort_artist">Artis</string>
<string name="lbl_sort_album">Album</string> <string name="lbl_sort_album">Album</string>

View file

@ -185,7 +185,7 @@
<string name="def_sample_rate">Nessuna frequenza di campionamento</string> <string name="def_sample_rate">Nessuna frequenza di campionamento</string>
<string name="fmt_db_neg">-%.1f dB</string> <string name="fmt_db_neg">-%.1f dB</string>
<string name="info_indexer_channel_name">Caricamento musica</string> <string name="info_indexer_channel_name">Caricamento musica</string>
<string name="lbl_indexing">Caricamento libreria musicale…</string> <string name="lbl_indexing_desc">Caricamento libreria musicale…</string>
<string name="lbl_sort_duration">Durata</string> <string name="lbl_sort_duration">Durata</string>
<string name="lbl_sort_count">Conteggio canzoni</string> <string name="lbl_sort_count">Conteggio canzoni</string>
<string name="lbl_sort_disc">Disco</string> <string name="lbl_sort_disc">Disco</string>

View file

@ -102,7 +102,7 @@
<string name="set_dirs_mode">Modo</string> <string name="set_dirs_mode">Modo</string>
<string name="err_no_perms">O Auxio precisa de permissão para ler sua biblioteca de músicas</string> <string name="err_no_perms">O Auxio precisa de permissão para ler sua biblioteca de músicas</string>
<string name="info_app_desc">Um leitor de música simples e racional para android.</string> <string name="info_app_desc">Um leitor de música simples e racional para android.</string>
<string name="lbl_indexing">Carregando sua biblioteca de músicas…</string> <string name="lbl_indexing_desc">Carregando sua biblioteca de músicas…</string>
<string name="lbl_sort_year">Ano</string> <string name="lbl_sort_year">Ano</string>
<string name="lbl_sort_duration">Duração</string> <string name="lbl_sort_duration">Duração</string>
<string name="lbl_sort_count">Contagem de Músicas</string> <string name="lbl_sort_count">Contagem de Músicas</string>

View file

@ -76,7 +76,7 @@
<string name="info_widget_desc">Müzik çalmayı görüntüle ve kontrol et</string> <string name="info_widget_desc">Müzik çalmayı görüntüle ve kontrol et</string>
<string name="info_app_desc">Android için basit, rasyonel bir müzik çalar.</string> <string name="info_app_desc">Android için basit, rasyonel bir müzik çalar.</string>
<string name="info_indexer_channel_name">Müzik Yükleniyor</string> <string name="info_indexer_channel_name">Müzik Yükleniyor</string>
<string name="lbl_indexing">Müzik kitaplığınız yükleniyor…</string> <string name="lbl_indexing_desc">Müzik kitaplığınız yükleniyor…</string>
<string name="lbl_sort_name">İsim</string> <string name="lbl_sort_name">İsim</string>
<string name="lbl_sort_artist">Sanatçı</string> <string name="lbl_sort_artist">Sanatçı</string>
<string name="lbl_sort_album">Albüm</string> <string name="lbl_sort_album">Albüm</string>

View file

@ -185,7 +185,7 @@
<string name="fmt_lib_album_count">已加载专辑数量:%d</string> <string name="fmt_lib_album_count">已加载专辑数量:%d</string>
<string name="def_sample_rate">没有采样率信息</string> <string name="def_sample_rate">没有采样率信息</string>
<string name="info_indexer_channel_name">音乐加载中</string> <string name="info_indexer_channel_name">音乐加载中</string>
<string name="lbl_indexing">正在加载您的音乐库……</string> <string name="lbl_indexing_desc">正在加载您的音乐库……</string>
<string name="lbl_sort_disc">碟片</string> <string name="lbl_sort_disc">碟片</string>
<string name="lbl_sort_duration">时长</string> <string name="lbl_sort_duration">时长</string>
<string name="lbl_sort_count">歌曲计数</string> <string name="lbl_sort_count">歌曲计数</string>

View file

@ -28,7 +28,8 @@
<string name="set_key_reindex" translatable="false">auxio_reindex</string> <string name="set_key_reindex" translatable="false">auxio_reindex</string>
<string name="set_key_music_dirs" translatable="false">auxio_music_dirs</string> <string name="set_key_music_dirs" translatable="false">auxio_music_dirs</string>
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string> <string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
<string name="set_key_quality_tags">auxio_quality_tags</string> <string name="set_key_observing">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> <string name="set_key_search_filter" translatable="false">KEY_SEARCH_FILTER</string>

View file

@ -2,14 +2,16 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"> <resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<!-- Info namespace | App labels --> <!-- Info namespace | App labels -->
<string name="info_app_desc">A simple, rational music player for android.</string> <string name="info_app_desc">A simple, rational music player for android.</string>
<string name="info_playback_channel_name">Music Playback</string> <string name="info_playback_channel_name">Music playback</string>
<string name="info_indexer_channel_name">Music Loading</string> <string name="info_indexer_channel_name">Music loading</string>
<string name="info_widget_desc">View and control music playback</string> <string name="info_widget_desc">View and control music playback</string>
<!-- Label Namespace | Static Labels --> <!-- Label Namespace | Static Labels -->
<string name="lbl_indexing">Loading your music library…</string>
<string name="lbl_retry">Retry</string> <string name="lbl_retry">Retry</string>
<string name="lbl_grant">Grant</string> <string name="lbl_grant">Grant</string>
<string name="lbl_indexing_desc">Loading your music library…</string>
<string name="lbl_observing">Automatic reloading</string>
<string name="lbl_observing_desc">Monitoring your music library for changes… (You can disable this in settings)</string>
<string name="lbl_genres">Genres</string> <string name="lbl_genres">Genres</string>
<string name="lbl_artists">Artists</string> <string name="lbl_artists">Artists</string>
@ -146,7 +148,9 @@
<string name="set_dirs_mode_include">Include</string> <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_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">Ignore MediaStore tags</string>
<string name="set_quality_tags_desc">Increases tag quality, but requires longer loading times</string> <string name="set_quality_tags_desc">Increases tag quality, but requires longer loading times (Experimental)</string>
<string name="set_observing">Automatic reloading</string>
<string name="set_observing_desc">Reload music whenever your audio files change (Experimental)</string>
<!-- Error Namespace | Error Labels --> <!-- Error Namespace | Error Labels -->
<string name="err_no_music">No music found</string> <string name="err_no_music">No music found</string>
@ -227,6 +231,7 @@
<string name="clr_orange">Orange</string> <string name="clr_orange">Orange</string>
<string name="clr_brown">Brown</string> <string name="clr_brown">Brown</string>
<string name="clr_grey">Grey</string> <string name="clr_grey">Grey</string>
<!-- As in "Dynamic Colors"/Material You theming -->
<string name="clr_dynamic">Dynamic</string> <string name="clr_dynamic">Dynamic</string>
<!-- Format Namespace | Value formatting/plurals --> <!-- Format Namespace | Value formatting/plurals -->
@ -247,6 +252,7 @@
<string name="fmt_lib_album_count">Albums loaded: %d</string> <string name="fmt_lib_album_count">Albums loaded: %d</string>
<string name="fmt_lib_artist_count">Artists loaded: %d</string> <string name="fmt_lib_artist_count">Artists loaded: %d</string>
<string name="fmt_lib_genre_count">Genres loaded: %d</string> <string name="fmt_lib_genre_count">Genres loaded: %d</string>
<!-- AS in the total duration of all songs in the music library -->
<string name="fmt_lib_total_duration">Total duration: %s</string> <string name="fmt_lib_total_duration">Total duration: %s</string>
<plurals name="fmt_song_count"> <plurals name="fmt_song_count">

View file

@ -187,5 +187,13 @@
app:summary="@string/set_quality_tags_desc" app:summary="@string/set_quality_tags_desc"
app:title="@string/set_quality_tags" /> app:title="@string/set_quality_tags" />
<org.oxycblt.auxio.settings.ui.M3SwitchPreference
app:allowDividerBelow="false"
app:defaultValue="false"
app:iconSpaceReserved="false"
app:key="@string/set_key_observing"
app:summary="@string/set_observing_desc"
app:title="@string/set_observing" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>