Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Media Notification Controls for Android 13 #540

Merged
merged 1 commit into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
7.27
-----
* New Features:
* Added ability to set playback effects in Tasker "Control Playback" action.
([#415](https://github.com/Automattic/pocket-casts-android/pull/509)).

* Allowed customization of actions through Settings in Media Notification Control for Android 13 users.
([#499](https://github.com/Automattic/pocket-casts-android/pull/540)).

7.26
-----

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package au.com.shiftyjelly.pocketcasts.settings

import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
Expand Down Expand Up @@ -39,6 +40,7 @@ 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
Expand All @@ -47,6 +49,7 @@ 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.utils.extensions.isPositive
import au.com.shiftyjelly.pocketcasts.views.dialog.ConfirmationDialog
Expand Down Expand Up @@ -133,6 +136,15 @@ class PlaybackSettingsFragment : BaseFragment() {
showSetAllArchiveDialog(it)
}
)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
MediaNotificationControls(
saved = settings.defaultMediaNotificationControlsFlow.collectAsState().value,
onSave = {
settings.setDefaultMediaNotificationControls(it)
}
)
}
Comment on lines +140 to +147
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

}

SettingSection(heading = stringResource(LR.string.settings_general_player)) {
Expand Down Expand Up @@ -272,6 +284,20 @@ class PlaybackSettingsFragment : BaseFragment() {
},
)

@Composable
fun MediaNotificationControls(
saved: List<MediaNotificationControls>,
onSave: (List<MediaNotificationControls>) -> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ 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
Expand All @@ -27,6 +29,7 @@ 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
Expand All @@ -38,6 +41,7 @@ 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

Expand Down Expand Up @@ -268,6 +272,95 @@ fun ProgressDialog(
}
}

@Composable
fun <T> CheckboxDialog(
title: String,
options: List<Pair<T, String>>,
savedOption: List<T>,
maxOptions: Int,
onSave: (List<T>) -> 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,
Expand Down Expand Up @@ -323,6 +416,32 @@ 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,37 @@ fun <T> SettingRadioDialogRow(
}
}

@Composable
fun <T> SettingCheckBoxDialogRow(
primaryText: String,
modifier: Modifier = Modifier,
secondaryText: String? = null,
options: List<T>,
savedOption: List<T>,
maxOptions: Int = savedOption.size,
optionToLocalisedString: (T) -> String,
onSave: (List<T>) -> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<string name="play_episode">Play episode</string>
<string name="play_last">Play last</string>
<string name="play_next">Play next</string>
<string name="playback_speed">Playback speed</string>
<string name="play_now">Play now</string>
<string name="play_on">Play on&#8230;</string>
<string name="played">Played</string>
Expand Down Expand Up @@ -1010,6 +1011,13 @@
<string name="settings_lock_image">lock image</string>
<string name="settings_manage_downloads_include_starred">Include starred</string>
<string name="settings_manage_downloads_total">Total</string>
<string name="settings_media_notification_controls">Media notification controls</string>
<string name="settings_media_notification_controls_summary">Choose two actions to be displayed in the Android 13 playback notification, Android Auto, and other places the custom media controls are available</string>
<string name="settings_media_notification_controls_title_archive" translatable="false">@string/archive</string>
<string name="settings_media_notification_controls_title_mark_as_played" translatable="false">@string/mark_as_played</string>
<string name="settings_media_notification_controls_title_play_next" translatable="false">@string/play_next</string>
<string name="settings_media_notification_controls_title_playback_speed" translatable="false">@string/playback_speed</string>
<string name="settings_media_notification_controls_title_star" translatable="false">@string/star</string>
<string name="settings_no_thanks">No thanks</string>
<string name="settings_notifications_new_episodes">New Episodes</string>
<string name="settings_notification_actions">Actions</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package au.com.shiftyjelly.pocketcasts.preferences

import android.content.Context
import android.net.Uri
import androidx.annotation.StringRes
import au.com.shiftyjelly.pocketcasts.models.to.PlaybackEffects
import au.com.shiftyjelly.pocketcasts.models.to.PodcastGrouping
import au.com.shiftyjelly.pocketcasts.models.to.RefreshState
Expand Down Expand Up @@ -207,6 +208,32 @@ interface Settings {
fun toIndex(): Int = options.indexOf(this)
}

sealed class MediaNotificationControls(@StringRes val controlName: Int, val key: String) {

companion object {
val All
get() = listOf(Archive, MarkAsPlayed, PlayNext, PlaybackSpeed, Star)

const val MaxSelectedOptions = 2

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

object Archive : MediaNotificationControls(LR.string.archive, ARCHIVE_KEY)

object MarkAsPlayed : MediaNotificationControls(LR.string.mark_as_played, MARK_AS_PLAYED_KEY)

object PlayNext : MediaNotificationControls(LR.string.play_next, PLAY_NEXT_KEY)

object PlaybackSpeed : MediaNotificationControls(LR.string.playback_speed, PLAYBACK_SPEED_KEY)

object Star : MediaNotificationControls(LR.string.star, STAR_KEY)
}

sealed class AutoArchiveInactive(val timeSeconds: Int) {
object Never : AutoArchiveInactive(-1)
object Hours24 : AutoArchiveInactive(24 * 60 * 60)
Expand Down Expand Up @@ -258,6 +285,7 @@ interface Settings {
val autoAddUpNextLimit: Observable<Int>

val defaultPodcastGroupingFlow: StateFlow<PodcastGrouping>
val defaultMediaNotificationControlsFlow: StateFlow<List<MediaNotificationControls>>
val defaultShowArchivedFlow: StateFlow<Boolean>
val intelligentPlaybackResumptionFlow: StateFlow<Boolean>
val keepScreenAwakeFlow: StateFlow<Boolean>
Expand Down Expand Up @@ -520,6 +548,8 @@ interface Settings {
fun getAutoPlayNextEpisodeOnEmpty(): Boolean
fun defaultShowArchived(): Boolean
fun setDefaultShowArchived(value: Boolean)
fun defaultMediaNotificationControls(): List<MediaNotificationControls>
fun setDefaultMediaNotificationControls(mediaNotificationControls: List<MediaNotificationControls>)
fun setMultiSelectItems(items: List<Int>)
fun getMultiSelectItems(): List<Int>
fun setLastPauseTime(date: Date)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import au.com.shiftyjelly.pocketcasts.models.type.PodcastsSortType
import au.com.shiftyjelly.pocketcasts.models.type.TrimMode
import au.com.shiftyjelly.pocketcasts.preferences.Settings.Companion.DEFAULT_MAX_AUTO_ADD_LIMIT
import au.com.shiftyjelly.pocketcasts.preferences.Settings.Companion.SETTINGS_ENCRYPT_SECRET
import au.com.shiftyjelly.pocketcasts.preferences.Settings.MediaNotificationControls
import au.com.shiftyjelly.pocketcasts.preferences.Settings.NotificationChannel
import au.com.shiftyjelly.pocketcasts.preferences.Settings.NotificationId
import au.com.shiftyjelly.pocketcasts.preferences.di.PrivateSharedPreferences
Expand Down Expand Up @@ -93,6 +94,7 @@ class SettingsImpl @Inject constructor(
override val autoAddUpNextLimit = BehaviorRelay.create<Int>().apply { accept(getAutoAddUpNextLimit()) }

override val defaultPodcastGroupingFlow = MutableStateFlow(defaultPodcastGrouping())
override val defaultMediaNotificationControlsFlow = MutableStateFlow(defaultMediaNotificationControls())
override val defaultShowArchivedFlow = MutableStateFlow(defaultShowArchived())
override val keepScreenAwakeFlow = MutableStateFlow(keepScreenAwake())
override val intelligentPlaybackResumptionFlow = MutableStateFlow(getIntelligentPlaybackResumption())
Expand Down Expand Up @@ -1188,6 +1190,25 @@ class SettingsImpl @Inject constructor(
defaultShowArchivedFlow.update { value }
}

override fun defaultMediaNotificationControls(): List<MediaNotificationControls> {
val selectedValue = MediaNotificationControls.All.map { mediaControl ->
val defaultValue =
(mediaControl == MediaNotificationControls.PlaybackSpeed || mediaControl == MediaNotificationControls.Star)
Pair(mediaControl, getBoolean(mediaControl.key, defaultValue))
}

return selectedValue.filter { (_, value) -> value }.map { (mediaControl, _) ->
mediaControl
}
}

override fun setDefaultMediaNotificationControls(mediaNotificationControls: List<MediaNotificationControls>) {
MediaNotificationControls.All.forEach { mediaControl ->
setBoolean(mediaControl.key, mediaNotificationControls.contains(mediaControl))
}
defaultMediaNotificationControlsFlow.update { mediaNotificationControls }
}

override fun defaultPodcastGrouping(): PodcastGrouping {
val index = getInt("default_podcast_grouping", 0)
return PodcastGrouping.All.getOrNull(index) ?: PodcastGrouping.None
Expand Down
Loading