Skip to content

Commit 14bbca6

Browse files
CopilotYBTopaz8
andcommitted
Add favorite, shuffle, and repeat buttons to Android lock screen notification with proper state management
Co-authored-by: YBTopaz8 <41630728+YBTopaz8@users.noreply.github.com>
1 parent b40e811 commit 14bbca6

File tree

2 files changed

+215
-16
lines changed

2 files changed

+215
-16
lines changed

Dimmer/Dimmer.Droid/DimmerAudio/ExoPlayerService.cs

Lines changed: 195 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ public class ExoPlayerService : MediaSessionService
7373
internal static MediaItem? currentMediaItem; // Choose a unique ID
7474
public static SongModelView? CurrentSongContext; // Choose a unique ID
7575
public static SongModelView? CurrentSongExposed => CurrentSongContext;
76+
77+
// Static methods to expose state for notification
78+
internal static bool ShuffleStateInternal = false;
79+
internal static int RepeatModeInternal = 1; // 0=All, 1=Off, 2=One
80+
81+
public static bool GetShuffleState() => ShuffleStateInternal;
82+
public static int GetRepeatMode() => RepeatModeInternal;
7683
// --- Service Lifecycle ---
7784
private ExoPlayerServiceBinder? _binder;
7885

@@ -321,14 +328,21 @@ public async override void OnCreate()
321328
var shuffleButton = new CommandButton.Builder(CommandButton.IconUndefined)
322329
.SetDisplayName("Shuffle")
323330
.SetSessionCommand(new SessionCommand(ActionShuffle, Bundle.Empty))
324-
.SetCustomIconResId(Resource.Drawable.shuffle)
331+
.SetCustomIconResId(Resource.Drawable.media3_icon_shuffle_off)
325332
.Build();
333+
334+
var repeatButton = new CommandButton.Builder(CommandButton.IconUndefined)
335+
.SetDisplayName("Repeat")
336+
.SetSessionCommand(new SessionCommand(ActionRepeat, Bundle.Empty))
337+
.SetCustomIconResId(Resource.Drawable.media3_icon_repeat_off)
338+
.Build();
339+
326340
mediaSession = new MediaSession.Builder(this, notificationPlayer)!
327341
.SetSessionActivity(pendingIntent)!
328342
.SetCallback(sessionCallback)!
329343
.SetId("Dimmer_MediaSession_Main")!
330344

331-
.SetCustomLayout(customLayout: new List<CommandButton> { heartButton, shuffleButton })
345+
.SetCustomLayout(customLayout: new List<CommandButton> { heartButton, shuffleButton, repeatButton })
332346
.Build();
333347

334348
_binder = new ExoPlayerServiceBinder(this);
@@ -406,20 +420,107 @@ public override StartCommandResult OnStartCommand(Intent? intent, StartCommandFl
406420
switch (action)
407421
{
408422
case ActionFavorite:
409-
//MyViewModel.ToggleFavorite(CurrentSongContext);
410-
RefreshNotification(); // Forces the icons to flip
423+
HandleFavoriteAction();
411424
break;
412425
case ActionShuffle:
413-
//ToggleShuffle();
426+
HandleShuffleAction();
427+
break;
428+
case ActionRepeat:
429+
HandleRepeatAction();
414430
break;
415431
case ActionLyrics:
416-
// Throw your "Not Implemented" lyrics logic here!
432+
HandleLyricsAction();
417433
break;
418434
}
419435

420436

421437
return StartCommandResult.Sticky;
422438
}
439+
440+
private void HandleFavoriteAction()
441+
{
442+
if (CurrentSongContext != null)
443+
{
444+
// Get the ViewModel and toggle favorite state
445+
var viewModel = MainApplication.ServiceProvider.GetService<BaseViewModel>();
446+
if (viewModel != null)
447+
{
448+
Task.Run(async () =>
449+
{
450+
try
451+
{
452+
if (CurrentSongContext.IsFavorite)
453+
{
454+
await viewModel.RemoveSongFromFavorite(CurrentSongContext);
455+
}
456+
else
457+
{
458+
await viewModel.AddFavoriteRatingToSong(CurrentSongContext);
459+
}
460+
461+
MainThread.BeginInvokeOnMainThread(() => RefreshNotification());
462+
}
463+
catch (Exception ex)
464+
{
465+
Console.WriteLine($"[ExoPlayerService] Error toggling favorite: {ex.Message}");
466+
}
467+
});
468+
}
469+
}
470+
}
471+
472+
private void HandleShuffleAction()
473+
{
474+
var viewModel = MainApplication.ServiceProvider.GetService<BaseViewModel>();
475+
if (viewModel != null)
476+
{
477+
MainThread.BeginInvokeOnMainThread(() =>
478+
{
479+
try
480+
{
481+
viewModel.ToggleShuffle();
482+
ShuffleStateInternal = viewModel.IsShuffleActive;
483+
RefreshNotification();
484+
}
485+
catch (Exception ex)
486+
{
487+
Console.WriteLine($"[ExoPlayerService] Error toggling shuffle: {ex.Message}");
488+
}
489+
});
490+
}
491+
}
492+
493+
private void HandleRepeatAction()
494+
{
495+
var viewModel = MainApplication.ServiceProvider.GetService<BaseViewModel>();
496+
if (viewModel != null)
497+
{
498+
MainThread.BeginInvokeOnMainThread(() =>
499+
{
500+
try
501+
{
502+
viewModel.ToggleRepeatMode();
503+
RepeatModeInternal = (int)viewModel.CurrentRepeatMode;
504+
RefreshNotification();
505+
}
506+
catch (Exception ex)
507+
{
508+
Console.WriteLine($"[ExoPlayerService] Error toggling repeat: {ex.Message}");
509+
}
510+
});
511+
}
512+
}
513+
514+
private void HandleLyricsAction()
515+
{
516+
// Open the app to the lyrics view
517+
var intent = new Intent(this, typeof(TransitionActivity));
518+
intent.SetAction(Intent.ActionMain);
519+
intent.AddCategory(Intent.CategoryLauncher);
520+
intent.AddFlags(ActivityFlags.NewTask);
521+
intent.PutExtra("ShowLyrics", true);
522+
StartActivity(intent);
523+
}
423524

