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:
parent
94f2d28936
commit
3a7768ad22
28 changed files with 224 additions and 88 deletions
|
@ -3,8 +3,10 @@
|
|||
## dev
|
||||
|
||||
#### What's New
|
||||
- Massively overhauled how music is loaded [#72]:
|
||||
- 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
|
||||
- Auxio can now reload music without requiring a restart
|
||||
- Added a shuffle shortcut
|
||||
- Widgets now have a more sleek and consistent button layout
|
||||
- "Rounded album covers" is now "Round mode"
|
||||
|
|
|
@ -33,9 +33,9 @@ import org.oxycblt.auxio.detail.recycler.SortHeader
|
|||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.MimeType
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.system.MimeType
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.recycler.Header
|
||||
|
|
|
@ -46,10 +46,10 @@ import org.oxycblt.auxio.home.list.SongListFragment
|
|||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.IndexerViewModel
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.system.Indexer
|
||||
import org.oxycblt.auxio.music.system.IndexerViewModel
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
|
@ -313,7 +313,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
|
||||
when (indexing) {
|
||||
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
|
||||
}
|
||||
is Indexer.Indexing.Songs -> {
|
||||
|
|
|
@ -15,15 +15,18 @@
|
|||
* 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 kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.oxycblt.auxio.music.system.Indexer
|
||||
|
||||
/**
|
||||
* A ViewModel representing the current music indexing state.
|
||||
* @author OxygenCobalt
|
||||
*
|
||||
* TODO: Indeterminate state for Home + Settings
|
||||
*/
|
||||
class IndexerViewModel : ViewModel(), Indexer.Callback {
|
||||
private val indexer = Indexer.getInstance()
|
|
@ -23,8 +23,6 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
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.util.unlikelyToBeNull
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* 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.content.Context
|
||||
|
@ -109,7 +109,7 @@ val StorageVolume.directoryCompat: String?
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
directory?.absolutePath
|
||||
} else {
|
||||
// Replicate getDirectory: getPath if mounted, null if not
|
||||
// Replicate API: getPath if mounted, null if not
|
||||
when (stateCompat) {
|
||||
Environment.MEDIA_MOUNTED,
|
||||
Environment.MEDIA_MOUNTED_READ_ONLY ->
|
|
@ -19,7 +19,7 @@ package org.oxycblt.auxio.music.dirs
|
|||
|
||||
import android.content.Context
|
||||
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.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.MonoAdapter
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
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 */
|
||||
data class MusicDirs(val dirs: List<Directory>, val shouldInclude: Boolean)
|
||||
|
|
|
@ -28,7 +28,7 @@ import androidx.core.view.isVisible
|
|||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.R
|
||||
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.ui.fragment.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.context
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* 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.database.Cursor
|
||||
|
@ -29,7 +29,6 @@ import org.oxycblt.auxio.music.audioUri
|
|||
import org.oxycblt.auxio.music.id3GenreName
|
||||
import org.oxycblt.auxio.music.iso8601year
|
||||
import org.oxycblt.auxio.music.plainTrackNo
|
||||
import org.oxycblt.auxio.music.system.Indexer
|
||||
import org.oxycblt.auxio.music.trackDiscNo
|
||||
import org.oxycblt.auxio.music.year
|
||||
import org.oxycblt.auxio.util.logD
|
|
@ -32,10 +32,6 @@ import org.oxycblt.auxio.music.Artist
|
|||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
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.ui.Sort
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -130,6 +126,10 @@ class Indexer {
|
|||
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) {
|
||||
requireBackgroundThread()
|
||||
|
||||
|
@ -190,7 +190,7 @@ class Indexer {
|
|||
|
||||
@Synchronized
|
||||
private fun emitIndexing(indexing: Indexing?, generation: Long) {
|
||||
checkGenerationImpl(generation)
|
||||
checkGeneration(generation)
|
||||
|
||||
if (indexing == indexingState) {
|
||||
// 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) {
|
||||
synchronized(this) {
|
||||
checkGenerationImpl(generation)
|
||||
checkGeneration(generation)
|
||||
|
||||
// Do not check for redundancy here, as we actually need to notify a switch
|
||||
// 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) {
|
||||
// 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.
|
||||
|
|
|
@ -30,7 +30,7 @@ import org.oxycblt.auxio.util.logD
|
|||
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||
|
||||
/** 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) {
|
||||
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
||||
|
||||
|
@ -52,7 +52,7 @@ class IndexerNotification(private val context: Context) :
|
|||
setContentIntent(context.newMainPendingIntent())
|
||||
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ class IndexerNotification(private val context: Context) :
|
|||
when (indexing) {
|
||||
is Indexer.Indexing.Indeterminate -> {
|
||||
logD("Updating state to $indexing")
|
||||
setContentText(context.getString(R.string.lbl_indexing))
|
||||
setContentText(context.getString(R.string.lbl_indexing_desc))
|
||||
setProgress(0, 0, true)
|
||||
return true
|
||||
}
|
||||
|
@ -87,3 +87,37 @@ class IndexerNotification(private val context: Context) :
|
|||
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"
|
||||
}
|
||||
}
|
|
@ -17,9 +17,14 @@
|
|||
|
||||
package org.oxycblt.auxio.music.system
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.database.ContentObserver
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.app.ServiceCompat
|
||||
import coil.imageLoader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -31,6 +36,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.contentResolverSafe
|
||||
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.
|
||||
*
|
||||
* @author OxygenCobalt
|
||||
*
|
||||
* TODO: Add file observing
|
||||
*/
|
||||
class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||
private val indexer = Indexer.getInstance()
|
||||
|
@ -52,18 +56,23 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
|
||||
private val serviceJob = Job()
|
||||
private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO)
|
||||
private lateinit var indexerContentObserver: SystemContentObserver
|
||||
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private lateinit var settings: Settings
|
||||
|
||||
private var isForeground = false
|
||||
private lateinit var notification: IndexerNotification
|
||||
private lateinit var indexingNotification: IndexingNotification
|
||||
private lateinit var observingNotification: ObservingNotification
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
notification = IndexerNotification(this)
|
||||
settings = Settings(this, this)
|
||||
indexerContentObserver = SystemContentObserver()
|
||||
|
||||
indexingNotification = IndexingNotification(this)
|
||||
observingNotification = ObservingNotification(this)
|
||||
|
||||
indexer.registerController(this)
|
||||
if (musicStore.library == null && indexer.isIndeterminate) {
|
||||
|
@ -81,12 +90,14 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
// cancelLast actually stops foreground for us as it updates the loading state to
|
||||
// null or completed.
|
||||
indexer.cancelLast()
|
||||
indexer.unregisterController(this)
|
||||
serviceJob.cancel()
|
||||
// De-initialize the components first to prevent stray reloading events
|
||||
settings.release()
|
||||
indexerContentObserver.release()
|
||||
indexer.unregisterController(this)
|
||||
|
||||
// Then cancel the other components.
|
||||
indexer.cancelLast()
|
||||
serviceJob.cancel()
|
||||
}
|
||||
|
||||
// --- CONTROLLER CALLBACKS ---
|
||||
|
@ -109,7 +120,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
val newLibrary = state.response.library
|
||||
|
||||
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
|
||||
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
|
||||
// notification permission, and there is no point implementing permission
|
||||
// on-boarding for such when it will only be used for this.
|
||||
stopForegroundSession()
|
||||
updateIdleSession()
|
||||
}
|
||||
is Indexer.State.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 = 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()
|
||||
}
|
||||
updateActiveSession(state.indexing)
|
||||
}
|
||||
null -> {
|
||||
// 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
|
||||
// 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 ---
|
||||
|
||||
override fun onSettingChanged(key: String) {
|
||||
if (key == getString(R.string.set_key_music_dirs) ||
|
||||
key == getString(R.string.set_key_music_dirs_include) ||
|
||||
key == getString(R.string.set_key_quality_tags)) {
|
||||
onStartIndexing()
|
||||
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_observing) -> {
|
||||
if (!indexer.isIndexing) {
|
||||
updateIdleSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopForegroundSession() {
|
||||
if (isForeground) {
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
isForeground = false
|
||||
/** Internal content observer intended to work with the automatic reloading framework. */
|
||||
private inner class SystemContentObserver(
|
||||
private val handler: Handler = Handler(Looper.getMainLooper())
|
||||
) : 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* 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.database.Cursor
|
||||
|
@ -27,20 +27,19 @@ import androidx.annotation.RequiresApi
|
|||
import androidx.core.database.getIntOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
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.albumCoverUri
|
||||
import org.oxycblt.auxio.music.audioUri
|
||||
import org.oxycblt.auxio.music.directoryCompat
|
||||
import org.oxycblt.auxio.music.id3GenreName
|
||||
import org.oxycblt.auxio.music.mediaStoreVolumeNameCompat
|
||||
import org.oxycblt.auxio.music.packedDiscNo
|
||||
import org.oxycblt.auxio.music.packedTrackNo
|
||||
import org.oxycblt.auxio.music.queryCursor
|
||||
import org.oxycblt.auxio.music.system.Directory
|
||||
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.storageVolumesCompat
|
||||
import org.oxycblt.auxio.music.trackDiscNo
|
||||
import org.oxycblt.auxio.music.useQuery
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
|
@ -182,6 +181,8 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
audios.add(buildAudio(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)
|
||||
}
|
||||
}
|
||||
|
@ -462,8 +463,8 @@ class Api21MediaStoreBackend : MediaStoreBackend() {
|
|||
}
|
||||
|
||||
/**
|
||||
* A [MediaStoreBackend] that selects directories and builds paths using the modern volume
|
||||
* primitives available from API 29 onwards.
|
||||
* A [MediaStoreBackend] that selects directories and builds paths using the modern volume fields
|
||||
* available from API 29 onwards.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
|
@ -503,7 +504,7 @@ open class VolumeAwareMediaStoreBackend : MediaStoreBackend() {
|
|||
val relativePath = cursor.getString(relativePathIndex)
|
||||
|
||||
// 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 }
|
||||
if (volume != null) {
|
||||
audio.dir = Directory(volume, relativePath.removeSuffix(File.separator))
|
||||
|
@ -532,7 +533,7 @@ open class Api29MediaStoreBackend : VolumeAwareMediaStoreBackend() {
|
|||
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.
|
||||
val rawTrack = cursor.getIntOrNull(trackIndex)
|
||||
if (rawTrack != null) {
|
|
@ -25,8 +25,8 @@ import androidx.core.content.edit
|
|||
import androidx.preference.PreferenceManager
|
||||
import org.oxycblt.auxio.R
|
||||
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.system.Directory
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
|
||||
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
|
||||
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. */
|
||||
val useQualityTags: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_quality_tags), false)
|
||||
|
|
|
@ -27,10 +27,10 @@ import android.util.Log
|
|||
import androidx.core.content.edit
|
||||
import java.io.File
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.system.Directory
|
||||
import org.oxycblt.auxio.music.system.directoryCompat
|
||||
import org.oxycblt.auxio.music.system.isInternalCompat
|
||||
import org.oxycblt.auxio.music.system.storageVolumesCompat
|
||||
import org.oxycblt.auxio.music.Directory
|
||||
import org.oxycblt.auxio.music.directoryCompat
|
||||
import org.oxycblt.auxio.music.isInternalCompat
|
||||
import org.oxycblt.auxio.music.storageVolumesCompat
|
||||
import org.oxycblt.auxio.ui.accent.Accent
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.queryAll
|
||||
|
|
|
@ -31,8 +31,8 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import coil.Coil
|
||||
import org.oxycblt.auxio.R
|
||||
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.system.IndexerViewModel
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.playback.replaygain.PreAmpCustomizeDialog
|
||||
import org.oxycblt.auxio.settings.ui.IntListPreference
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<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>
|
||||
<!-- 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_grant">Udělit</string>
|
||||
<string name="lbl_genres">Žánry</string>
|
||||
|
|
|
@ -192,7 +192,7 @@
|
|||
<string name="lbl_shuffle_shortcut_short">Mischen</string>
|
||||
<string name="lbl_shuffle_shortcut_long">Alle mischen</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_song_detail">Zeige Eigenschaften an</string>
|
||||
<string name="lbl_props">Lied-Eigenschaften</string>
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
<string name="info_playback_channel_name">Lecture 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="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_artist">Artiste</string>
|
||||
<string name="lbl_sort_album">Album</string>
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
<string name="lbl_size">Ukuran</string>
|
||||
<string name="lbl_sample_rate">Tingkat sampel</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_artist">Artis</string>
|
||||
<string name="lbl_sort_album">Album</string>
|
||||
|
|
|
@ -185,7 +185,7 @@
|
|||
<string name="def_sample_rate">Nessuna frequenza di campionamento</string>
|
||||
<string name="fmt_db_neg">-%.1f dB</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_count">Conteggio canzoni</string>
|
||||
<string name="lbl_sort_disc">Disco</string>
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
<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="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_duration">Duração</string>
|
||||
<string name="lbl_sort_count">Contagem de Músicas</string>
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
<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_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_artist">Sanatçı</string>
|
||||
<string name="lbl_sort_album">Albüm</string>
|
||||
|
|
|
@ -185,7 +185,7 @@
|
|||
<string name="fmt_lib_album_count">已加载专辑数量:%d</string>
|
||||
<string name="def_sample_rate">没有采样率信息</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_duration">时长</string>
|
||||
<string name="lbl_sort_count">歌曲计数</string>
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
<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_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>
|
||||
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<!-- Info namespace | App labels -->
|
||||
<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_indexer_channel_name">Music Loading</string>
|
||||
<string name="info_playback_channel_name">Music playback</string>
|
||||
<string name="info_indexer_channel_name">Music loading</string>
|
||||
<string name="info_widget_desc">View and control music playback</string>
|
||||
|
||||
<!-- Label Namespace | Static Labels -->
|
||||
<string name="lbl_indexing">Loading your music library…</string>
|
||||
<string name="lbl_retry">Retry</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_artists">Artists</string>
|
||||
|
@ -146,7 +148,9 @@
|
|||
<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 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 -->
|
||||
<string name="err_no_music">No music found</string>
|
||||
|
@ -227,6 +231,7 @@
|
|||
<string name="clr_orange">Orange</string>
|
||||
<string name="clr_brown">Brown</string>
|
||||
<string name="clr_grey">Grey</string>
|
||||
<!-- As in "Dynamic Colors"/Material You theming -->
|
||||
<string name="clr_dynamic">Dynamic</string>
|
||||
|
||||
<!-- Format Namespace | Value formatting/plurals -->
|
||||
|
@ -247,6 +252,7 @@
|
|||
<string name="fmt_lib_album_count">Albums loaded: %d</string>
|
||||
<string name="fmt_lib_artist_count">Artists 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>
|
||||
|
||||
<plurals name="fmt_song_count">
|
||||
|
|
|
@ -187,5 +187,13 @@
|
|||
app:summary="@string/set_quality_tags_desc"
|
||||
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>
|
||||
</PreferenceScreen>
|
Loading…
Reference in a new issue