Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ interface Settings {
) {
companion object {
val All
get() = listOf(PlaybackSpeed, Star, MarkAsPlayed, PlayNext, Archive)
get() = listOf(PlaybackSpeed, Star, MarkAsPlayed, PlayNext, Archive, Bookmark)
val items = All.associateBy { it.key }

const val MAX_VISIBLE_OPTIONS = 3
Expand All @@ -238,6 +238,7 @@ interface Settings {
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"
private const val BOOKMARK_KEY = "default_media_control_bookmark_key"

fun itemForId(id: String): MediaNotificationControls? {
return items[id]
Expand Down Expand Up @@ -287,6 +288,13 @@ interface Settings {
key = STAR_KEY,
serverId = "star",
)

data object Bookmark : MediaNotificationControls(
controlName = LR.string.bookmark,
iconRes = IR.drawable.ic_bookmark,
key = BOOKMARK_KEY,
serverId = "bookmark",
)
}

val currentSessionId: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,7 @@ class SettingsImpl @Inject constructor(
defaultValue = MediaNotificationControls.All,
fromString = { MediaNotificationControls.itemForId(it) },
toString = { it.key },
addMissingDefaultValues = true,
)

override val podcastGroupingDefault = run {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ abstract class UserSetting<T>(
private val defaultValue: List<T>,
private val fromString: (String) -> T?,
private val toString: (T) -> String,
private val addMissingDefaultValues: Boolean = false,
) : UserSetting<List<T>>(
sharedPrefKey = sharedPrefKey,
sharedPrefs = sharedPrefs,
Expand All @@ -281,8 +282,13 @@ abstract class UserSetting<T>(
return if (strValue.isNullOrEmpty()) {
defaultValue
} else {
val commaSeparatedString = strValue.split(",")
commaSeparatedString.mapNotNull(fromString)
val stored = strValue.split(",").mapNotNull(fromString)
if (addMissingDefaultValues) {
val missing = defaultValue.filterNot { it in stored }
if (missing.isEmpty()) stored else stored + missing
} else {
stored
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import au.com.shiftyjelly.pocketcasts.utils.log.LogBuffer
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
Expand Down Expand Up @@ -91,6 +93,7 @@ internal class Media3SessionCallback(
.add(SessionCommand(APP_ACTION_CHANGE_SPEED, Bundle.EMPTY))
.add(SessionCommand(APP_ACTION_ARCHIVE, Bundle.EMPTY))
.add(SessionCommand(APP_ACTION_PLAY_NEXT, Bundle.EMPTY))
.add(SessionCommand(APP_ACTION_ADD_BOOKMARK, Bundle.EMPTY))
.add(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
.build()

Expand Down Expand Up @@ -126,6 +129,7 @@ internal class Media3SessionCallback(
APP_ACTION_CHANGE_SPEED -> launchCommandFuture("Change speed") { actions.changePlaybackSpeedSuspend() }
APP_ACTION_ARCHIVE -> launchCommandFuture("Archive") { actions.archiveSuspend() }
APP_ACTION_PLAY_NEXT -> launchCommandFuture("Play next") { playbackManager.playNextInQueue() }
APP_ACTION_ADD_BOOKMARK -> launchCommandFuture(tag = "Bookmark", context = Dispatchers.Main) { addBookmark() }
else -> Futures.immediateFuture(SessionResult(SessionError.ERROR_NOT_SUPPORTED))
}
}
Expand Down Expand Up @@ -316,9 +320,9 @@ internal class Media3SessionCallback(
* actual outcome — [SessionResult.RESULT_SUCCESS] on completion or
* [SessionError.ERROR_UNKNOWN] on failure.
*/
private fun launchCommandFuture(tag: String, block: suspend () -> Unit): ListenableFuture<SessionResult> {
private fun launchCommandFuture(tag: String, context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit): ListenableFuture<SessionResult> {
val future = SettableFuture.create<SessionResult>()
scope.launch {
scope.launch(context) {
commandMutex.withLock {
try {
block()
Expand All @@ -332,13 +336,17 @@ internal class Media3SessionCallback(
return future
}

private suspend fun addBookmark() {
val isAutoConnected = Util.isAndroidAutoConnectedFlow(contextProvider()).first()
bookmarkHelper.handleAddBookmarkAction(contextProvider(), isAutoConnected)
}

private fun handleMediaButtonAction(action: HeadphoneAction) {
when (action) {
HeadphoneAction.ADD_BOOKMARK -> {
scope.launch(Dispatchers.Main) {
try {
val isAutoConnected = Util.isAndroidAutoConnectedFlow(contextProvider()).first()
bookmarkHelper.handleAddBookmarkAction(contextProvider(), isAutoConnected)
addBookmark()
} catch (e: Exception) {
Timber.e(e, "Add bookmark failed")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import androidx.media.utils.MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.CommandButton
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionError
import au.com.shiftyjelly.pocketcasts.analytics.SourceView
Expand Down Expand Up @@ -66,6 +65,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -708,6 +708,13 @@ class MediaSessionManager(
null
}
}

MediaNotificationControls.Bookmark ->
CommandButton.Builder(CommandButton.ICON_UNDEFINED)
.setSessionCommand(SessionCommand(APP_ACTION_ADD_BOOKMARK, Bundle.EMPTY))
.setDisplayName(context.getString(LR.string.add_bookmark))
.setCustomIconResId(IR.drawable.ic_bookmark)
.build()
Comment on lines +712 to +717
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

Media3 custom layout adds a SessionCommand for APP_ACTION_ADD_BOOKMARK, but Media3SessionCallback currently neither advertises this command in onConnect() nor handles it in onCustomCommand(). In Media3 session mode this will likely make the bookmark button disabled or return ERROR_NOT_SUPPORTED. Add APP_ACTION_ADD_BOOKMARK to the accepted SessionCommands and implement its handler to invoke the existing bookmark flow (similar to the legacy onAddBookmark() / HeadphoneAction.ADD_BOOKMARK handling).

Copilot uses AI. Check for mistakes.
}
}

Expand Down Expand Up @@ -1034,6 +1041,8 @@ class MediaSessionManager(
}
}
}

MediaNotificationControls.Bookmark -> addCustomAction(stateBuilder, APP_ACTION_ADD_BOOKMARK, "Add bookmark", IR.drawable.ic_bookmark)
}
}
}
Expand Down Expand Up @@ -1095,12 +1104,9 @@ class MediaSessionManager(

private fun onAddBookmark() {
logEvent("add bookmark")
val coroutineContext = CoroutineScope(Dispatchers.Main + Job())
coroutineContext.launch {
Util.isAndroidAutoConnectedFlow(context).collect {
bookmarkHelper.handleAddBookmarkAction(context, it)
coroutineContext.cancel()
}
scope.launch(Dispatchers.Main) {
val isAndroidAuto = Util.isAndroidAutoConnectedFlow(context).first()
bookmarkHelper.handleAddBookmarkAction(context, isAndroidAuto)
}
}

Expand Down Expand Up @@ -1253,6 +1259,8 @@ class MediaSessionManager(
APP_ACTION_PLAY_NEXT -> enqueueCommand("custom action: play next") {
playbackManager.playNextInQueue()
}

APP_ACTION_ADD_BOOKMARK -> onAddBookmark()
}
}

Expand Down Expand Up @@ -1354,6 +1362,7 @@ internal const val APP_ACTION_MARK_AS_PLAYED = "markAsPlayed"
internal const val APP_ACTION_CHANGE_SPEED = "changeSpeed"
internal const val APP_ACTION_ARCHIVE = "archive"
internal const val APP_ACTION_PLAY_NEXT = "playNext"
internal const val APP_ACTION_ADD_BOOKMARK = "addBookmark"

private val NOTHING_PLAYING: MediaMetadataCompat = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, "")
Expand Down