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:
OxygenCobalt 2021-03-15 15:39:08 -06:00
parent 787212ee59
commit 68887ffb64
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 241 additions and 150 deletions

View file

@ -22,7 +22,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:launchMode="singleInstance" android:launchMode="singleTask"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustPan"> android:windowSoftInputMode="adjustPan">

View file

@ -5,8 +5,6 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
import org.oxycblt.auxio.logD import org.oxycblt.auxio.logD
import java.io.File
import java.io.IOException
/** /**
* Database for storing blacklisted paths. * 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. * Write a list of [paths] to the database.
* @return Whether this file has been added to the database or not.
*/ */
fun addPath(file: File): Boolean { fun writePaths(paths: List<String>) {
assertBackgroundThread() assertBackgroundThread()
val path = file.canonicalPathSafe
logD("Adding path $path to blacklist")
if (hasFile(path)) {
logD("Path already exists. Ignoring.")
return false
}
writableDatabase.execute { writableDatabase.execute {
val values = ContentValues(1) delete(TABLE_NAME, null, null)
values.put(COLUMN_PATH, path)
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) { fun readPaths(): List<String> {
assertBackgroundThread()
writableDatabase.execute {
delete(TABLE_NAME, "$COLUMN_PATH=?", arrayOf(file.canonicalPathSafe))
}
}
fun getPaths(): List<String> {
assertBackgroundThread() assertBackgroundThread()
val paths = mutableListOf<String>() val paths = mutableListOf<String>()
@ -81,22 +66,6 @@ class BlacklistDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n
return paths 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 { companion object {
const val DB_VERSION = 1 const val DB_VERSION = 1
const val DB_NAME = "auxio_blacklist_database.db" const val DB_NAME = "auxio_blacklist_database.db"

View file

@ -45,7 +45,7 @@ class MusicLoader(private val context: Context) {
private fun buildSelector() { private fun buildSelector() {
val blacklistDatabase = BlacklistDatabase.getInstance(context) val blacklistDatabase = BlacklistDatabase.getInstance(context)
val paths = blacklistDatabase.getPaths() val paths = blacklistDatabase.readPaths()
for (path in paths) { for (path in paths) {
selector += " AND ${Media.DATA} NOT LIKE ?" selector += " AND ${Media.DATA} NOT LIKE ?"
@ -267,5 +267,7 @@ class MusicLoader(private val context: Context) {
genres.add(unknownGenre) genres.add(unknownGenre)
} }
genres.removeAll { it.songs.isEmpty() }
} }
} }

View file

@ -8,7 +8,6 @@ import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.R
import org.oxycblt.auxio.logD import org.oxycblt.auxio.logD
import org.oxycblt.auxio.logE import org.oxycblt.auxio.logE
import org.oxycblt.auxio.music.Album 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.playback.state.PlaybackStateManager
import org.oxycblt.auxio.recycler.SortMode import org.oxycblt.auxio.recycler.SortMode
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.createToast
/** /**
* The ViewModel that provides a UI frontend for [PlaybackStateManager]. * 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. * Force save the current [PlaybackStateManager] state to the database.
* Called by SettingsListFragment. * Called by SettingsListFragment.
*/ */
fun savePlaybackState(context: Context) { fun savePlaybackState(context: Context, onDone: () -> Unit) {
viewModelScope.launch { viewModelScope.launch {
playbackManager.saveStateToDatabase(context) playbackManager.saveStateToDatabase(context)
context.getString(R.string.debug_state_saved).createToast(context) onDone()
} }
} }

View file

@ -16,6 +16,7 @@ import org.oxycblt.auxio.recycler.DisplayMode
import org.oxycblt.auxio.settings.blacklist.BlacklistDialog import org.oxycblt.auxio.settings.blacklist.BlacklistDialog
import org.oxycblt.auxio.settings.ui.AccentDialog import org.oxycblt.auxio.settings.ui.AccentDialog
import org.oxycblt.auxio.ui.Accent import org.oxycblt.auxio.ui.Accent
import org.oxycblt.auxio.ui.createToast
/** /**
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat]. * The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat].
@ -126,7 +127,9 @@ class SettingsListFragment : PreferenceFragmentCompat() {
SettingsManager.Keys.KEY_SAVE_STATE -> { SettingsManager.Keys.KEY_SAVE_STATE -> {
onPreferenceClickListener = Preference.OnPreferenceClickListener { onPreferenceClickListener = Preference.OnPreferenceClickListener {
playbackModel.savePlaybackState(requireContext()) playbackModel.savePlaybackState(requireContext()) {
getString(R.string.debug_state_saved).createToast(requireContext())
}
true true
} }
} }

View file

@ -1,5 +1,7 @@
package org.oxycblt.auxio.settings.blacklist package org.oxycblt.auxio.settings.blacklist
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.view.LayoutInflater import android.view.LayoutInflater
@ -8,6 +10,7 @@ import android.view.ViewGroup
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.getCustomView 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.internal.list.DialogRecyclerView
import com.afollestad.materialdialogs.utils.invalidateDividers import com.afollestad.materialdialogs.utils.invalidateDividers
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.oxycblt.auxio.MainActivity
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentBlacklistBinding import org.oxycblt.auxio.databinding.FragmentBlacklistBinding
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.createToast import org.oxycblt.auxio.ui.createToast
import java.io.File import java.io.File
import kotlin.system.exitProcess
/**
* Dialog that manages the currently excluded directories.
* @author OxygenCobalt
*/
class BlacklistDialog : BottomSheetDialogFragment() { 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 override fun getTheme() = R.style.Theme_BottomSheetFix
@ -48,6 +62,14 @@ class BlacklistDialog : BottomSheetDialogFragment() {
dismiss() dismiss()
} }
binding.blacklistConfirm.setOnClickListener {
if (blacklistModel.modified) {
saveAndRestart()
} else {
dismiss()
}
}
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
blacklistModel.paths.observe(viewLifecycleOwner) { paths -> blacklistModel.paths.observe(viewLifecycleOwner) { paths ->
@ -59,6 +81,13 @@ class BlacklistDialog : BottomSheetDialogFragment() {
return binding.root 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() { private fun showFileDialog() {
MaterialDialog(requireActivity()).show { MaterialDialog(requireActivity()).show {
positiveButton(R.string.label_add) { 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" // to be excluded, as that would lead to the user being stuck at the "No Music Found"
// screen. // screen.
if (path == Environment.getExternalStorageDirectory().absolutePath) { if (path == Environment.getExternalStorageDirectory().absolutePath) {
getString(R.string.error_folder_would_brick_app) getString(R.string.error_folder_would_brick_app).createToast(requireContext())
.createToast(requireContext())
return return
} }
@ -104,4 +132,23 @@ class BlacklistDialog : BottomSheetDialogFragment() {
blacklistModel.addPath(path) 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)
}
} }

View file

@ -5,6 +5,10 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemBlacklistEntryBinding import org.oxycblt.auxio.databinding.ItemBlacklistEntryBinding
import org.oxycblt.auxio.ui.inflater import org.oxycblt.auxio.ui.inflater
/**
* Adapter that shows the blacklist entries and their "Clear" button.
* @author OxygenCobalt
*/
class BlacklistEntryAdapter( class BlacklistEntryAdapter(
private val onClear: (String) -> Unit private val onClear: (String) -> Unit
) : RecyclerView.Adapter<BlacklistEntryAdapter.ViewHolder>() { ) : RecyclerView.Adapter<BlacklistEntryAdapter.ViewHolder>() {

View file

@ -1,26 +1,92 @@
package org.oxycblt.auxio.settings.blacklist package org.oxycblt.auxio.settings.blacklist
import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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>()) private val mPaths = MutableLiveData(mutableListOf<String>())
val paths: LiveData<MutableList<String>> get() = mPaths val paths: LiveData<MutableList<String>> get() = mPaths
fun addPath(path: String) { private val blacklistDatabase = BlacklistDatabase.getInstance(context)
if (mPaths.value!!.contains(path)) { var modified = false
return 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!!.add(path)
mPaths.value = mPaths.value 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) { fun removePath(path: String) {
mPaths.value!!.remove(path) mPaths.value!!.remove(path)
mPaths.value = mPaths.value 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
}
} }
} }

View file

@ -92,7 +92,7 @@ class SongsFragment : Fragment() {
* Perform the (Frustratingly Long and Complicated) FastScrollerView setup. * Perform the (Frustratingly Long and Complicated) FastScrollerView setup.
*/ */
private fun FastScrollerView.setup(recycler: RecyclerView, thumb: CobaltScrollThumb) { 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. // API 22 and below don't support the state color, so just use the accent.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
@ -104,38 +104,36 @@ class SongsFragment : Fragment() {
{ pos -> { pos ->
val char = musicStore.songs[pos].name.first val char = musicStore.songs[pos].name.first
FastScrollItemIndicator.Text(
// Use "#" if the character is a digit, also has the nice side-effect of // Use "#" if the character is a digit, also has the nice side-effect of
// truncating extra numbers. // truncating extra numbers.
if (char.isDigit()) { if (char.isDigit()) "#" else char.toString()
FastScrollItemIndicator.Text("#") )
} else {
FastScrollItemIndicator.Text(char.toString())
}
}, },
null, false null, false
) )
showIndicator = { _, i, total -> showIndicator = { _, i, total ->
if (concatInterval == -1) { if (truncateInterval == -1) {
// If the scroller size is too small to contain all the entries, truncate entries // If the scroller size is too small to contain all the entries, truncate entries
// so that the fast scroller entries fit. // so that the fast scroller entries fit.
val maxEntries = (height / (indicatorTextSize + textPadding)) val maxEntries = (height / (indicatorTextSize + textPadding))
if (total > maxEntries.toInt()) { if (total > maxEntries.toInt()) {
concatInterval = ceil(total / maxEntries).toInt() truncateInterval = ceil(total / maxEntries).toInt()
check(concatInterval > 1) { check(truncateInterval > 1) {
"Needed to truncate, but concatInterval was 1 or lower anyway" "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 { } else {
concatInterval = 1 truncateInterval = 1
} }
} }
// Any items that need to be truncated will be hidden // Any items that need to be truncated will be hidden
(i % concatInterval) == 0 (i % truncateInterval) == 0
} }
addIndicatorCallback { _, _, pos -> addIndicatorCallback { _, _, pos ->

View file

@ -3,6 +3,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> 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 <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -16,9 +20,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/inter_exbold" android:fontFamily="@font/inter_exbold"
android:textAlignment="viewStart"
android:padding="@dimen/padding_medium" android:padding="@dimen/padding_medium"
android:text="@string/setting_content_blacklist" android:text="@string/setting_content_blacklist"
android:textAlignment="viewStart"
android:textColor="?attr/colorPrimary" android:textColor="?attr/colorPrimary"
android:textSize="@dimen/text_size_toolbar_header" android:textSize="@dimen/text_size_toolbar_header"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -58,7 +62,7 @@
android:id="@+id/blacklist_confirm" android:id="@+id/blacklist_confirm"
style="@style/Widget.Button.Dialog" style="@style/Widget.Button.Dialog"
android:layout_marginEnd="@dimen/margin_medium" 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_constraintBottom_toBottomOf="@+id/blacklist_cancel"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/blacklist_cancel" /> app:layout_constraintTop_toTopOf="@+id/blacklist_cancel" />
@ -74,5 +78,5 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</layout> </layout>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <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_channel_name">Music Playback</string> <string name="info_channel_name">Music Playback</string>
@ -51,7 +52,7 @@
<string name="label_author">Developed by OxygenCobalt</string> <string name="label_author">Developed by OxygenCobalt</string>
<string name="label_add">Add</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> <string name="label_empty_blacklist">No excluded folders</string>
<!-- Settings namespace | Settings-related labels --> <!-- Settings namespace | Settings-related labels -->

View file

@ -15,7 +15,6 @@ Here are the music formats that Auxio supports, as per the [Supported ExoPlayer
| MKA | ✅ | | | MKA | ✅ | |
| OGG | ✅ | Containing Vorbis, Opus, and FLAC | | OGG | ✅ | Containing Vorbis, Opus, and FLAC |
| WAV | ✅ | | | WAV | ✅ | |
| MPEG_TS | ✅ | | | MPEG | ✅ | |
| MPEG_TS | ✅ | |
| AAC | ✅ | | | AAC | ✅ | |
| FLAC | ❌ | Auxio must be patched with the [FLAC Extension](https://github.com/google/ExoPlayer/tree/release-v2/extensions/flac) | | FLAC | ❌ | Auxio must be patched with the [FLAC Extension](https://github.com/google/ExoPlayer/tree/release-v2/extensions/flac) |