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
|
## dev
|
||||||
|
|
||||||
#### What's New
|
#### 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
|
- 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
|
- 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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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()
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 ->
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue