From 3b72a207125ff889a8e257bc86c95b5909d7606d Mon Sep 17 00:00:00 2001 From: ZakirSheikh Date: Tue, 8 Aug 2023 11:13:17 +0530 Subject: [PATCH] [FEATURE] Blacklist Functionality Added - Users can now easily blacklist a folder by long-pressing it in the FolderView. - A Blacklist dialog has been added for easier management. [FEATURE] Trashcan Feature Implemented - Users on Android 11 and above can now toggle the trashcan feature on/off. [FEATURE] Fetch Artwork from MediaStore Toggle Added - This feature is currently under development and requires further polishing. [FEATURE] Recent Playlist Size Customization - Users can now specify the maximum number of tracks that the recent playlist can hold. This commit fixes #32 #28 #27 and partially #25 --- .../main/java/com/prime/media/MainActivity.kt | 42 +++-- .../prime/media/core/compose/Preferences.kt | 150 ++++++++++++++++++ .../prime/media/core/db/ContentResolver.kt | 64 ++++++++ .../com/prime/media/core/playback/Playback.kt | 12 +- .../com/prime/media/directory/store/Audios.kt | 15 +- .../prime/media/directory/store/Folders.kt | 51 +++++- .../java/com/prime/media/impl/Repository.kt | 97 +++++++++-- .../com/prime/media/impl/SettingsViewModel.kt | 96 +++++++---- .../com/prime/media/settings/Blacklist.kt | 135 ++++++++++++++++ .../java/com/prime/media/settings/Settings.kt | 84 ++++++++++ .../com/prime/media/settings/ViewState.kt | 15 +- app/src/main/res/values/strings.xml | 85 +++++++++- 12 files changed, 766 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/com/prime/media/core/compose/Preferences.kt create mode 100644 app/src/main/java/com/prime/media/settings/Blacklist.kt diff --git a/app/src/main/java/com/prime/media/MainActivity.kt b/app/src/main/java/com/prime/media/MainActivity.kt index 82b55d8e..d45d7f0a 100644 --- a/app/src/main/java/com/prime/media/MainActivity.kt +++ b/app/src/main/java/com/prime/media/MainActivity.kt @@ -52,6 +52,7 @@ import com.prime.media.core.db.findAudio import com.prime.media.core.db.toMediaItem import com.prime.media.core.playback.Remote import com.primex.core.MetroGreen +import com.primex.core.OrientRed import com.primex.core.Text import com.primex.preferences.Key import com.primex.preferences.Preferences @@ -336,21 +337,34 @@ class MainActivity : ComponentActivity(), SystemFacade { } override fun launchEqualizer(id: Int) { - if (id == AudioEffect.ERROR_BAD_VALUE) { - Toast.makeText(this, "No Session Id", Toast.LENGTH_LONG).show(); - return - } - val res = kotlin.runCatching { - startActivity( - Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { - putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName) - putExtra(AudioEffect.EXTRA_AUDIO_SESSION, id) - putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC); - } - ) + lifecycleScope.launch { + if (id == AudioEffect.ERROR_BAD_VALUE) + return@launch show(R.string.error_msg, R.string.error) + val result = kotlin.runCatching { + startActivity( + Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { + putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName) + putExtra(AudioEffect.EXTRA_AUDIO_SESSION, id) + putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC); + } + ) + } + if (!result.isFailure) + return@launch + val res = channel.show( + message = R.string.equalizer_3rd_party_not_found_msg, + action = R.string.launch, + accent = Color.OrientRed, + duration = Duration.Short + ) + if (res != Channel.Result.ActionPerformed) + return@launch + runCatching { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse("market://search?q=equalizer") + startActivity(intent) + } } - if (res.exceptionOrNull() is ActivityNotFoundException) - Toast.makeText(this, "There is no equalizer", Toast.LENGTH_SHORT).show(); } @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) diff --git a/app/src/main/java/com/prime/media/core/compose/Preferences.kt b/app/src/main/java/com/prime/media/core/compose/Preferences.kt new file mode 100644 index 00000000..82ac40c4 --- /dev/null +++ b/app/src/main/java/com/prime/media/core/compose/Preferences.kt @@ -0,0 +1,150 @@ +package com.prime.media.core.compose + +import android.R +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Slider +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.primex.core.composableOrNull +import com.primex.core.rememberState +import com.primex.material2.Label +import com.primex.material2.Preference +import com.primex.material2.Text + + +@Composable +private fun TextButtons( + modifier: Modifier = Modifier, + onConfirmClick: () -> Unit, + onCancelClick: () -> Unit +) { + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton(onClick = onCancelClick) { + Label(text = androidx.compose.ui.res.stringResource(id = R.string.cancel)) + } + + TextButton(onClick = onConfirmClick) { + Label(text = androidx.compose.ui.res.stringResource(id = R.string.ok)) + } + } +} + +@Composable +fun SliderPreference2( + title: CharSequence, + defaultValue: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier = Modifier, + steps: Int = 0, + enabled: Boolean = true, + singleLineTitle: Boolean = true, + iconSpaceReserved: Boolean = true, + icon: ImageVector? = null, + summery: CharSequence? = null, + forceVisible: Boolean = false, + iconChange: ImageVector? = null, + preview: CharSequence? = null, + valueRange: ClosedFloatingPointRange = 0f..1f, +) { + + val revealable = + @Composable { + val startPadding = (if (iconSpaceReserved) 24.dp + 16.dp else 0.dp) + 8.dp + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = startPadding) + ) { + // place slider + var value by rememberState(initial = defaultValue) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + if (iconChange != null) + Icon( + imageVector = iconChange, + contentDescription = null, + ) + Slider( + value = value, + onValueChange = { + value = it + }, + valueRange = valueRange, + steps = steps, + modifier = Modifier.weight(1f) + ) + if (iconChange != null) { + Icon( + imageVector = iconChange, + contentDescription = null, + modifier = Modifier.scale(1.5f) + ) + } + } + + val manager = LocalFocusManager.current + val onCancelClick = { + if (!forceVisible) + manager.clearFocus(true) + } + val onConfirmClick = { + if (!forceVisible) + manager.clearFocus(true) + onValueChange(value) + } + TextButtons( + onCancelClick = onCancelClick, + onConfirmClick = onConfirmClick, + modifier = Modifier.fillMaxWidth() + ) + } + } + + Preference( + modifier = modifier, + title = title, + enabled = enabled, + singleLineTitle = singleLineTitle, + iconSpaceReserved = iconSpaceReserved, + icon = icon, + forceVisible = forceVisible, + summery = summery, + widget = composableOrNull(preview != null) { + Text( + text = preview ?: "", + style = MaterialTheme.typography.body2, + fontWeight = FontWeight.Bold, + modifier = Modifier + .size(60.dp) + .wrapContentSize(Alignment.Center), + textAlign = TextAlign.Center + ) + }, + revealable = revealable + ) +} diff --git a/app/src/main/java/com/prime/media/core/db/ContentResolver.kt b/app/src/main/java/com/prime/media/core/db/ContentResolver.kt index 874f33c0..499259bf 100644 --- a/app/src/main/java/com/prime/media/core/db/ContentResolver.kt +++ b/app/src/main/java/com/prime/media/core/db/ContentResolver.kt @@ -876,4 +876,68 @@ suspend fun ContentResolver.delete(activity: ComponentActivity, vararg uri: Uri) // Launch the activity for result using the IntentSenderRequest object launcher.launch(intentSenderRequest) } +} + +/** + * A simple extension method that trashes files insetead of deleting them. + * @see delete + */ +@RequiresApi(Build.VERSION_CODES.R) +suspend fun ContentResolver.trash(activity: ComponentActivity, vararg uri: Uri): Int { + if (uri.isEmpty()) return -1 // error + return suspendCoroutine { continuation -> + // Create a lazy ActivityResultLauncher object + var launcher: ActivityResultLauncher? = null + // Assign result to launcher in such a way tha it allows us to + // unregister later. + val contract = ActivityResultContracts.StartIntentSenderForResult() + launcher = activity.registerActivityResultLauncher(contract) { + // unregister launcher + launcher?.unregister() + // user cancelled + if (it.resultCode == Activity.RESULT_CANCELED) { + continuation.resume(-3 /*cancelled*/) + return@registerActivityResultLauncher + } + // some unknown error occurred + if (it.resultCode != Activity.RESULT_OK) { + continuation.resume(-1 /*Error*/) + return@registerActivityResultLauncher + } + // construct which ids have been removed. + val ids = uri.joinToString(", ") { + "${ContentUris.parseId(it)}" + } + // assuming that all uri's are content uris. + val parent = ContentUris.removeId(uri[0]) + // check how many are still there; maybe some error occurred while deleting some files. + val projection = arrayOf(MediaStore.MediaColumns._ID) + val count = + query( + parent, + projection, + "${MediaStore.MediaColumns._ID} IN ($ids)", + null, + null + ).use { it?.count ?: -1 } + // resume with how many files have been trashed or error code. + continuation.resume(if (count > 0) uri.size - count else if (count == 0) uri.size else count) + } + val request = MediaStore.createTrashRequest(this, uri.toList(), true).intentSender + // Create an IntentSenderRequest object from the IntentSender object + val intentSenderRequest = IntentSenderRequest.Builder(request).build() + // Launch the activity for result using the IntentSenderRequest object + launcher.launch(intentSenderRequest) + } +} + +/** + * A simple extension method that trashes files instead of deleting them. + * @see delete + */ +@RequiresApi(Build.VERSION_CODES.R) +fun ContentResolver.trash(activity: Activity, vararg uri: Uri): Int { + val deleteRequest = MediaStore.createTrashRequest(this, uri.toList(), true).intentSender + activity.startIntentSenderForResult(deleteRequest, 100, null, 0, 0, 0) + return -2 // dialog is about to be shown. } \ No newline at end of file diff --git a/app/src/main/java/com/prime/media/core/playback/Playback.kt b/app/src/main/java/com/prime/media/core/playback/Playback.kt index 045448c6..25e9da53 100644 --- a/app/src/main/java/com/prime/media/core/playback/Playback.kt +++ b/app/src/main/java/com/prime/media/core/playback/Playback.kt @@ -86,11 +86,13 @@ private val PREF_KEY_ORDERS = stringPreferenceKey( } } ) +private val PREF_KEY_RECENT_PLAYLIST_LIMIT = + intPreferenceKey( "_max_recent_size", defaultValue = 50) /** * A simple extension fun that adds items to recent. */ -private suspend fun Playlists.addToRecent(item: MediaItem) { +private suspend fun Playlists.addToRecent(item: MediaItem, limit: Long) { val playlistId = get(PLAYLIST_RECENT)?.id ?: insert(Playlist(name = PLAYLIST_RECENT)) @@ -114,8 +116,6 @@ private suspend fun Playlists.addToRecent(item: MediaItem) { } else -> { - // check the limit in this case - val limit = 200L // delete above member delete(playlistId, limit) insert(Member(item, playlistId, 0)) @@ -167,6 +167,9 @@ class Playback : MediaLibraryService(), Callback, Player.Listener { * The root of the playing queue */ const val ROOT_QUEUE = com.prime.media.core.playback.ROOT_QUEUE + + @JvmField + val PREF_KEY_RECENT_PLAYLIST_LIMIT = com.prime.media.core.playback.PREF_KEY_RECENT_PLAYLIST_LIMIT } /** @@ -293,7 +296,8 @@ class Playback : MediaLibraryService(), Callback, Player.Listener { // save current index in preference preferences[PREF_KEY_INDEX] = player.currentMediaItemIndex if (mediaItem != null) { - GlobalScope.launch { playlists.addToRecent(mediaItem) } + val limit = preferences.value(PREF_KEY_RECENT_PLAYLIST_LIMIT) + GlobalScope.launch { playlists.addToRecent(mediaItem, limit.toLong()) } session.notifyChildrenChanged(ROOT_QUEUE, 0, null) } } diff --git a/app/src/main/java/com/prime/media/directory/store/Audios.kt b/app/src/main/java/com/prime/media/directory/store/Audios.kt index 46674d22..40bae15e 100644 --- a/app/src/main/java/com/prime/media/directory/store/Audios.kt +++ b/app/src/main/java/com/prime/media/directory/store/Audios.kt @@ -59,6 +59,7 @@ import com.prime.media.core.db.albumUri import com.prime.media.core.db.key import com.prime.media.core.db.toMediaItem import com.prime.media.core.util.toMember +import com.prime.media.settings.Settings import com.primex.core.* import com.primex.material2.* import dagger.hilt.android.lifecycle.HiltViewModel @@ -70,6 +71,8 @@ import javax.inject.Inject import kotlin.collections.ArrayList import kotlin.random.Random.Default.nextInt import com.primex.core.Text +import com.primex.preferences.Preferences +import com.primex.preferences.value private const val TAG = "AudiosViewModel" @@ -84,6 +87,7 @@ class AudiosViewModel @Inject constructor( private val repository: Repository, private val toaster: Channel, private val remote: Remote, + private val preferences: Preferences ) : DirectoryViewModel