From f3de9f7a3dba155d7f6fc39e9c2adc579ef8dc78 Mon Sep 17 00:00:00 2001 From: Karan Sharma Date: Fri, 9 Dec 2022 11:45:24 +0530 Subject: [PATCH] Fix media notification controls configuration to support 3 icons --- CHANGELOG.md | 2 + .../settings/MediaActionTouchCallback.kt | 71 +++++ .../MediaNotificationControlsFragment.kt | 254 ++++++++++++++++++ .../settings/PlaybackSettingsFragment.kt | 33 +-- .../res/layout/adapter_media_action_item.xml | 51 ++++ .../res/layout/adapter_media_action_title.xml | 28 ++ .../fragment_media_notification_controls.xml | 30 +++ .../pocketcasts/compose/components/Dialog.kt | 119 -------- .../compose/components/Settings.kt | 31 --- .../src/main/res/values/strings.xml | 6 +- .../pocketcasts/preferences/Settings.kt | 27 +- .../pocketcasts/preferences/SettingsImpl.kt | 24 +- .../playback/MediaSessionManager.kt | 2 +- 13 files changed, 476 insertions(+), 202 deletions(-) create mode 100644 modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/MediaActionTouchCallback.kt create mode 100644 modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/MediaNotificationControlsFragment.kt create mode 100644 modules/features/settings/src/main/res/layout/adapter_media_action_item.xml create mode 100644 modules/features/settings/src/main/res/layout/adapter_media_action_title.xml create mode 100644 modules/features/settings/src/main/res/layout/fragment_media_notification_controls.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a525b35d34..6e347a03886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ([#477](https://github.com/Automattic/pocket-casts-android/pull/477)). * Fix unable to permanently change "Skip back time" setting ([#632](https://github.com/Automattic/pocket-casts-android/pull/632)). + * Fix media notification controls configuration to support 3 icons + ([#641](https://github.com/Automattic/pocket-casts-android/pull/641)). 7.28 ----- diff --git a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/MediaActionTouchCallback.kt b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/MediaActionTouchCallback.kt new file mode 100644 index 00000000000..72a19a9b15c --- /dev/null +++ b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/MediaActionTouchCallback.kt @@ -0,0 +1,71 @@ +package au.com.shiftyjelly.pocketcasts.settings + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView + +class MediaActionTouchCallback( + private val listener: ItemTouchHelperAdapter +) : ItemTouchHelper.Callback() { + interface ItemTouchHelperAdapter { + fun onMediaActionItemMove(fromPosition: Int, toPosition: Int) + fun onMediaActionItemStartDrag(viewHolder: MediaActionAdapter.ItemViewHolder) + fun onMediaActionItemTouchHelperFinished(position: Int) + } + + interface ItemTouchHelperViewHolder { + fun onItemDrag() + fun onItemSwipe() + fun onItemClear() + } + + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + return if (viewHolder is MediaActionAdapter.ItemViewHolder) { + val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN + ItemTouchHelper.SimpleCallback.makeMovementFlags(dragFlags, 0) + } else { + ItemTouchHelper.SimpleCallback.makeMovementFlags(0, 0) + } + } + + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + return if (viewHolder is MediaActionAdapter.ItemViewHolder && target is MediaActionAdapter.ItemViewHolder && + viewHolder.bindingAdapterPosition != RecyclerView.NO_POSITION && target.bindingAdapterPosition != RecyclerView.NO_POSITION + ) { + listener.onMediaActionItemMove(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + true + } else { + false + } + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + if (viewHolder is ItemTouchHelperViewHolder) { + when (actionState) { + ItemTouchHelper.ACTION_STATE_DRAG -> viewHolder.onItemDrag() + ItemTouchHelper.ACTION_STATE_SWIPE -> viewHolder.onItemSwipe() + } + } + + super.onSelectedChanged(viewHolder, actionState) + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + + if (viewHolder is ItemTouchHelperViewHolder) { + viewHolder.onItemClear() + } + listener.onMediaActionItemTouchHelperFinished(viewHolder.bindingAdapterPosition) + } + + override fun isItemViewSwipeEnabled(): Boolean { + return false + } + + override fun isLongPressDragEnabled(): Boolean { + return true + } +} diff --git a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/MediaNotificationControlsFragment.kt b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/MediaNotificationControlsFragment.kt new file mode 100644 index 00000000000..8e4fe5007b1 --- /dev/null +++ b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/MediaNotificationControlsFragment.kt @@ -0,0 +1,254 @@ +package au.com.shiftyjelly.pocketcasts.settings + +import android.animation.AnimatorSet +import android.animation.ArgbEvaluator +import android.animation.ObjectAnimator +import android.animation.PropertyValuesHolder +import android.graphics.Color +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import au.com.shiftyjelly.pocketcasts.localization.R +import au.com.shiftyjelly.pocketcasts.models.entity.Playable +import au.com.shiftyjelly.pocketcasts.preferences.Settings +import au.com.shiftyjelly.pocketcasts.settings.databinding.AdapterMediaActionItemBinding +import au.com.shiftyjelly.pocketcasts.settings.databinding.AdapterMediaActionTitleBinding +import au.com.shiftyjelly.pocketcasts.settings.databinding.FragmentMediaNotificationControlsBinding +import au.com.shiftyjelly.pocketcasts.ui.extensions.getThemeColor +import au.com.shiftyjelly.pocketcasts.ui.helper.ColorUtils +import au.com.shiftyjelly.pocketcasts.ui.helper.FragmentHostListener +import au.com.shiftyjelly.pocketcasts.ui.theme.ThemeColor +import au.com.shiftyjelly.pocketcasts.utils.extensions.dpToPx +import au.com.shiftyjelly.pocketcasts.views.extensions.setRippleBackground +import au.com.shiftyjelly.pocketcasts.views.fragments.BaseFragment +import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber +import java.util.Collections +import javax.inject.Inject +import au.com.shiftyjelly.pocketcasts.settings.R as SR +import au.com.shiftyjelly.pocketcasts.ui.R as UR + +@AndroidEntryPoint +class MediaNotificationControlsFragment : BaseFragment(), MediaActionTouchCallback.ItemTouchHelperAdapter { + private var items = emptyList() + + @Inject + lateinit var settings: Settings + + private lateinit var itemTouchHelper: ItemTouchHelper + private val adapter = MediaActionAdapter(dragListener = this::onMediaActionItemStartDrag) + private val mediaTitle = MediaActionTitle(R.string.settings_prioritize_your_notification_actions, R.string.settings_your_top_actions_will_be_available_in_your_notif_and_android_auto_player) + private val otherActionsTitle = MediaActionTitle(R.string.settings_other_media_actions) + private var binding: FragmentMediaNotificationControlsBinding? = null + private var dragStartPosition: Int? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = FragmentMediaNotificationControlsBinding.inflate(inflater, container, false) + return binding?.root + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val binding = binding ?: return + + val toolbar = binding.toolbar + toolbar.setTitle(R.string.settings_rearrange_media_actions) + toolbar.setTitleTextColor(toolbar.context.getThemeColor(UR.attr.secondary_text_01)) + toolbar.setNavigationOnClickListener { + (activity as? FragmentHostListener)?.closeModal(this) + } + toolbar.navigationIcon?.setTint(ThemeColor.secondaryIcon01(theme.activeTheme)) + + val backgroundColor = view.context.getThemeColor(UR.attr.primary_ui_01) + + view.setBackgroundColor(backgroundColor) + adapter.selectedBackground = ColorUtils.calculateCombinedColor(backgroundColor, view.context.getThemeColor(UR.attr.primary_ui_02_selected)) + + val recyclerView = binding.recyclerView + recyclerView.adapter = adapter + (recyclerView.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false + (recyclerView.itemAnimator as? SimpleItemAnimator)?.changeDuration = 0 + + val callback = MediaActionTouchCallback(listener = this) + itemTouchHelper = ItemTouchHelper(callback) + itemTouchHelper.attachToRecyclerView(recyclerView) + + viewLifecycleOwner.lifecycleScope.launchWhenStarted { + settings.defaultMediaNotificationControlsFlow.collect { + val itemsPlusTitles = mutableListOf() + itemsPlusTitles.addAll(it) + itemsPlusTitles.add(3, otherActionsTitle) + itemsPlusTitles.add(0, mediaTitle) + items = itemsPlusTitles + adapter.submitList(items) + } + } + } + + override fun onMediaActionItemMove(fromPosition: Int, toPosition: Int) { + val listData = items.toMutableList() + + Timber.d("Swapping $fromPosition to $toPosition") + Timber.d("List: $listData") + + if (fromPosition < toPosition) { + for (index in fromPosition until toPosition) { + Collections.swap(listData, index, index + 1) + } + } else { + for (index in fromPosition downTo toPosition + 1) { + Collections.swap(listData, index, index - 1) + } + } + + // Make sure the titles are in the right spot + listData.remove(otherActionsTitle) + listData.remove(mediaTitle) + listData.add(3, otherActionsTitle) + listData.add(0, mediaTitle) + + adapter.submitList(listData) + items = listData.toList() + + Timber.d("Swapped: $items") + } + + override fun onMediaActionItemStartDrag(viewHolder: MediaActionAdapter.ItemViewHolder) { + dragStartPosition = viewHolder.bindingAdapterPosition + itemTouchHelper.startDrag(viewHolder) + } + + override fun onMediaActionItemTouchHelperFinished(position: Int) { + settings.setMediaNotificationControlItems(items.filterIsInstance().map { it.key }) + } +} + +data class MediaActionTitle(@StringRes val title: Int, @StringRes val subTitle: Int? = null) + +private val MEDIA_ACTION_ITEM_DIFF = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean { + return if (oldItem is Settings.MediaNotificationControls && newItem is Settings.MediaNotificationControls) { + oldItem.key == newItem.key + } else { + return oldItem == newItem + } + } + + override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean { + return true + } +} + +class MediaActionAdapter(val listener: ((Settings.MediaNotificationControls) -> Unit)? = null, val dragListener: ((ItemViewHolder) -> Unit)?) : ListAdapter(MEDIA_ACTION_ITEM_DIFF) { + var playable: Playable? = null + set(value) { + field = value + notifyDataSetChanged() + } + + var normalBackground = Color.TRANSPARENT + var selectedBackground = Color.BLACK + + class TitleViewHolder(val binding: AdapterMediaActionTitleBinding) : RecyclerView.ViewHolder(binding.root) + + inner class ItemViewHolder(val binding: AdapterMediaActionItemBinding) : RecyclerView.ViewHolder(binding.root), MediaActionTouchCallback.ItemTouchHelperViewHolder { + + override fun onItemDrag() { + AnimatorSet().apply { + val backgroundView = itemView + + val elevation = ObjectAnimator.ofPropertyValuesHolder(backgroundView, PropertyValuesHolder.ofFloat(View.TRANSLATION_Z, 16.dpToPx(backgroundView.resources.displayMetrics).toFloat())) + + val color = ObjectAnimator.ofInt(backgroundView, "backgroundColor", normalBackground, selectedBackground) + color.setEvaluator(ArgbEvaluator()) + + playTogether(elevation, color) + start() + } + } + + override fun onItemSwipe() { + } + + override fun onItemClear() { + AnimatorSet().apply { + val backgroundView = itemView + val elevation = ObjectAnimator.ofPropertyValuesHolder(backgroundView, PropertyValuesHolder.ofFloat(View.TRANSLATION_Z, 0.toFloat())) + + backgroundView.setRippleBackground(false) + play(elevation) + start() + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + SR.layout.adapter_media_action_item -> { + val binding = AdapterMediaActionItemBinding.inflate(inflater, parent, false) + ItemViewHolder(binding) + } + SR.layout.adapter_media_action_title -> { + val binding = AdapterMediaActionTitleBinding.inflate(inflater, parent, false) + TitleViewHolder(binding) + } + else -> throw IllegalStateException("Unknown view type in shelf") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = getItem(position) + + if (item is Settings.MediaNotificationControls && holder is ItemViewHolder) { + val binding = holder.binding + + binding.lblTitle.setText(item.controlName) + binding.imgIcon.setImageResource(item.iconRes) + + if (listener != null) { + holder.itemView.setOnClickListener { listener.invoke(item) } + } + + binding.dragHandle.setOnTouchListener { _, event -> + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + dragListener?.invoke(holder) + } + false + } + } else if (item is MediaActionTitle && holder is TitleViewHolder) { + val binding = holder.binding + + binding.lblTitle.setText(item.title) + + if (item.subTitle != null) { + binding.lblSubtitle.isVisible = true + holder.binding.lblSubtitle.setText(item.subTitle) + } + } + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is MediaActionTitle -> SR.layout.adapter_media_action_title + is Settings.MediaNotificationControls -> SR.layout.adapter_media_action_item + else -> throw IllegalStateException("Unknown item type in shelf") + } + } +} diff --git a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/PlaybackSettingsFragment.kt b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/PlaybackSettingsFragment.kt index dfd925bf28d..ca81e95c3e4 100644 --- a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/PlaybackSettingsFragment.kt +++ b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/PlaybackSettingsFragment.kt @@ -1,6 +1,5 @@ package au.com.shiftyjelly.pocketcasts.settings -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -40,7 +39,6 @@ import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground import au.com.shiftyjelly.pocketcasts.compose.bars.ThemedTopAppBar import au.com.shiftyjelly.pocketcasts.compose.components.DialogButtonState import au.com.shiftyjelly.pocketcasts.compose.components.DialogFrame -import au.com.shiftyjelly.pocketcasts.compose.components.SettingCheckBoxDialogRow import au.com.shiftyjelly.pocketcasts.compose.components.SettingRadioDialogRow import au.com.shiftyjelly.pocketcasts.compose.components.SettingRow import au.com.shiftyjelly.pocketcasts.compose.components.SettingRowToggle @@ -49,8 +47,8 @@ import au.com.shiftyjelly.pocketcasts.compose.theme import au.com.shiftyjelly.pocketcasts.images.R import au.com.shiftyjelly.pocketcasts.models.to.PodcastGrouping import au.com.shiftyjelly.pocketcasts.preferences.Settings -import au.com.shiftyjelly.pocketcasts.preferences.Settings.MediaNotificationControls import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager +import au.com.shiftyjelly.pocketcasts.ui.helper.FragmentHostListener import au.com.shiftyjelly.pocketcasts.utils.extensions.isPositive import au.com.shiftyjelly.pocketcasts.views.dialog.ConfirmationDialog import au.com.shiftyjelly.pocketcasts.views.fragments.BaseFragment @@ -137,14 +135,13 @@ class PlaybackSettingsFragment : BaseFragment() { } ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - MediaNotificationControls( - saved = settings.defaultMediaNotificationControlsFlow.collectAsState().value, - onSave = { - settings.setDefaultMediaNotificationControls(it) - } - ) - } + SettingRow( + primaryText = stringResource(LR.string.settings_media_notification_controls), + secondaryText = stringResource(LR.string.settings_customize_buttons_displayed_in_android_13_notification_and_android_auto), + modifier = Modifier.clickable { + (activity as? FragmentHostListener)?.addFragment(MediaNotificationControlsFragment()) + } + ) } SettingSection(heading = stringResource(LR.string.settings_general_player)) { @@ -295,20 +292,6 @@ class PlaybackSettingsFragment : BaseFragment() { }, ) - @Composable - fun MediaNotificationControls( - saved: List, - onSave: (List) -> Unit - ) = SettingCheckBoxDialogRow( - primaryText = stringResource(LR.string.settings_media_notification_controls), - secondaryText = stringResource(LR.string.settings_media_notification_controls_summary), - options = MediaNotificationControls.All, - maxOptions = MediaNotificationControls.MaxSelectedOptions, - savedOption = saved, - optionToLocalisedString = { getString(it.controlName) }, - onSave = onSave - ) - @Composable private fun SkipTime( primaryText: String, diff --git a/modules/features/settings/src/main/res/layout/adapter_media_action_item.xml b/modules/features/settings/src/main/res/layout/adapter_media_action_item.xml new file mode 100644 index 00000000000..57ad9d732d2 --- /dev/null +++ b/modules/features/settings/src/main/res/layout/adapter_media_action_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + \ No newline at end of file diff --git a/modules/features/settings/src/main/res/layout/adapter_media_action_title.xml b/modules/features/settings/src/main/res/layout/adapter_media_action_title.xml new file mode 100644 index 00000000000..466fa5fc8ac --- /dev/null +++ b/modules/features/settings/src/main/res/layout/adapter_media_action_title.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/modules/features/settings/src/main/res/layout/fragment_media_notification_controls.xml b/modules/features/settings/src/main/res/layout/fragment_media_notification_controls.xml new file mode 100644 index 00000000000..56ced5287fe --- /dev/null +++ b/modules/features/settings/src/main/res/layout/fragment_media_notification_controls.xml @@ -0,0 +1,30 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/Dialog.kt b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/Dialog.kt index ed04d36fdf6..15aa429af02 100644 --- a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/Dialog.kt +++ b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/Dialog.kt @@ -16,8 +16,6 @@ import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card -import androidx.compose.material.Checkbox -import androidx.compose.material.CheckboxDefaults import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.RadioButton @@ -29,7 +27,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview @@ -41,7 +38,6 @@ import au.com.shiftyjelly.pocketcasts.compose.AppTheme import au.com.shiftyjelly.pocketcasts.compose.preview.ThemePreviewParameterProvider import au.com.shiftyjelly.pocketcasts.compose.theme import au.com.shiftyjelly.pocketcasts.ui.theme.Theme -import okhttp3.internal.toImmutableList import java.util.* import au.com.shiftyjelly.pocketcasts.localization.R as LR @@ -272,95 +268,6 @@ fun ProgressDialog( } } -@Composable -fun CheckboxDialog( - title: String, - options: List>, - savedOption: List, - maxOptions: Int, - onSave: (List) -> Unit, - dismissDialog: () -> Unit, -) { - var selected by remember { mutableStateOf(savedOption) } - - DialogFrame( - title = title, - buttons = listOf( - DialogButtonState( - text = stringResource(LR.string.cancel), - onClick = dismissDialog - ), - DialogButtonState( - text = stringResource(LR.string.ok), - onClick = { - onSave(selected) - dismissDialog() - } - ) - ), - onDismissRequest = dismissDialog, - ) { - Column { - options.forEach { (item, itemLabel) -> - DialogCheckBox( - text = itemLabel, - selected = selected.contains(item), - enabled = maxOptions > selected.size || selected.contains(item), - onClick = { - selected = if (selected.contains(item)) { - selected.toMutableList().apply { - remove(item) - }.toImmutableList() - } else { - selected.toMutableList().apply { - add(item) - }.toImmutableList() - } - } - ) - } - } - } -} - -@Composable -fun DialogCheckBox( - text: String, - selected: Boolean, - enabled: Boolean, - onClick: () -> Unit -) { - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 48.dp) - .selectable( - selected = selected, - enabled = enabled, - role = Role.Checkbox, - onClick = onClick, - ) - ) { - Spacer(Modifier.width(24.dp)) - Checkbox( - checked = selected, - enabled = enabled, - onCheckedChange = null, - colors = CheckboxDefaults.colors( - disabledColor = Color.Gray - ) - ) - Spacer(Modifier.width(12.dp)) - TextP40( - text = text, - modifier = Modifier.padding(vertical = 12.dp) - ) - Spacer(Modifier.width(24.dp)) - } -} - @Composable private fun DialogFramePreview( theme: Theme.ThemeType = Theme.ThemeType.LIGHT, @@ -416,32 +323,6 @@ private fun RadioDialogPreview_light() = RadioDialogPreview(Theme.ThemeType.LIGH @Composable private fun RadioDialogPreview_dark() = RadioDialogPreview(Theme.ThemeType.DARK) -@Composable -private fun CheckboxDialogPreview(theme: Theme.ThemeType) { - AppTheme(theme) { - CheckboxDialog( - title = "Title", - options = listOf( - Pair("Star", stringResource(id = LR.string.settings_media_notification_controls_title_star)), - Pair("Archive", stringResource(id = LR.string.settings_media_notification_controls_title_archive)), - Pair("PlayNext", stringResource(id = LR.string.settings_media_notification_controls_title_play_next)) - ), - savedOption = listOf("Archive", "PlayNext"), - maxOptions = 2, - onSave = {}, - dismissDialog = {} - ) - } -} - -@Preview -@Composable -private fun CheckboxDialogPreview_light() = CheckboxDialogPreview(Theme.ThemeType.LIGHT) - -@Preview -@Composable -private fun CheckboxDialogPreview_dark() = CheckboxDialogPreview(Theme.ThemeType.DARK) - @Preview(showBackground = true) @Composable private fun ProgressDialogPreview( diff --git a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/Settings.kt b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/Settings.kt index b048ed26fe3..94980694207 100644 --- a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/Settings.kt +++ b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/components/Settings.kt @@ -104,37 +104,6 @@ fun SettingRadioDialogRow( } } -@Composable -fun SettingCheckBoxDialogRow( - primaryText: String, - modifier: Modifier = Modifier, - secondaryText: String? = null, - options: List, - savedOption: List, - maxOptions: Int = savedOption.size, - optionToLocalisedString: (T) -> String, - onSave: (List) -> Unit, -) { - - var showDialog by remember { mutableStateOf(false) } - SettingRow( - primaryText = primaryText, - secondaryText = secondaryText, - modifier = modifier.clickable { showDialog = true } - ) { - if (showDialog) { - CheckboxDialog( - title = primaryText, - options = options.map { Pair(it, optionToLocalisedString(it)) }, - savedOption = savedOption, - maxOptions = maxOptions, - onSave = onSave, - dismissDialog = { showDialog = false } - ) - } - } -} - /* * Click handling should be done in the modifier passed to this composable to ensure the * entire row is clickable. diff --git a/modules/services/localization/src/main/res/values/strings.xml b/modules/services/localization/src/main/res/values/strings.xml index 1fda693a037..9b4e2f51570 100644 --- a/modules/services/localization/src/main/res/values/strings.xml +++ b/modules/services/localization/src/main/res/values/strings.xml @@ -979,6 +979,7 @@ Select filters Choose podcasts close upgrade offer + Customize the buttons displayed in Android 13 playback notifications and Android Auto. Developer Clean up Are you sure you want to delete these downloaded files? @@ -1012,7 +1013,6 @@ Include starred Total Media notification controls - Choose two actions to be displayed in the Android 13 playback notification, Android Auto, and other places the custom media controls are available @string/archive @string/mark_as_played @string/play_next @@ -1037,6 +1037,7 @@ New episodes Open Player Automatically If on, the full-screen player will open when you start playing a podcast episode. + Other Media Action Intelligent playback resumption If on, Pocket Casts will go back a little in episodes you resume so you can catch up more comfortably. Podcast Artwork @@ -1053,6 +1054,8 @@ 1 podcast %d podcasts No podcasts selected + Prioritize your notification actions + Rearrange Media Actions Refresh all podcast artwork Pocket Casts is now refreshing all your podcast images, this will happen in the background and may take a little time. @string/ok @@ -1257,6 +1260,7 @@ Allow us to collect crash reports. Link your account to crashes Allow us to link your Pocket Casts account to crash reports. + Your top actions will be available in your notification and Android Auto player. @string/folders If you love podcasts half as much as we do, you probably have a lot of them. If you\'re a Pocket Casts Plus subscriber, you can now sort these into folders and file them into neat groups.\n\nThanks to your support, your Home Screen has never looked better! diff --git a/modules/services/preferences/src/main/java/au/com/shiftyjelly/pocketcasts/preferences/Settings.kt b/modules/services/preferences/src/main/java/au/com/shiftyjelly/pocketcasts/preferences/Settings.kt index ab1488ef7ba..a8369a5ff62 100644 --- a/modules/services/preferences/src/main/java/au/com/shiftyjelly/pocketcasts/preferences/Settings.kt +++ b/modules/services/preferences/src/main/java/au/com/shiftyjelly/pocketcasts/preferences/Settings.kt @@ -2,6 +2,7 @@ package au.com.shiftyjelly.pocketcasts.preferences import android.content.Context import android.net.Uri +import androidx.annotation.DrawableRes import androidx.annotation.StringRes import au.com.shiftyjelly.pocketcasts.models.to.PlaybackEffects import au.com.shiftyjelly.pocketcasts.models.to.PodcastGrouping @@ -13,6 +14,7 @@ import au.com.shiftyjelly.pocketcasts.utils.Util import io.reactivex.Observable import kotlinx.coroutines.flow.StateFlow import java.util.Date +import au.com.shiftyjelly.pocketcasts.images.R as IR import au.com.shiftyjelly.pocketcasts.localization.R as LR interface Settings { @@ -211,30 +213,35 @@ interface Settings { fun toIndex(): Int = options.indexOf(this) } - sealed class MediaNotificationControls(@StringRes val controlName: Int, val key: String) { + sealed class MediaNotificationControls(@StringRes val controlName: Int, @DrawableRes val iconRes: Int, val key: String) { companion object { val All - get() = listOf(Archive, MarkAsPlayed, PlayNext, PlaybackSpeed, Star) + get() = listOf(PlaybackSpeed, Star, MarkAsPlayed, PlayNext, Archive) + val items = All.associateBy { it.key } - const val MaxSelectedOptions = 2 + const val MAX_VISIBLE_OPTIONS = 3 private const val ARCHIVE_KEY = "default_media_control_archive" private const val MARK_AS_PLAYED_KEY = "default_media_control_mark_as_played" private const val PLAY_NEXT_KEY = "default_media_control_play_next_key" private const val PLAYBACK_SPEED_KEY = "default_media_control_playback_speed_key" private const val STAR_KEY = "default_media_control_star_key" + + fun itemForId(id: String): MediaNotificationControls? { + return items[id] + } } - object Archive : MediaNotificationControls(LR.string.archive, ARCHIVE_KEY) + object Archive : MediaNotificationControls(LR.string.archive, IR.drawable.ic_archive, ARCHIVE_KEY) - object MarkAsPlayed : MediaNotificationControls(LR.string.mark_as_played, MARK_AS_PLAYED_KEY) + object MarkAsPlayed : MediaNotificationControls(LR.string.mark_as_played, IR.drawable.ic_markasplayed, MARK_AS_PLAYED_KEY) - object PlayNext : MediaNotificationControls(LR.string.play_next, PLAY_NEXT_KEY) + object PlayNext : MediaNotificationControls(LR.string.play_next, com.google.android.gms.cast.framework.R.drawable.cast_ic_mini_controller_skip_next, PLAY_NEXT_KEY) - object PlaybackSpeed : MediaNotificationControls(LR.string.playback_speed, PLAYBACK_SPEED_KEY) + object PlaybackSpeed : MediaNotificationControls(LR.string.playback_speed, IR.drawable.auto_1x, PLAYBACK_SPEED_KEY) - object Star : MediaNotificationControls(LR.string.star, STAR_KEY) + object Star : MediaNotificationControls(LR.string.star, IR.drawable.ic_star, STAR_KEY) } sealed class AutoArchiveInactive(val timeSeconds: Int) { @@ -555,8 +562,8 @@ interface Settings { fun getAutoPlayNextEpisodeOnEmpty(): Boolean fun defaultShowArchived(): Boolean fun setDefaultShowArchived(value: Boolean) - fun defaultMediaNotificationControls(): List - fun setDefaultMediaNotificationControls(mediaNotificationControls: List) + fun getMediaNotificationControlItems(): List + fun setMediaNotificationControlItems(items: List) fun setMultiSelectItems(items: List) fun getMultiSelectItems(): List fun setLastPauseTime(date: Date) diff --git a/modules/services/preferences/src/main/java/au/com/shiftyjelly/pocketcasts/preferences/SettingsImpl.kt b/modules/services/preferences/src/main/java/au/com/shiftyjelly/pocketcasts/preferences/SettingsImpl.kt index 126bbeefc21..b2d299bec66 100644 --- a/modules/services/preferences/src/main/java/au/com/shiftyjelly/pocketcasts/preferences/SettingsImpl.kt +++ b/modules/services/preferences/src/main/java/au/com/shiftyjelly/pocketcasts/preferences/SettingsImpl.kt @@ -96,7 +96,7 @@ class SettingsImpl @Inject constructor( override val autoAddUpNextLimit = BehaviorRelay.create().apply { accept(getAutoAddUpNextLimit()) } override val defaultPodcastGroupingFlow = MutableStateFlow(defaultPodcastGrouping()) - override val defaultMediaNotificationControlsFlow = MutableStateFlow(defaultMediaNotificationControls()) + override val defaultMediaNotificationControlsFlow = MutableStateFlow(getMediaNotificationControlItems()) override val defaultShowArchivedFlow = MutableStateFlow(defaultShowArchived()) override val keepScreenAwakeFlow = MutableStateFlow(keepScreenAwake()) override val openPlayerAutomaticallyFlow = MutableStateFlow(openPlayerAutomatically()) @@ -1202,23 +1202,17 @@ class SettingsImpl @Inject constructor( defaultShowArchivedFlow.update { value } } - override fun defaultMediaNotificationControls(): List { - val selectedValue = MediaNotificationControls.All.map { mediaControl -> - val defaultValue = - (mediaControl == MediaNotificationControls.PlaybackSpeed || mediaControl == MediaNotificationControls.Star) - Pair(mediaControl, getBoolean(mediaControl.key, defaultValue)) - } + override fun getMediaNotificationControlItems(): List { + var items = getStringList("media_notification_controls_action") - return selectedValue.filter { (_, value) -> value }.map { (mediaControl, _) -> - mediaControl - } + if (items.isEmpty()) + items = MediaNotificationControls.All.map { it.key } + return items.mapNotNull { MediaNotificationControls.itemForId(it) } } - override fun setDefaultMediaNotificationControls(mediaNotificationControls: List) { - MediaNotificationControls.All.forEach { mediaControl -> - setBoolean(mediaControl.key, mediaNotificationControls.contains(mediaControl)) - } - defaultMediaNotificationControlsFlow.update { mediaNotificationControls } + override fun setMediaNotificationControlItems(items: List) { + setStringList("media_notification_controls_action", items) + defaultMediaNotificationControlsFlow.update { items.mapNotNull { MediaNotificationControls.itemForId(it) } } } override fun defaultPodcastGrouping(): PodcastGrouping { diff --git a/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/playback/MediaSessionManager.kt b/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/playback/MediaSessionManager.kt index e86d4547dfe..c33993f463e 100644 --- a/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/playback/MediaSessionManager.kt +++ b/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/playback/MediaSessionManager.kt @@ -336,7 +336,7 @@ class MediaSessionManager( addCustomAction(stateBuilder, APP_ACTION_SKIP_FWD, "Skip forward", IR.drawable.auto_skipforward) } - settings.defaultMediaNotificationControls().forEach { mediaControl -> + settings.getMediaNotificationControlItems().take(MediaNotificationControls.MAX_VISIBLE_OPTIONS).forEach { mediaControl -> when (mediaControl) { MediaNotificationControls.Archive -> addCustomAction(stateBuilder, APP_ACTION_ARCHIVE, "Archive", IR.drawable.ic_archive) MediaNotificationControls.MarkAsPlayed -> addCustomAction(stateBuilder, APP_ACTION_MARK_AS_PLAYED, "Mark as played", IR.drawable.auto_markasplayed)