Skip to content

Commit

Permalink
Add Media Notification controls for Android 13 under General-> Defaul…
Browse files Browse the repository at this point in the history
…ts setting
  • Loading branch information
ksog66 committed Nov 4, 2022
1 parent c57ff73 commit 1c433fb
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 36 deletions.
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)
}
)
}
}

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
8 changes: 8 additions & 0 deletions modules/services/localization/src/main/res/values/strings.xml
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

0 comments on commit 1c433fb

Please sign in to comment.