424525

425526
public override IBinder? OnBind(Intent? intent)
@@ -518,6 +619,15 @@ public Task Prepare(
518619
try
519620
{
520621
CurrentSongContext =song;
622+
623+
// Sync shuffle and repeat state from ViewModel
624+
var viewModel = MainApplication.ServiceProvider?.GetService<BaseViewModel>();
625+
if (viewModel != null)
626+
{
627+
ShuffleStateInternal = viewModel.IsShuffleActive;
628+
RepeatModeInternal = (int)viewModel.CurrentRepeatMode;
629+
}
630+
521631
if (player is null)
522632
{
523633

@@ -604,13 +714,27 @@ public void UpdateMediaSessionLayout()
604714
.SetCustomIconResId(isFav ? Resource.Drawable.media3_icon_heart_filled : Resource.Drawable.heart)
605715
.Build();
606716

717+
bool isShuffleOn = ShuffleStateInternal;
607718
var shuffleBtn = new CommandButton.Builder(CommandButton.IconUndefined)
608719
.SetDisplayName("Shuffle")
609720
.SetSessionCommand(new SessionCommand(ActionShuffle, Bundle.Empty))
610-
.SetCustomIconResId(Resource.Drawable.shuffle)
721+
.SetCustomIconResId(isShuffleOn ? Resource.Drawable.media3_icon_shuffle_on : Resource.Drawable.media3_icon_shuffle_off)
722+
.Build();
723+
724+
int repeatMode = RepeatModeInternal;
725+
int repeatIcon = repeatMode switch
726+
{
727+
2 => Resource.Drawable.media3_icon_repeat_one, // Repeat One
728+
0 => Resource.Drawable.media3_icon_repeat_all, // Repeat All
729+
_ => Resource.Drawable.media3_icon_repeat_off // Repeat Off
730+
};
731+
var repeatBtn = new CommandButton.Builder(CommandButton.IconUndefined)
732+
.SetDisplayName("Repeat")
733+
.SetSessionCommand(new SessionCommand(ActionRepeat, Bundle.Empty))
734+
.SetCustomIconResId(repeatIcon)
611735
.Build();
612736

613-
var layout = new List<CommandButton> { heartBtn, shuffleBtn };
737+
var layout = new List<CommandButton> { heartBtn, shuffleBtn, repeatBtn };
614738
mediaSession?.SetCustomLayout((System.Collections.Generic.IList<CommandButton>)layout);
615739
}
616740
// --- Player Event Listener ---
@@ -854,7 +978,7 @@ public MediaSession.ConnectionResult OnConnect(
854978
.Add(SessionCommand.CommandCodeSessionSetRating)!
855979
.Add(new SessionCommand(ExoPlayerService.ActionFavorite, Bundle.Empty))
856980
.Add(new SessionCommand(ExoPlayerService.ActionShuffle, Bundle.Empty))!
857-
.Add(new SessionCommand(ExoPlayerService.ActionShuffle, Bundle.Empty))!
981+
.Add(new SessionCommand(ExoPlayerService.ActionRepeat, Bundle.Empty))!
858982
.Build();
859983

860984

@@ -957,18 +1081,75 @@ public SessionResult OnCustomCommand(MediaSession? session, MediaSession.Control
9571081
var currentSong = ExoPlayerService.CurrentSongContext;
9581082
if (currentSong != null)
9591083
{
960-
currentSong.IsFavorite = !currentSong.IsFavorite;
961-
ExoPlayerService.UpdateFavoriteState(currentSong);
1084+
var viewModel = MainApplication.ServiceProvider.GetService<BaseViewModel>();
1085+
if (viewModel != null)
1086+
{
1087+
Task.Run(async () =>
1088+
{
1089+
try
1090+
{
1091+
if (currentSong.IsFavorite)
1092+
{
1093+
await viewModel.RemoveSongFromFavorite(currentSong);
1094+
}
1095+
else
1096+
{
1097+
await viewModel.AddFavoriteRatingToSong(currentSong);
1098+
}
1099+
1100+
MainThread.BeginInvokeOnMainThread(() => service.UpdateMediaSessionLayout());
1101+
}
1102+
catch (Exception ex)
1103+
{
1104+
Console.WriteLine($"[ExoPlayerService] Error toggling favorite: {ex.Message}");
1105+
}
1106+
});
1107+
}
9621108
}
9631109
return (SessionResult)SessionResult.ResultSuccess;
1110+
9641111
case ExoPlayerService.ActionShuffle:
9651112
// Toggle shuffle mode
966-
if (service.player != null)
1113+
var vm = MainApplication.ServiceProvider.GetService<BaseViewModel>();
1114+
if (vm != null)
1115+
{
1116+
MainThread.BeginInvokeOnMainThread(() =>
1117+
{
1118+
try
1119+
{
1120+
vm.ToggleShuffle();
1121+
ExoPlayerService.ShuffleStateInternal = vm.IsShuffleActive;
1122+
service.UpdateMediaSessionLayout();
1123+
}
1124+
catch (Exception ex)
1125+
{
1126+
Console.WriteLine($"[ExoPlayerService] Error toggling shuffle: {ex.Message}");
1127+
}
1128+
});
1129+
}
1130+
return (SessionResult)SessionResult.ResultSuccess;
1131+
1132+
case ExoPlayerService.ActionRepeat:
1133+
// Toggle repeat mode
1134+
var vmRepeat = MainApplication.ServiceProvider.GetService<BaseViewModel>();
1135+
if (vmRepeat != null)
9671136
{
968-
bool newShuffleState = !service.player.ShuffleModeEnabled;
969-
service.player.ShuffleModeEnabled = newShuffleState;
1137+
MainThread.BeginInvokeOnMainThread(() =>
1138+
{
1139+
try
1140+
{
1141+
vmRepeat.ToggleRepeatMode();
1142+
ExoPlayerService.RepeatModeInternal = (int)vmRepeat.CurrentRepeatMode;
1143+
service.UpdateMediaSessionLayout();
1144+
}
1145+
catch (Exception ex)
1146+
{
1147+
Console.WriteLine($"[ExoPlayerService] Error toggling repeat: {ex.Message}");
1148+
}
1149+
});
9701150
}
9711151
return (SessionResult)SessionResult.ResultSuccess;
1152+
9721153
default:
9731154
return (SessionResult)SessionResult.ResultErrorUnknown;
9741155
}

Dimmer/Dimmer.Droid/DimmerAudio/NotificationHelper.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public static PlayerNotificationManager BuildManager(
119119
var actionList = new List<string> {
120120
DimmerActionReceiver.ActionFavorite,
121121
DimmerActionReceiver.ActionShuffle,
122+
DimmerActionReceiver.ActionRepeat,
122123
DimmerActionReceiver.ActionLyrics
123124
};
124125

@@ -213,10 +214,11 @@ class DimmerActionReceiver : Java.Lang.Object, PlayerNotificationManager.ICustom
213214
public DimmerActionReceiver(Context ctx) => _ctx = ctx;
214215
public const string ActionFavorite = "ACTION_FAVORITE";
215216
public const string ActionShuffle = "ACTION_SHUFFLE";
217+
public const string ActionRepeat = "ACTION_REPEAT";
216218

217219
public IList<string> GetCustomActions(IPlayer? player)
218220
{
219-
return new List<string> { ActionFavorite, ActionShuffle, ActionLyrics };
221+
return new List<string> { ActionFavorite, ActionShuffle, ActionRepeat, ActionLyrics };
220222
}
221223

222224
// 2. Create the actual Button UI (C# logic)
@@ -232,7 +234,23 @@ public IList<string> GetCustomActions(IPlayer? player)
232234
return CreateAction(context, heartIcon, "Favorite", ActionFavorite);
233235

234236
case ActionShuffle:
235-
return CreateAction(context, Resource.Drawable.shuffle, "Shuffle", ActionShuffle);
237+
// Check shuffle state to pick icon
238+
bool isShuffleOn = ExoPlayerService.GetShuffleState();
239+
int shuffleIcon = isShuffleOn
240+
? Resource.Drawable.media3_icon_shuffle_on
241+
: Resource.Drawable.media3_icon_shuffle_off;
242+
return CreateAction(context, shuffleIcon, "Shuffle", ActionShuffle);
243+
244+
case ActionRepeat:
245+
// Check repeat mode to pick icon
246+
int repeatMode = ExoPlayerService.GetRepeatMode();
247+
int repeatIcon = repeatMode switch
248+
{
249+
2 => Resource.Drawable.media3_icon_repeat_one, // Repeat One
250+
0 => Resource.Drawable.media3_icon_repeat_all, // Repeat All
251+
_ => Resource.Drawable.media3_icon_repeat_off // Repeat Off
252+
};
253+
return CreateAction(context, repeatIcon, "Repeat", ActionRepeat);
236254

237255
case ActionLyrics:
238256
return CreateAction(context, Resource.Drawable.lyrics, "Lyrics", ActionLyrics);

0 commit comments

Comments
 (0)