Add blacklist restart functionality
When the user selects the "Save" button in the blacklist dialog, the app will now restart to reload the music library with the new directories.
This commit is contained in:
parent
787212ee59
commit
68887ffb64
12 changed files with 241 additions and 150 deletions
|
@ -22,7 +22,7 @@
|
|||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:launchMode="singleInstance"
|
||||
android:launchMode="singleTask"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
|
|
|
@ -5,8 +5,6 @@ import android.content.Context
|
|||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import org.oxycblt.auxio.logD
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Database for storing blacklisted paths.
|
||||
|
@ -30,44 +28,31 @@ class BlacklistDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n
|
|||
}
|
||||
|
||||
/**
|
||||
* Add a [File] to the the blacklist.
|
||||
* @return Whether this file has been added to the database or not.
|
||||
* Write a list of [paths] to the database.
|
||||
*/
|
||||
fun addPath(file: File): Boolean {
|
||||
fun writePaths(paths: List<String>) {
|
||||
assertBackgroundThread()
|
||||
|
||||
val path = file.canonicalPathSafe
|
||||
|
||||
logD("Adding path $path to blacklist")
|
||||
|
||||
if (hasFile(path)) {
|
||||
logD("Path already exists. Ignoring.")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
writableDatabase.execute {
|
||||
val values = ContentValues(1)
|
||||
values.put(COLUMN_PATH, path)
|
||||
delete(TABLE_NAME, null, null)
|
||||
|
||||
insert(TABLE_NAME, null, values)
|
||||
logD("Deleted paths db")
|
||||
|
||||
for (path in paths) {
|
||||
insert(
|
||||
TABLE_NAME, null,
|
||||
ContentValues(1).apply {
|
||||
put(COLUMN_PATH, path)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a [File] from this blacklist.
|
||||
* Get the current list of paths from the database.
|
||||
*/
|
||||
fun removePath(file: File) {
|
||||
assertBackgroundThread()
|
||||
|
||||
writableDatabase.execute {
|
||||
delete(TABLE_NAME, "$COLUMN_PATH=?", arrayOf(file.canonicalPathSafe))
|
||||
}
|
||||
}
|
||||
|
||||
fun getPaths(): List<String> {
|
||||
fun readPaths(): List<String> {
|
||||
assertBackgroundThread()
|
||||
|
||||
val paths = mutableListOf<String>()
|
||||
|
@ -81,22 +66,6 @@ class BlacklistDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n
|
|||
return paths
|
||||
}
|
||||
|
||||
private fun hasFile(path: String): Boolean {
|
||||
val exists = readableDatabase.queryUse(TABLE_NAME, null, "$COLUMN_PATH=?", path) { cursor ->
|
||||
cursor.moveToFirst()
|
||||
}
|
||||
|
||||
return exists ?: false
|
||||
}
|
||||
|
||||
private val File.canonicalPathSafe: String get() {
|
||||
return try {
|
||||
canonicalPath
|
||||
} catch (e: IOException) {
|
||||
absolutePath
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DB_VERSION = 1
|
||||
const val DB_NAME = "auxio_blacklist_database.db"
|
||||
|
|
|
@ -45,7 +45,7 @@ class MusicLoader(private val context: Context) {
|
|||
private fun buildSelector() {
|
||||
val blacklistDatabase = BlacklistDatabase.getInstance(context)
|
||||
|
||||
val paths = blacklistDatabase.getPaths()
|
||||
val paths = blacklistDatabase.readPaths()
|
||||
|
||||
for (path in paths) {
|
||||
selector += " AND ${Media.DATA} NOT LIKE ?"
|
||||
|
@ -267,5 +267,7 @@ class MusicLoader(private val context: Context) {
|
|||
|
||||
genres.add(unknownGenre)
|
||||
}
|
||||
|
||||
genres.removeAll { it.songs.isEmpty() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import androidx.lifecycle.Transformations
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.logD
|
||||
import org.oxycblt.auxio.logE
|
||||
import org.oxycblt.auxio.music.Album
|
||||
|
@ -24,7 +23,6 @@ import org.oxycblt.auxio.playback.state.PlaybackMode
|
|||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.recycler.SortMode
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.createToast
|
||||
|
||||
/**
|
||||
* The ViewModel that provides a UI frontend for [PlaybackStateManager].
|
||||
|
@ -360,11 +358,11 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
* Force save the current [PlaybackStateManager] state to the database.
|
||||
* Called by SettingsListFragment.
|
||||
*/
|
||||
fun savePlaybackState(context: Context) {
|
||||
fun savePlaybackState(context: Context, onDone: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
playbackManager.saveStateToDatabase(context)
|
||||
|
||||
context.getString(R.string.debug_state_saved).createToast(context)
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.oxycblt.auxio.recycler.DisplayMode
|
|||
import org.oxycblt.auxio.settings.blacklist.BlacklistDialog
|
||||
import org.oxycblt.auxio.settings.ui.AccentDialog
|
||||
import org.oxycblt.auxio.ui.Accent
|
||||
import org.oxycblt.auxio.ui.createToast
|
||||
|
||||
/**
|
||||
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat].
|
||||
|
@ -126,7 +127,9 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
|||
|
||||
SettingsManager.Keys.KEY_SAVE_STATE -> {
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
playbackModel.savePlaybackState(requireContext())
|
||||
playbackModel.savePlaybackState(requireContext()) {
|
||||
getString(R.string.debug_state_saved).createToast(requireContext())
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.oxycblt.auxio.settings.blacklist
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.LayoutInflater
|
||||
|
@ -8,6 +10,7 @@ import android.view.ViewGroup
|
|||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.customview.getCustomView
|
||||
|
@ -16,13 +19,24 @@ import com.afollestad.materialdialogs.files.selectedFolder
|
|||
import com.afollestad.materialdialogs.internal.list.DialogRecyclerView
|
||||
import com.afollestad.materialdialogs.utils.invalidateDividers
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import org.oxycblt.auxio.MainActivity
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentBlacklistBinding
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.createToast
|
||||
import java.io.File
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Dialog that manages the currently excluded directories.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class BlacklistDialog : BottomSheetDialogFragment() {
|
||||
private val blacklistModel: BlacklistViewModel by activityViewModels()
|
||||
private val blacklistModel: BlacklistViewModel by viewModels {
|
||||
BlacklistViewModel.Factory(requireContext())
|
||||
}
|
||||
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
||||
override fun getTheme() = R.style.Theme_BottomSheetFix
|
||||
|
||||
|
@ -48,6 +62,14 @@ class BlacklistDialog : BottomSheetDialogFragment() {
|
|||
dismiss()
|
||||
}
|
||||
|
||||
binding.blacklistConfirm.setOnClickListener {
|
||||
if (blacklistModel.modified) {
|
||||
saveAndRestart()
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
blacklistModel.paths.observe(viewLifecycleOwner) { paths ->
|
||||
|
@ -59,6 +81,13 @@ class BlacklistDialog : BottomSheetDialogFragment() {
|
|||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
|
||||
// If we have dismissed the dialog, then just drop any path changes.
|
||||
blacklistModel.loadDatabasePaths()
|
||||
}
|
||||
|
||||
private fun showFileDialog() {
|
||||
MaterialDialog(requireActivity()).show {
|
||||
positiveButton(R.string.label_add) {
|
||||
|
@ -95,8 +124,7 @@ class BlacklistDialog : BottomSheetDialogFragment() {
|
|||
// to be excluded, as that would lead to the user being stuck at the "No Music Found"
|
||||
// screen.
|
||||
if (path == Environment.getExternalStorageDirectory().absolutePath) {
|
||||
getString(R.string.error_folder_would_brick_app)
|
||||
.createToast(requireContext())
|
||||
getString(R.string.error_folder_would_brick_app).createToast(requireContext())
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -104,4 +132,23 @@ class BlacklistDialog : BottomSheetDialogFragment() {
|
|||
blacklistModel.addPath(path)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAndRestart() {
|
||||
blacklistModel.save {
|
||||
playbackModel.savePlaybackState(requireContext(), ::reincarnate)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reincarnate() {
|
||||
// Instead of having to do a ton of cleanup and make a ton of horrible code changes
|
||||
// to restart this application non-destructively, I just restart the UI task [There is only
|
||||
// one, after all] and then kill the application using exitProcess. Works well enough.
|
||||
val intent = Intent(requireContext().applicationContext, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
requireContext().startActivity(intent)
|
||||
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,10 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import org.oxycblt.auxio.databinding.ItemBlacklistEntryBinding
|
||||
import org.oxycblt.auxio.ui.inflater
|
||||
|
||||
/**
|
||||
* Adapter that shows the blacklist entries and their "Clear" button.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class BlacklistEntryAdapter(
|
||||
private val onClear: (String) -> Unit
|
||||
) : RecyclerView.Adapter<BlacklistEntryAdapter.ViewHolder>() {
|
||||
|
|
|
@ -1,26 +1,92 @@
|
|||
package org.oxycblt.auxio.settings.blacklist
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.database.BlacklistDatabase
|
||||
|
||||
class BlacklistViewModel : ViewModel() {
|
||||
/**
|
||||
* ViewModel that acts as a wrapper around [BlacklistDatabase], allowing for the addition/removal
|
||||
* of paths. Use [Factory] to instantiate this.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class BlacklistViewModel(context: Context) : ViewModel() {
|
||||
private val mPaths = MutableLiveData(mutableListOf<String>())
|
||||
val paths: LiveData<MutableList<String>> get() = mPaths
|
||||
|
||||
fun addPath(path: String) {
|
||||
if (mPaths.value!!.contains(path)) {
|
||||
return
|
||||
private val blacklistDatabase = BlacklistDatabase.getInstance(context)
|
||||
var modified = false
|
||||
private set
|
||||
|
||||
init {
|
||||
loadDatabasePaths()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a path to this viewmodel. It will not write the path to the database unless
|
||||
* [save] is called.
|
||||
*/
|
||||
fun addPath(path: String) {
|
||||
if (mPaths.value!!.contains(path)) return
|
||||
|
||||
mPaths.value!!.add(path)
|
||||
|
||||
mPaths.value = mPaths.value
|
||||
|
||||
modified = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a path from this viewmodel, it will not remove this path from the database unless
|
||||
* [save] is called.
|
||||
*/
|
||||
fun removePath(path: String) {
|
||||
mPaths.value!!.remove(path)
|
||||
|
||||
mPaths.value = mPaths.value
|
||||
|
||||
modified = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the pending paths to the database. [onDone] will be called on completion.
|
||||
*/
|
||||
fun save(onDone: () -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
blacklistDatabase.writePaths(mPaths.value!!)
|
||||
modified = false
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the paths stored in the database to this ViewModel, will erase any pending changes.
|
||||
*/
|
||||
fun loadDatabasePaths() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val paths = blacklistDatabase.readPaths().toMutableList()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
// Can only change LiveData on the main thread.
|
||||
mPaths.value = paths
|
||||
}
|
||||
|
||||
modified = false
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(private val context: Context) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
check(modelClass.isAssignableFrom(BlacklistViewModel::class.java)) {
|
||||
"BlacklistViewModel.Factory does not support this class"
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return BlacklistViewModel(context) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ class SongsFragment : Fragment() {
|
|||
* Perform the (Frustratingly Long and Complicated) FastScrollerView setup.
|
||||
*/
|
||||
private fun FastScrollerView.setup(recycler: RecyclerView, thumb: CobaltScrollThumb) {
|
||||
var concatInterval: Int = -1
|
||||
var truncateInterval: Int = -1
|
||||
|
||||
// API 22 and below don't support the state color, so just use the accent.
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||
|
@ -104,38 +104,36 @@ class SongsFragment : Fragment() {
|
|||
{ pos ->
|
||||
val char = musicStore.songs[pos].name.first
|
||||
|
||||
FastScrollItemIndicator.Text(
|
||||
// Use "#" if the character is a digit, also has the nice side-effect of
|
||||
// truncating extra numbers.
|
||||
if (char.isDigit()) {
|
||||
FastScrollItemIndicator.Text("#")
|
||||
} else {
|
||||
FastScrollItemIndicator.Text(char.toString())
|
||||
}
|
||||
if (char.isDigit()) "#" else char.toString()
|
||||
)
|
||||
},
|
||||
null, false
|
||||
)
|
||||
|
||||
showIndicator = { _, i, total ->
|
||||
if (concatInterval == -1) {
|
||||
if (truncateInterval == -1) {
|
||||
// If the scroller size is too small to contain all the entries, truncate entries
|
||||
// so that the fast scroller entries fit.
|
||||
val maxEntries = (height / (indicatorTextSize + textPadding))
|
||||
|
||||
if (total > maxEntries.toInt()) {
|
||||
concatInterval = ceil(total / maxEntries).toInt()
|
||||
truncateInterval = ceil(total / maxEntries).toInt()
|
||||
|
||||
check(concatInterval > 1) {
|
||||
"Needed to truncate, but concatInterval was 1 or lower anyway"
|
||||
check(truncateInterval > 1) {
|
||||
"Needed to truncate, but truncateInterval was 1 or lower anyway"
|
||||
}
|
||||
|
||||
logD("More entries than screen space, truncating by $concatInterval.")
|
||||
logD("More entries than screen space, truncating by $truncateInterval.")
|
||||
} else {
|
||||
concatInterval = 1
|
||||
truncateInterval = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Any items that need to be truncated will be hidden
|
||||
(i % concatInterval) == 0
|
||||
(i % truncateInterval) == 0
|
||||
}
|
||||
|
||||
addIndicatorCallback { _, _, pos ->
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -16,9 +20,9 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_exbold"
|
||||
android:textAlignment="viewStart"
|
||||
android:padding="@dimen/padding_medium"
|
||||
android:text="@string/setting_content_blacklist"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textSize="@dimen/text_size_toolbar_header"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -58,7 +62,7 @@
|
|||
android:id="@+id/blacklist_confirm"
|
||||
style="@style/Widget.Button.Dialog"
|
||||
android:layout_marginEnd="@dimen/margin_medium"
|
||||
android:text="@string/label_confirm"
|
||||
android:text="@string/label_save"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/blacklist_cancel"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/blacklist_cancel" />
|
||||
|
@ -74,5 +78,5 @@
|
|||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</layout>
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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_channel_name">Music Playback</string>
|
||||
|
@ -51,7 +52,7 @@
|
|||
<string name="label_author">Developed by OxygenCobalt</string>
|
||||
|
||||
<string name="label_add">Add</string>
|
||||
<string name="label_confirm">Confirm</string>
|
||||
<string name="label_save">Save</string>
|
||||
<string name="label_empty_blacklist">No excluded folders</string>
|
||||
|
||||
<!-- Settings namespace | Settings-related labels -->
|
||||
|
|
|
@ -15,7 +15,6 @@ Here are the music formats that Auxio supports, as per the [Supported ExoPlayer
|
|||
| MKA | ✅ | |
|
||||
| OGG | ✅ | Containing Vorbis, Opus, and FLAC |
|
||||
| WAV | ✅ | |
|
||||
| MPEG_TS | ✅ | |
|
||||
| MPEG_TS | ✅ | |
|
||||
| MPEG | ✅ | |
|
||||
| AAC | ✅ | |
|
||||
| FLAC | ❌ | Auxio must be patched with the [FLAC Extension](https://github.com/google/ExoPlayer/tree/release-v2/extensions/flac) |
|
||||
|
|
Loading…
Reference in a new issue