-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathMusicService.cs
More file actions
357 lines (309 loc) · 16.2 KB
/
MusicService.cs
File metadata and controls
357 lines (309 loc) · 16.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
using System;
using Acr.UserDialogs;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Media;
using Android.OS;
using Android.Provider;
using Android.Runtime;
using Android.Support.V4.Media;
using Android.Support.V4.Media.Session;
using AndroidX.Core.App;
using AndroidX.Media;
using BMM.Api.Abstraction;
using BMM.Api.Framework;
using BMM.Core.Constants;
using BMM.Core.Implementations.Analytics;
using BMM.Core.Implementations.Downloading.DownloadQueue;
using BMM.Core.Implementations.Exceptions;
using BMM.Core.Implementations.Security;
using BMM.Core.Messages.MediaPlayer;
using BMM.Core.NewMediaPlayer.Abstractions;
using BMM.UI.Droid.Application.Constants.Player;
using BMM.UI.Droid.Application.Extensions;
using BMM.UI.Droid.Application.Helpers;
using BMM.UI.Droid.Application.Implementations.Notifications;
using BMM.UI.Droid.Application.NewMediaPlayer.Controller;
using BMM.UI.Droid.Application.NewMediaPlayer.Listeners;
using BMM.UI.Droid.Application.NewMediaPlayer.Notification;
using BMM.UI.Droid.Application.NewMediaPlayer.Playback;
using BMM.UI.Droid.Utils;
using Com.Google.Android.Exoplayer2;
using Com.Google.Android.Exoplayer2.Ext.Mediasession;
using Com.Google.Android.Exoplayer2.Trackselection;
using Com.Google.Android.Exoplayer2.Upstream;
using MvvmCross;
using MvvmCross.Base;
using MvvmCross.Plugin.Messenger;
using AudioAttributes = Com.Google.Android.Exoplayer2.Audio.AudioAttributes;
namespace BMM.UI.Droid.Application.NewMediaPlayer.Service
{
[Service(Name = "brunstad.MusicService")]
public class MusicService : MediaBrowserServiceCompat, TimelineQueueEditor.IMediaDescriptionConverter
{
private const int DefaultBufferTimeInMs = 15000;
private const int MinBufferTimeInMs = 300000;
private const int MaxBufferTimeInMs = 300000;
private NotificationManagerCompat _notificationManager;
private NowPlayingNotificationBuilder _notificationBuilder;
private MediaSessionCompat _mediaSession;
private IExoPlayer _exoPlayer;
private MediaSourceSetter _mediaSourceSetter;
private MediaControllerCompat _mediaController;
private bool _isForegroundService;
private PeriodicExecutor _progressUpdater = new PeriodicExecutor();
public static IExoPlayer CurrentExoPlayerInstance { get; private set; }
private IExoPlayer ExoPlayer
{
get
{
if (_exoPlayer == null)
{
var audioManager = (AudioManager)GetSystemService(AudioService);
var playerInstance = new IExoPlayer.Builder(ApplicationContext)
!.SetTrackSelector(new DefaultTrackSelector(ApplicationContext))
!.SetMediaSourceFactory(_mediaSourceFactory)
!.SetLoadControl(new LoadControl())
!.Build();
playerInstance!.SetHandleAudioBecomingNoisy(true);
playerInstance.AddListener(new PlayerListener(playerInstance));
// ToDo: we actually allow Music and Speeches within one playlist. Now it's always music.
var audioAttributes = new AudioAttributes.Builder()
.SetContentType(C.ContentTypeMusic)
.SetUsage(C.UsageMedia)
.Build();
playerInstance.SetAudioAttributes(audioAttributes, true);
_exoPlayer = playerInstance;
//_exoPlayer = new AudioFocusExoPlayerDecorator(playerInstance, audioManager, Mvx.IoCProvider.Resolve<ILogger>());
CurrentExoPlayerInstance = _exoPlayer;
}
return _exoPlayer;
}
}
/// <summary>
/// Triggers a periodic update of the current position and buffered position. Unfortunately MediaSessionConnector does not update
/// the buffered position except when a status change happens. Therefore we have to do it in the MusicService. It might cause unforeseen
/// problems and is not my preferred solution. Look in the history for an alternative implementation.
/// </summary>
private bool _periodicallyUpdateProgress;
private SingleMediaSourceFactory _mediaSourceFactory;
public bool PeriodicallyUpdateProgress
{
get => _periodicallyUpdateProgress;
set
{
if (!_periodicallyUpdateProgress && value)
{
_progressUpdater.SchedulePeriodicExecution(() =>
{
if (_exoPlayer == null)
{
// this should never happen in case it does I want to know about it
throw new Exception("_progressUpdater is called after destroying ExoPlayer");
}
if (Mvx.IoCProvider.CanResolve<IMvxMessenger>())
{
Mvx.IoCProvider.Resolve<IMvxMainThreadAsyncDispatcher>()
.ExecuteOnMainThreadAsync(() =>
Mvx.IoCProvider.Resolve<IMvxMessenger>()
.Publish(new PlaybackPositionChangedMessage(this)
{
CurrentPosition = ExoPlayer.CurrentPosition,
BufferedPosition = ExoPlayer.BufferedPosition
}));
}
});
}
if (!value)
{
_progressUpdater.StopStateUpdater();
}
_periodicallyUpdateProgress = value;
}
}
public override void OnCreate()
{
SetupHelper.EnsureInitialized();
base.OnCreate();
_mediaSession = new MediaSessionCompat(this, nameof(MusicService), null, null);
_mediaSession.Active = true;
SessionToken = _mediaSession.SessionToken;
_mediaController = new MediaControllerCompat(this, _mediaSession);
_mediaController.RegisterCallback(
new MusicServiceMediaCallback
{
OnMetadataChangedImpl = compat => UpdateNotification(_mediaController.PlaybackState),
OnPlaybackStateChangedImpl = compat =>
{
UpdateNotification(compat);
PeriodicallyUpdateProgress = compat.IsPlaying();
}
});
var metadataMapper = Mvx.IoCProvider.Resolve<IMetadataMapper>();
var queue = Mvx.IoCProvider.Resolve<IMediaQueue>();
var analytics = Mvx.IoCProvider.Resolve<IAnalytics>();
_notificationBuilder = new NowPlayingNotificationBuilder(this, metadataMapper, queue, Mvx.IoCProvider.Resolve<NotificationChannelBuilder>());
_notificationManager = NotificationManagerCompat.From(this);
_mediaSourceFactory = new SingleMediaSourceFactory(
this,
Mvx.IoCProvider.Resolve<IMediaRequestHttpHeaders>(),
Mvx.IoCProvider.Resolve<IAccessTokenProvider>());
var mediaSessionConnector = new MediaSessionConnector(_mediaSession);
_mediaSourceSetter = new MediaSourceSetter(() => mediaSessionConnector,
_ => new TimelineQueueEditor(_mediaController, new QueueDataAdapter(), this));
var preparer = new ExoPlaybackPreparer(ExoPlayer, queue, metadataMapper, _mediaSourceFactory, _mediaSourceSetter, analytics);
mediaSessionConnector.SetPlayer(ExoPlayer);
mediaSessionConnector.SetPlaybackPreparer(preparer);
mediaSessionConnector.SetQueueNavigator(new MetadataReadingQueueNavigator(_mediaSession, metadataMapper));
SetCustomActionsIfNeeded(mediaSessionConnector);
//mediaSessionConnector.SetControlDispatcher(new IdleRecoveringControlDispatcher(_mediaSourceSetter));
mediaSessionConnector.SetErrorMessageProvider(new CustomErrorMessageProvider(() => ExoPlayer,
Mvx.IoCProvider.Resolve<ISdkVersionHelper>(),
Mvx.IoCProvider.Resolve<ILogger>()));
_mediaSourceSetter.CreateNew();
}
private static void SetCustomActionsIfNeeded(MediaSessionConnector mediaSessionConnector)
{
if (Build.VERSION.SdkInt < BuildVersionCodes.Tiramisu)
return;
mediaSessionConnector.SetCustomActionProviders(
new SkipBackwardActionProvider(),
new SkipForwardActionProvider());
}
/// <summary>
/// Handle case when user swipes the app away from the recent app list by stopping the service (and any ongoing playback).
/// Unfortunately it's never triggered even though the documentation indicates it should: https://developer.android.com/reference/android/app/Service#onTaskRemoved(android.content.Intent)
/// </summary>
public override void OnTaskRemoved(Intent rootIntent)
{
base.OnTaskRemoved(rootIntent);
// By stopping playback, the player will transition to [Player.STATE_IDLE]. This will cause a state change in
// the MediaSession, and (most importantly) call [MediaControllerCallback.onPlaybackStateChanged]. Because the
// playback state will be reported as [PlaybackStateCompat.STATE_NONE], the service will first remove itself
// as a foreground service, and will then call [stopSelf].
ExoPlayer.Stop();
}
/// <summary>
/// This is only called if the user kills the app (swipe from recent apps) while ExoPlayer is currently playing.
/// </summary>
public override void OnDestroy()
{
Mvx.IoCProvider.Resolve<IDownloadQueue>().AppWasKilled();
Mvx.IoCProvider.Resolve<IMvxMessenger>().Publish(new PlaybackStatusChangedMessage(this, new DefaultPlaybackState()));
_mediaSession.Active = false;
_mediaSession.Release();
base.OnDestroy();
_progressUpdater.Dispose();
_exoPlayer.Stop();
_exoPlayer = null;
}
public override BrowserRoot OnGetRoot(string clientPackageName, int clientUid, Bundle rootHints)
{
// If we don't want to allow any arbitrary app to browser our content we need to check the origin
return new BrowserRoot(ExoPlayerConstants.MediaIdRoot, null);
}
public override void OnLoadChildren(string parentId, Result result)
{
var list = new JavaList<MediaBrowserCompat.MediaItem>();
// For now we always return an empty list since we don't really support browsing our library.
// If we want to support Android Car or Android Wear we should return meaningful data.
result.SendResult(list);
}
private void UpdateNotification(PlaybackStateCompat state)
{
if (string.IsNullOrEmpty(_mediaController.Metadata?.Description?.MediaId))
return;
Mvx.IoCProvider.Resolve<IExceptionHandler>()
.FireAndForgetWithoutUserMessages(async () =>
{
var updatedState = state.State;
Android.App.Notification notification = null;
if (updatedState != PlaybackStateCompat.StateNone)
{
notification = await _notificationBuilder.BuildNotification(_mediaSession.SessionToken, ApplicationContext, _mediaController);
if (notification == null)
return;
}
if (Build.VERSION.SdkInt >= BuildVersionCodes.UpsideDownCake)
{
if (updatedState == PlaybackStateCompat.StateBuffering || updatedState == PlaybackStateCompat.StatePlaying)
{
if (Build.VERSION.SdkInt >= BuildVersionCodes.UpsideDownCake)
StartForeground(NowPlayingNotificationBuilder.NowPlayingNotification,
notification,
ForegroundService.TypeMediaPlayback);
_isForegroundService = true;
}
else if (updatedState == PlaybackStateCompat.StateNone || updatedState == PlaybackStateCompat.StateStopped)
{
if (_isForegroundService)
{
StopForeground(StopForegroundFlags.Detach);
if (notification != null)
_notificationManager.Notify(NowPlayingNotificationBuilder.NowPlayingNotification, notification);
else
RemoveNowPlayingNotification();
_isForegroundService = false;
}
}
}
else
{
if (updatedState == PlaybackStateCompat.StateBuffering || updatedState == PlaybackStateCompat.StatePlaying)
{
StartForeground(NowPlayingNotificationBuilder.NowPlayingNotification, notification);
_isForegroundService = true;
}
else if (_isForegroundService)
{
StopForeground(false);
if (notification != null)
_notificationManager.Notify(NowPlayingNotificationBuilder.NowPlayingNotification, notification);
else
RemoveNowPlayingNotification();
_isForegroundService = false;
}
}
});
}
/// <summary>
/// Removes the <see cref="NowPlayingNotificationBuilder.NowPlayingNotification"/> notification
///
/// Since `stopForeground(false)` was already called (<see cref="UpdateNotification"/>) it's possible to cancel the notification
/// with `notificationManager.cancel(NOW_PLAYING_NOTIFICATION)` if minSdkVersion is >= [Build.VERSION_CODES.LOLLIPOP].
///
/// Prior to [Build.VERSION_CODES.LOLLIPOP], notifications associated with a foreground service remained marked as "ongoing" even
/// after calling [Service.stopForeground], and cannot be cancelled normally.
///
/// Fortunately, it's possible to simply call [Service.stopForeground] a second time, this time with `true`. This won't change
/// anything about the service's state, but will simply remove the notification.
/// </summary>
private void RemoveNowPlayingNotification()
{
StopForeground(true);
}
private class LoadControl : DefaultLoadControl
{
public LoadControl() : base(new DefaultAllocator(true, C.DefaultBufferSegmentSize),
MinBufferTimeInMs,
MaxBufferTimeInMs,
DefaultBufferTimeInMs,
MinBufferTimeInMs,
NumericConstants.Undefined,
true,
DefaultBackBufferDurationMs,
DefaultRetainBackBufferFromKeyframe)
{
}
/// <summary>
/// Duration in microseconds of media to retain in the buffer prior to the current playback position, for fast backward seeking.
/// </summary>
public override long BackBufferDurationUs => C.MsToUs(System.Convert.ToInt64(TimeSpan.FromHours(1).TotalMilliseconds));
}
public MediaItem Convert(MediaDescriptionCompat description)
{
return Mvx.IoCProvider.Resolve<IMetadataMapper>().ToMediaItem(description);
}
}
}