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
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">

View file

@ -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"

View file

@ -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() }
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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>() {

View file

@ -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
}
}
}

View file

@ -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 ->

View file

@ -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>

View file

@ -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 -->

View file

@ -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) |