diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 051b31d24..1aab4d51e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,7 +22,7 @@ diff --git a/app/src/main/java/org/oxycblt/auxio/database/BlacklistDatabase.kt b/app/src/main/java/org/oxycblt/auxio/database/BlacklistDatabase.kt index ab1365cf0..b200671a4 100644 --- a/app/src/main/java/org/oxycblt/auxio/database/BlacklistDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/database/BlacklistDatabase.kt @@ -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) { 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 { + fun readPaths(): List { assertBackgroundThread() val paths = mutableListOf() @@ -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" diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt index f463fcb39..21a5bf1ab 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt @@ -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() } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index c85e4de59..0b6e3386c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -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() } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt index efbbcc730..c33c9982c 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -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 } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistDialog.kt index 8831ea0eb..e474db850 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistDialog.kt @@ -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) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistEntryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistEntryAdapter.kt index 2ce4a50e6..f335fb3d7 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistEntryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistEntryAdapter.kt @@ -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() { diff --git a/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistViewModel.kt b/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistViewModel.kt index ba8ab7878..b5bbe1e02 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/blacklist/BlacklistViewModel.kt @@ -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()) val paths: LiveData> get() = mPaths + 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 - } + 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 create(modelClass: Class): T { + check(modelClass.isAssignableFrom(BlacklistViewModel::class.java)) { + "BlacklistViewModel.Factory does not support this class" + } + + @Suppress("UNCHECKED_CAST") + return BlacklistViewModel(context) as T + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt index 2109eb2d2..0c380a8e6 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt @@ -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 - // 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()) - } + FastScrollItemIndicator.Text( + // Use "#" if the character is a digit, also has the nice side-effect of + // truncating extra numbers. + 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 -> diff --git a/app/src/main/res/layout/fragment_blacklist.xml b/app/src/main/res/layout/fragment_blacklist.xml index 9a38bd786..3864c614f 100644 --- a/app/src/main/res/layout/fragment_blacklist.xml +++ b/app/src/main/res/layout/fragment_blacklist.xml @@ -3,76 +3,80 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - + android:layout_height="match_parent"> - + android:layout_height="match_parent" + android:background="@color/background" + android:orientation="vertical" + android:paddingBottom="@dimen/margin_medium" + android:theme="@style/Theme.Neutral"> - + - + -