diff --git a/src/Lib/SharedMauiLib/INativeAudioService.cs b/src/Lib/SharedMauiLib/INativeAudioService.cs new file mode 100644 index 0000000..e012e44 --- /dev/null +++ b/src/Lib/SharedMauiLib/INativeAudioService.cs @@ -0,0 +1,24 @@ +namespace SharedMauiLib; + +public interface INativeAudioService +{ + Task InitializeAsync(string audioURI); + + Task PlayAsync(double position = 0); + + Task PauseAsync(); + + Task SetMuted(bool value); + + Task SetVolume(int value); + + Task SetCurrentTime(double value); + + ValueTask DisposeAsync(); + + bool IsPlaying { get; } + + double CurrentPosition { get; } + + event EventHandler IsPlayingChanged; +} \ No newline at end of file diff --git a/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/ActivityEvent.cs b/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/ActivityEvent.cs new file mode 100644 index 0000000..784d8b6 --- /dev/null +++ b/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/ActivityEvent.cs @@ -0,0 +1,13 @@ +namespace SharedMauiLib.Platforms.Android.CurrentActivity +{ + public enum ActivityEvent + { + Created, + Resumed, + Paused, + Destroyed, + SaveInstanceState, + Started, + Stopped + } +} \ No newline at end of file diff --git a/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/ActivityEventArgs.cs b/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/ActivityEventArgs.cs new file mode 100644 index 0000000..28250fb --- /dev/null +++ b/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/ActivityEventArgs.cs @@ -0,0 +1,16 @@ +using Android.App; + +namespace SharedMauiLib.Platforms.Android.CurrentActivity +{ + public class ActivityEventArgs : EventArgs + { + internal ActivityEventArgs(Activity activity, ActivityEvent ev) + { + Event = ev; + Activity = activity; + } + + public ActivityEvent Event { get; } + public Activity Activity { get; } + } +} \ No newline at end of file diff --git a/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/CrossCurrentActivity.cs b/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/CrossCurrentActivity.cs new file mode 100644 index 0000000..da89bac --- /dev/null +++ b/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/CrossCurrentActivity.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharedMauiLib.Platforms.Android.CurrentActivity +{ + public class CrossCurrentActivity + { + static Lazy implementation = new Lazy(() => CreateCurrentActivity(), System.Threading.LazyThreadSafetyMode.PublicationOnly); + + /// + /// Current settings to use + /// + public static ICurrentActivity Current + { + get + { + var ret = implementation.Value; + if (ret == null) + { + throw NotImplementedInReferenceAssembly(); + } + return ret; + } + } + + static ICurrentActivity CreateCurrentActivity() + { +#if NETSTANDARD1_0 || NETSTANDARD2_0 + return null; +#else + return new CurrentActivityImplementation(); +#endif + } + + internal static Exception NotImplementedInReferenceAssembly() + { + return new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation."); + } + } +} \ No newline at end of file diff --git a/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/CurrentActivityImplementation.cs b/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/CurrentActivityImplementation.cs new file mode 100644 index 0000000..c562c6a --- /dev/null +++ b/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/CurrentActivityImplementation.cs @@ -0,0 +1,162 @@ +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using System; +using System.Threading; +using System.Threading.Tasks; +using AndroidApp = Android.App; + +namespace SharedMauiLib.Platforms.Android.CurrentActivity +{ + /// + /// Implementation for Feature + /// + [Preserve(AllMembers = true)] + public class CurrentActivityImplementation : ICurrentActivity + { + + /// + /// Gets or sets the activity. + /// + /// The activity. + public Activity Activity + { + get => lifecycleListener?.Activity; + set + { + if (lifecycleListener == null) + Init(value, null); + } + } + + /// + /// Activity state changed event handler + /// + public event EventHandler ActivityStateChanged; + + + /// + /// Waits for an activity to be ready + /// + /// + public async Task WaitForActivityAsync(CancellationToken cancelToken = default) + { + if (Activity != null) + return Activity; + + var tcs = new TaskCompletionSource(); + var handler = new EventHandler((sender, args) => + { + if (args.Event == ActivityEvent.Created || args.Event == ActivityEvent.Resumed) + tcs.TrySetResult(args.Activity); + }); + + try + { + using (cancelToken.Register(() => tcs.TrySetCanceled())) + { + ActivityStateChanged += handler; + return await tcs.Task.ConfigureAwait(false); + } + } + finally + { + ActivityStateChanged -= handler; + } + } + + + internal void RaiseStateChanged(Activity activity, ActivityEvent ev) + => ActivityStateChanged?.Invoke(this, new ActivityEventArgs(activity, ev)); + + + ActivityLifecycleContextListener lifecycleListener; + + /// + /// Gets the current application context + /// + public Context AppContext => + AndroidApp.Application.Context; + + /// + /// Initialize current activity with application + /// + /// The main application + public void Init(AndroidApp.Application application) + { + if (lifecycleListener != null) + return; + + lifecycleListener = new ActivityLifecycleContextListener(); + application.RegisterActivityLifecycleCallbacks(lifecycleListener); + } + + /// + /// Initialize current activity with activity! + /// + /// The main activity + /// Bundle for activity + public void Init(Activity activity, Bundle bundle) + { + Init(activity.Application); + lifecycleListener.Activity = activity; + } + } + + [Preserve(AllMembers = true)] + class ActivityLifecycleContextListener : Java.Lang.Object, AndroidApp.Application.IActivityLifecycleCallbacks + { + WeakReference currentActivity = new WeakReference(null); + + public Context Context => + Activity ?? AndroidApp.Application.Context; + + public Activity Activity + { + get => currentActivity.TryGetTarget(out var a) ? a : null; + set => currentActivity.SetTarget(value); + } + + CurrentActivityImplementation Current => + (CurrentActivityImplementation)(CrossCurrentActivity.Current); + + public void OnActivityCreated(Activity activity, Bundle savedInstanceState) + { + Activity = activity; + Current.RaiseStateChanged(activity, ActivityEvent.Created); + } + + public void OnActivityDestroyed(Activity activity) + { + Current.RaiseStateChanged(activity, ActivityEvent.Destroyed); + } + + public void OnActivityPaused(Activity activity) + { + Activity = activity; + Current.RaiseStateChanged(activity, ActivityEvent.Paused); + } + + public void OnActivityResumed(Activity activity) + { + Activity = activity; + Current.RaiseStateChanged(activity, ActivityEvent.Resumed); + } + + public void OnActivitySaveInstanceState(Activity activity, Bundle outState) + { + Current.RaiseStateChanged(activity, ActivityEvent.SaveInstanceState); + } + + public void OnActivityStarted(Activity activity) + { + Current.RaiseStateChanged(activity, ActivityEvent.Started); + } + + public void OnActivityStopped(Activity activity) + { + Current.RaiseStateChanged(activity, ActivityEvent.Stopped); + } + } +} \ No newline at end of file diff --git a/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/ICurrentActivity.cs b/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/ICurrentActivity.cs new file mode 100644 index 0000000..96e0b9e --- /dev/null +++ b/src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/ICurrentActivity.cs @@ -0,0 +1,48 @@ +using Android.Content; +using Android.OS; +using AndroidApp = Android.App; + +namespace SharedMauiLib.Platforms.Android.CurrentActivity +{ + /// + /// Current Activity Interface + /// + public interface ICurrentActivity + { + /// + /// Gets or sets the activity. + /// + /// The activity. + AndroidApp.Activity Activity { get; set; } + + /// + /// Gets the current Application Context. + /// + /// The app context. + Context AppContext { get; } + + /// + /// Fires when activity state events are fired + /// + event EventHandler ActivityStateChanged; + + /// + /// Waits for an activity to be ready for use + /// + /// + Task WaitForActivityAsync(CancellationToken cancelToken = default); + + /// + /// Initialize Current Activity Plugin with Application + /// + /// + void Init(AndroidApp.Application application); + + /// + /// Initialize the current activity with activity and bundle + /// + /// + /// + void Init(AndroidApp.Activity activity, Bundle bundle); + } +} \ No newline at end of file diff --git a/src/Mobile/Platforms/Android/Services/EventHandlers.cs b/src/Lib/SharedMauiLib/Platforms/Android/EventHandlers.cs similarity index 71% rename from src/Mobile/Platforms/Android/Services/EventHandlers.cs rename to src/Lib/SharedMauiLib/Platforms/Android/EventHandlers.cs index 488f045..c6352f3 100644 --- a/src/Mobile/Platforms/Android/Services/EventHandlers.cs +++ b/src/Lib/SharedMauiLib/Platforms/Android/EventHandlers.cs @@ -1,4 +1,4 @@ -namespace Microsoft.NetConf2021.Maui.Platforms.Android.Services; +namespace SharedMauiLib.Platforms.Android; public delegate void StatusChangedEventHandler(object sender, EventArgs e); @@ -7,3 +7,5 @@ public delegate void BufferingEventHandler(object sender, EventArgs e); public delegate void CoverReloadedEventHandler(object sender, EventArgs e); public delegate void PlayingEventHandler(object sender, EventArgs e); + +public delegate void PlayingChangedEventHandler(object sender, bool e); \ No newline at end of file diff --git a/src/Lib/SharedMauiLib/Platforms/Android/IAudioActivity.cs b/src/Lib/SharedMauiLib/Platforms/Android/IAudioActivity.cs new file mode 100644 index 0000000..e614be6 --- /dev/null +++ b/src/Lib/SharedMauiLib/Platforms/Android/IAudioActivity.cs @@ -0,0 +1,15 @@ +namespace SharedMauiLib.Platforms.Android +{ + public interface IAudioActivity + { + public MediaPlayerServiceBinder Binder { get; set; } + + public event StatusChangedEventHandler StatusChanged; + + public event CoverReloadedEventHandler CoverReloaded; + + public event PlayingEventHandler Playing; + + public event BufferingEventHandler Buffering; + } +} diff --git a/src/Mobile/Platforms/Android/Services/MediaPlayerService.cs b/src/Lib/SharedMauiLib/Platforms/Android/MediaPlayerService.cs similarity index 91% rename from src/Mobile/Platforms/Android/Services/MediaPlayerService.cs rename to src/Lib/SharedMauiLib/Platforms/Android/MediaPlayerService.cs index 07e6717..cfed35c 100644 --- a/src/Mobile/Platforms/Android/Services/MediaPlayerService.cs +++ b/src/Lib/SharedMauiLib/Platforms/Android/MediaPlayerService.cs @@ -5,16 +5,15 @@ using Android.Net; using Android.Net.Wifi; using Android.OS; using Android.Media.Session; -using Microsoft.NetConf2021.Maui.Platforms.Android.Receivers; using AndroidNet = Android.Net; using Android.Graphics; -using Microsoft.Maui.Platform; -namespace Microsoft.NetConf2021.Maui.Platforms.Android.Services; +namespace SharedMauiLib.Platforms.Android; + [Service(Exported = true)] [IntentFilter(new[] { ActionPlay, ActionPause, ActionStop, ActionTogglePlayback, ActionNext, ActionPrevious })] -public class MediaPlayerService : Service, +public class MediaPlayerService : Service, AudioManager.IOnAudioFocusChangeListener, MediaPlayer.IOnBufferingUpdateListener, MediaPlayer.IOnCompletionListener, @@ -46,6 +45,12 @@ public class MediaPlayerService : Service, public event BufferingEventHandler Buffering; + public event PlayingChangedEventHandler PlayingChanged; + + public string AudioUrl; + + public bool isCurrentEpisode = true; + private readonly Handler PlayingHandler; private readonly Java.Lang.Runnable PlayingHandlerRunnable; @@ -55,9 +60,9 @@ public class MediaPlayerService : Service, { get { - return (mediaController.PlaybackState != null - ? mediaController.PlaybackState.State - : PlaybackStateCode.None); + return mediaController.PlaybackState != null + ? mediaController.PlaybackState.State + : PlaybackStateCode.None; } } @@ -66,7 +71,8 @@ public class MediaPlayerService : Service, PlayingHandler = new Handler(Looper.MainLooper); // Create a runnable, restarting itself if the status still is "playing" - PlayingHandlerRunnable = new Java.Lang.Runnable(() => { + PlayingHandlerRunnable = new Java.Lang.Runnable(() => + { OnPlaying(EventArgs.Empty); if (MediaPlayerState == PlaybackStateCode.Playing) @@ -76,7 +82,8 @@ public class MediaPlayerService : Service, }); // On Status changed to PLAYING, start raising the Playing event - StatusChanged += (object sender, EventArgs e) => { + StatusChanged += (sender, e) => + { if (MediaPlayerState == PlaybackStateCode.Playing) { PlayingHandler.PostDelayed(PlayingHandlerRunnable, 0); @@ -89,6 +96,11 @@ public class MediaPlayerService : Service, StatusChanged?.Invoke(this, e); } + protected virtual void OnPlayingChanged(bool e) + { + PlayingChanged?.Invoke(this, e); + } + protected virtual void OnCoverReloaded(EventArgs e) { if (CoverReloaded != null) @@ -131,7 +143,7 @@ public class MediaPlayerService : Service, { if (mediaSession == null) { - Intent nIntent = new Intent(ApplicationContext, typeof(MainActivity)); + Intent nIntent = new Intent(ApplicationContext, typeof(Activity)); remoteComponentName = new ComponentName(PackageName, new RemoteControlBroadcastReceiver().ComponentName); @@ -203,15 +215,13 @@ public class MediaPlayerService : Service, UpdatePlaybackState(PlaybackStateCode.Playing); } - public string AudioUrl ; - public int Position { get { if (mediaPlayer == null - || (MediaPlayerState != PlaybackStateCode.Playing - && MediaPlayerState != PlaybackStateCode.Paused)) + || MediaPlayerState != PlaybackStateCode.Playing + && MediaPlayerState != PlaybackStateCode.Paused) return -1; else return mediaPlayer.CurrentPosition; @@ -223,8 +233,8 @@ public class MediaPlayerService : Service, get { if (mediaPlayer == null - || (MediaPlayerState != PlaybackStateCode.Playing - && MediaPlayerState != PlaybackStateCode.Paused)) + || MediaPlayerState != PlaybackStateCode.Playing + && MediaPlayerState != PlaybackStateCode.Paused) return 0; else return mediaPlayer.Duration; @@ -256,7 +266,7 @@ public class MediaPlayerService : Service, get { if (cover == null) - cover = BitmapFactory.DecodeResource(Resources, Resource.Drawable.player_play); + cover = BitmapFactory.DecodeResource(Resources, Resource.Drawable.abc_ab_share_pack_mtrl_alpha); //TODO player_play return cover; } private set @@ -289,12 +299,14 @@ public class MediaPlayerService : Service, if (mediaSession == null) InitMediaSession(); - if (mediaPlayer.IsPlaying) + if (mediaPlayer.IsPlaying && isCurrentEpisode) { UpdatePlaybackState(PlaybackStateCode.Playing); return; } + isCurrentEpisode = true; + await PrepareAndPlayMediaPlayerAsync(); } @@ -330,18 +342,14 @@ public class MediaPlayerService : Service, byte[] imageByteArray = metaRetriever.GetEmbeddedPicture(); if (imageByteArray == null) - Cover = await BitmapFactory.DecodeResourceAsync(Resources, Resource.Drawable.player_play); + Cover = await BitmapFactory.DecodeResourceAsync(Resources, Resource.Drawable.abc_ab_share_pack_mtrl_alpha); //TODO player_play else Cover = await BitmapFactory.DecodeByteArrayAsync(imageByteArray, 0, imageByteArray.Length); } } catch (Exception ex) { - UpdatePlaybackState(PlaybackStateCode.Stopped); - - mediaPlayer.Reset(); - mediaPlayer.Release(); - mediaPlayer = null; + UpdatePlaybackStateStopped(); // Unable to start playback log error Console.WriteLine(ex); @@ -350,7 +358,8 @@ public class MediaPlayerService : Service, public async Task Seek(int position) { - await Task.Run(() => { + await Task.Run(() => + { if (mediaPlayer != null) { mediaPlayer.SeekTo(position); @@ -396,7 +405,7 @@ public class MediaPlayerService : Service, public async Task PlayPause() { - if (mediaPlayer == null || (mediaPlayer != null && MediaPlayerState == PlaybackStateCode.Paused)) + if (mediaPlayer == null || mediaPlayer != null && MediaPlayerState == PlaybackStateCode.Paused) { await Play(); } @@ -408,7 +417,8 @@ public class MediaPlayerService : Service, public async Task Pause() { - await Task.Run(() => { + await Task.Run(() => + { if (mediaPlayer == null) return; @@ -421,7 +431,8 @@ public class MediaPlayerService : Service, public async Task Stop() { - await Task.Run(() => { + await Task.Run(() => + { if (mediaPlayer == null) return; @@ -439,6 +450,18 @@ public class MediaPlayerService : Service, }); } + public void UpdatePlaybackStateStopped() + { + UpdatePlaybackState(PlaybackStateCode.Stopped); + + if (mediaPlayer != null) + { + mediaPlayer.Reset(); + mediaPlayer.Release(); + mediaPlayer = null; + } + } + private void UpdatePlaybackState(PlaybackStateCode state) { if (mediaSession == null || mediaPlayer == null) @@ -485,6 +508,16 @@ public class MediaPlayerService : Service, MediaPlayerState == PlaybackStateCode.Playing); } + internal void SetMuted(bool value) + { + mediaPlayer.SetVolume(0, 0); + } + + internal void SetVolume(int value) + { + mediaPlayer.SetVolume(value, value); + } + /// /// Updates the metadata on the lock screen /// @@ -662,13 +695,13 @@ public class MediaPlayerService : Service, public override async void OnPause() { - await mediaPlayerService.GetMediaPlayerService().Pause(); + mediaPlayerService.GetMediaPlayerService().OnPlayingChanged(false); base.OnPause(); } public override async void OnPlay() { - await mediaPlayerService.GetMediaPlayerService().Play(); + mediaPlayerService.GetMediaPlayerService().OnPlayingChanged(true); base.OnPlay(); } diff --git a/src/Lib/SharedMauiLib/Platforms/Android/MediaPlayerServiceConnection.cs b/src/Lib/SharedMauiLib/Platforms/Android/MediaPlayerServiceConnection.cs new file mode 100644 index 0000000..77230df --- /dev/null +++ b/src/Lib/SharedMauiLib/Platforms/Android/MediaPlayerServiceConnection.cs @@ -0,0 +1,33 @@ +using Android.Content; +using Android.OS; + +namespace SharedMauiLib.Platforms.Android +{ + public class MediaPlayerServiceConnection : Java.Lang.Object, IServiceConnection + { + readonly IAudioActivity instance; + + public MediaPlayerServiceConnection(IAudioActivity mediaPlayer) + { + this.instance = mediaPlayer; + } + + public void OnServiceConnected(ComponentName name, IBinder service) + { + if (service is MediaPlayerServiceBinder binder) + { + instance.Binder = binder; + + var mediaPlayerService = binder.GetMediaPlayerService(); + //mediaPlayerService.CoverReloaded += (object sender, EventArgs e) => { instance.CoverReloaded?.Invoke(sender, e); }; + //mediaPlayerService.StatusChanged += (object sender, EventArgs e) => { instance.StatusChanged?.Invoke(sender, e); }; + //mediaPlayerService.Playing += (object sender, EventArgs e) => { instance.Playing?.Invoke(sender, e); }; + //mediaPlayerService.Buffering += (object sender, EventArgs e) => { instance.Buffering?.Invoke(sender, e); }; + } + } + + public void OnServiceDisconnected(ComponentName name) + { + } + } +} diff --git a/src/Lib/SharedMauiLib/Platforms/Android/NativeAudioService.cs b/src/Lib/SharedMauiLib/Platforms/Android/NativeAudioService.cs new file mode 100644 index 0000000..7fde434 --- /dev/null +++ b/src/Lib/SharedMauiLib/Platforms/Android/NativeAudioService.cs @@ -0,0 +1,93 @@ +using Android.Media; +using AndroidApp = Android.App; + +namespace SharedMauiLib.Platforms.Android +{ + public class NativeAudioService : INativeAudioService + { + IAudioActivity instance; + + private MediaPlayer mediaPlayer => instance != null && + instance.Binder.GetMediaPlayerService() != null ? + instance.Binder.GetMediaPlayerService().mediaPlayer : null; + + public bool IsPlaying => mediaPlayer?.IsPlaying ?? false; + + public double CurrentPosition => mediaPlayer?.CurrentPosition / 1000 ?? 0; + public event EventHandler IsPlayingChanged; + + public Task InitializeAsync(string audioURI) + { + if (instance == null) + { + var activity = CurrentActivity.CrossCurrentActivity.Current; + instance = activity.Activity as IAudioActivity; + } + else + { + instance.Binder.GetMediaPlayerService().isCurrentEpisode = false; + instance.Binder.GetMediaPlayerService().UpdatePlaybackStateStopped(); + } + + this.instance.Binder.GetMediaPlayerService().PlayingChanged += (object sender, bool e) => + { + Task.Run(async () => { + if (e) + { + await this.PlayAsync(); + } + else + { + await this.PauseAsync(); + } + }); + IsPlayingChanged?.Invoke(this, e); + }; + + instance.Binder.GetMediaPlayerService().AudioUrl = audioURI; + + return Task.CompletedTask; + } + + public Task PauseAsync() + { + if (IsPlaying) + { + return instance.Binder.GetMediaPlayerService().Pause(); + } + + return Task.CompletedTask; + } + + public async Task PlayAsync(double position = 0) + { + await instance.Binder.GetMediaPlayerService().Play(); + await instance.Binder.GetMediaPlayerService().Seek((int)position * 1000); + } + + public Task SetMuted(bool value) + { + instance?.Binder.GetMediaPlayerService().SetMuted(value); + + return Task.CompletedTask; + } + + public Task SetVolume(int value) + { + instance?.Binder.GetMediaPlayerService().SetVolume(value); + + return Task.CompletedTask; + } + + public Task SetCurrentTime(double position) + { + return instance.Binder.GetMediaPlayerService().Seek((int)position * 1000); + } + + public ValueTask DisposeAsync() + { + instance.Binder?.Dispose(); + return ValueTask.CompletedTask; + } + } +} diff --git a/src/Mobile/Platforms/Android/Services/NotificationHelper.cs b/src/Lib/SharedMauiLib/Platforms/Android/NotificationHelper.cs similarity index 77% rename from src/Mobile/Platforms/Android/Services/NotificationHelper.cs rename to src/Lib/SharedMauiLib/Platforms/Android/NotificationHelper.cs index 098c0e3..151095d 100644 --- a/src/Mobile/Platforms/Android/Services/NotificationHelper.cs +++ b/src/Lib/SharedMauiLib/Platforms/Android/NotificationHelper.cs @@ -8,7 +8,8 @@ using static Android.App.Notification; using static Android.Resource; using AndroidMedia = Android.Media; -namespace Microsoft.NetConf2021.Maui.Platforms.Android.Services; + +namespace SharedMauiLib.Platforms.Android; public static class NotificationHelper { @@ -37,7 +38,7 @@ public static class NotificationHelper nm.CancelAll(); } - internal static void CreateNotificationChannel(Context context) + public static void CreateNotificationChannel(Context context) { if (Build.VERSION.SdkInt < BuildVersionCodes.O) { @@ -59,50 +60,50 @@ public static class NotificationHelper } internal static void StartNotification( - Context context, + Context context, MediaMetadata mediaMetadata, AndroidMedia.Session.MediaSession mediaSession, - Object largeIcon, + object largeIcon, bool isPlaying) { var pendingIntent = PendingIntent.GetActivity( context, 0, - new Intent(context, typeof(MainActivity)), + new Intent(context, typeof(Activity)), PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Mutable); MediaMetadata currentTrack = mediaMetadata; MediaStyle style = new MediaStyle(); style.SetMediaSession(mediaSession.SessionToken); - var builder = new Notification.Builder(context, CHANNEL_ID) + var builder = new Builder(context, CHANNEL_ID) .SetStyle(style) .SetContentTitle(currentTrack.GetString(MediaMetadata.MetadataKeyTitle)) .SetContentText(currentTrack.GetString(MediaMetadata.MetadataKeyArtist)) .SetSubText(currentTrack.GetString(MediaMetadata.MetadataKeyAlbum)) - .SetSmallIcon(Resource.Drawable.player_play) + .SetSmallIcon(Resource.Drawable.abc_ab_share_pack_mtrl_alpha) //TODO player_play .SetLargeIcon(largeIcon as Bitmap) .SetContentIntent(pendingIntent) .SetShowWhen(false) .SetOngoing(isPlaying) .SetVisibility(NotificationVisibility.Public); - builder.AddAction(NotificationHelper.GenerateActionCompat(context, Drawable.IcMediaPrevious, "Previous", MediaPlayerService.ActionPrevious)); + builder.AddAction(GenerateActionCompat(context, Drawable.IcMediaPrevious, "Previous", MediaPlayerService.ActionPrevious)); AddPlayPauseActionCompat(builder, context, isPlaying); - builder.AddAction(NotificationHelper.GenerateActionCompat(context, Drawable.IcMediaNext, "Next", MediaPlayerService.ActionNext)); + builder.AddAction(GenerateActionCompat(context, Drawable.IcMediaNext, "Next", MediaPlayerService.ActionNext)); style.SetShowActionsInCompactView(0, 1, 2); NotificationManagerCompat.From(context).Notify(NotificationId, builder.Build()); } private static void AddPlayPauseActionCompat( - Notification.Builder builder, + Builder builder, Context context, bool isPlaying) { if (isPlaying) - builder.AddAction(NotificationHelper.GenerateActionCompat(context, Drawable.IcMediaPause, "Pause", MediaPlayerService.ActionPause)); + builder.AddAction(GenerateActionCompat(context, Drawable.IcMediaPause, "Pause", MediaPlayerService.ActionPause)); else - builder.AddAction(NotificationHelper.GenerateActionCompat(context, Drawable.IcMediaPlay, "Play", MediaPlayerService.ActionPlay)); + builder.AddAction(GenerateActionCompat(context, Drawable.IcMediaPlay, "Play", MediaPlayerService.ActionPlay)); } } diff --git a/src/Lib/SharedMauiLib/Platforms/Android/RemoteControlBroadcastReceiver.cs b/src/Lib/SharedMauiLib/Platforms/Android/RemoteControlBroadcastReceiver.cs new file mode 100644 index 0000000..342606c --- /dev/null +++ b/src/Lib/SharedMauiLib/Platforms/Android/RemoteControlBroadcastReceiver.cs @@ -0,0 +1,68 @@ +using Android.App; +using Android.Content; +using Android.Views; + +namespace SharedMauiLib.Platforms.Android; + +[BroadcastReceiver(Exported = true)] +[IntentFilter(new[] { Intent.ActionMediaButton })] +public class RemoteControlBroadcastReceiver : BroadcastReceiver +{ + + /// + /// gets the class name for the component + /// + /// The name of the component. + public string ComponentName { get { return Class.Name; } } + + /// The Context in which the receiver is running. + /// + /// When we receive the action media button intent + /// parse the key event and tell our service what to do. + /// + /// Context. + /// Intent. + public override void OnReceive(Context context, Intent intent) + { + if (intent.Action != Intent.ActionMediaButton) + return; + + //The event will fire twice, up and down. + // we only want to handle the down event though. + var key = (KeyEvent)intent.GetParcelableExtra(Intent.ExtraKeyEvent); + if (key.Action != KeyEventActions.Down) + return; + + string action; + + switch (key.KeyCode) + { + case Keycode.Headsethook: + case Keycode.MediaPlayPause: + action = MediaPlayerService.ActionTogglePlayback; + break; + case Keycode.MediaPlay: + action = MediaPlayerService.ActionPlay; + break; + case Keycode.MediaPause: + action = MediaPlayerService.ActionPause; + break; + case Keycode.MediaStop: + action = MediaPlayerService.ActionStop; + break; + case Keycode.MediaNext: + action = MediaPlayerService.ActionNext; + break; + case Keycode.MediaPrevious: + action = MediaPlayerService.ActionPrevious; + break; + default: + return; + } + + var remoteIntent = new Intent(action); + context.StartService(remoteIntent); + } +} + + diff --git a/src/Lib/SharedMauiLib/Platforms/MacCatalyst/NativeAudioService.cs b/src/Lib/SharedMauiLib/Platforms/MacCatalyst/NativeAudioService.cs new file mode 100644 index 0000000..3e7b315 --- /dev/null +++ b/src/Lib/SharedMauiLib/Platforms/MacCatalyst/NativeAudioService.cs @@ -0,0 +1,74 @@ +using AVFoundation; +using Foundation; + +namespace SharedMauiLib.Platforms.MacCatalyst; + +public class NativeAudioService : INativeAudioService +{ + AVPlayer avPlayer; + string _uri; + + public bool IsPlaying => avPlayer != null + ? avPlayer.TimeControlStatus == AVPlayerTimeControlStatus.Playing + : false; + + public double CurrentPosition => avPlayer?.CurrentTime.Seconds ?? 0; + public event EventHandler IsPlayingChanged; + + public async Task InitializeAsync(string audioURI) + { + _uri = audioURI; + NSUrl fileURL = new NSUrl(_uri.ToString()); + + if (avPlayer != null) + { + await PauseAsync(); + } + + avPlayer = new AVPlayer(fileURL); + } + + public Task PauseAsync() + { + avPlayer?.Pause(); + + return Task.CompletedTask; + } + + public async Task PlayAsync(double position = 0) + { + await avPlayer.SeekAsync(new CoreMedia.CMTime((long)position, 1)); + avPlayer?.Play(); + } + + public Task SetCurrentTime(double value) + { + return avPlayer.SeekAsync(new CoreMedia.CMTime((long)value, 1)); + } + + public Task SetMuted(bool value) + { + if (avPlayer != null) + { + avPlayer.Muted = value; + } + + return Task.CompletedTask; + } + + public Task SetVolume(int value) + { + if (avPlayer != null) + { + avPlayer.Volume = value; + } + + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + avPlayer?.Dispose(); + return ValueTask.CompletedTask; + } +} diff --git a/src/Lib/SharedMauiLib/Platforms/Windows/NativeAudioService.cs b/src/Lib/SharedMauiLib/Platforms/Windows/NativeAudioService.cs new file mode 100644 index 0000000..32925f7 --- /dev/null +++ b/src/Lib/SharedMauiLib/Platforms/Windows/NativeAudioService.cs @@ -0,0 +1,91 @@ +using Windows.Media.Core; +using Windows.Media.Playback; + +namespace SharedMauiLib.Platforms.Windows; + +public class NativeAudioService : INativeAudioService +{ + string _uri; + MediaPlayer mediaPlayer; + + public bool IsPlaying => mediaPlayer != null + && mediaPlayer.CurrentState == MediaPlayerState.Playing; + + public double CurrentPosition => mediaPlayer?.Position.TotalSeconds ?? 0; + public event EventHandler IsPlayingChanged; + + public async Task InitializeAsync(string audioURI) + { + _uri = audioURI; + + if (mediaPlayer == null) + { + mediaPlayer = new MediaPlayer + { + Source = MediaSource.CreateFromUri(new Uri(_uri)), + AudioCategory = MediaPlayerAudioCategory.Media + }; + } + if (mediaPlayer != null) + { + await PauseAsync(); + mediaPlayer.Source = MediaSource.CreateFromUri(new Uri(_uri)); + } + + } + + public Task PauseAsync() + { + mediaPlayer?.Pause(); + return Task.CompletedTask; + } + + public Task PlayAsync(double position = 0) + { + if (mediaPlayer != null) + { + mediaPlayer.Position = TimeSpan.FromSeconds(position); + mediaPlayer.Play(); + } + + return Task.CompletedTask; + } + + public Task SetCurrentTime(double value) + { + if (mediaPlayer != null) + { + mediaPlayer.Position = TimeSpan.FromSeconds(value); + } + + return Task.CompletedTask; + } + + public Task SetMuted(bool value) + { + if (mediaPlayer != null) + { + mediaPlayer.IsMuted = value; + } + + return Task.CompletedTask; + } + + public Task SetVolume(int value) + { + if (mediaPlayer != null) + { + mediaPlayer.Volume = value != 0 + ? value / 100d + : 0; + } + + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + mediaPlayer?.Dispose(); + return ValueTask.CompletedTask; + } +} diff --git a/src/Mobile/Platforms/iOS/AudioService.cs b/src/Lib/SharedMauiLib/Platforms/iOS/NativeAudioService.cs similarity index 51% rename from src/Mobile/Platforms/iOS/AudioService.cs rename to src/Lib/SharedMauiLib/Platforms/iOS/NativeAudioService.cs index 40ed6f6..c08f907 100644 --- a/src/Mobile/Platforms/iOS/AudioService.cs +++ b/src/Lib/SharedMauiLib/Platforms/iOS/NativeAudioService.cs @@ -1,18 +1,19 @@ using AVFoundation; using Foundation; -namespace Microsoft.NetConf2021.Maui.Platforms.iOS; +namespace SharedMauiLib.Platforms.iOS; -public class AudioService : IAudioService +public class NativeAudioService : INativeAudioService { AVPlayer avPlayer; string _uri; public bool IsPlaying => avPlayer != null ? avPlayer.TimeControlStatus == AVPlayerTimeControlStatus.Playing - : false; //TODO + : false; public double CurrentPosition => avPlayer?.CurrentTime.Seconds ?? 0; + public event EventHandler IsPlayingChanged; public async Task InitializeAsync(string audioURI) { @@ -30,7 +31,7 @@ public class AudioService : IAudioService public Task PauseAsync() { avPlayer?.Pause(); - + return Task.CompletedTask; } @@ -39,4 +40,35 @@ public class AudioService : IAudioService await avPlayer.SeekAsync(new CoreMedia.CMTime((long)position, 1)); avPlayer?.Play(); } -} + + public Task SetCurrentTime(double value) + { + return avPlayer.SeekAsync(new CoreMedia.CMTime((long)value, 1)); + } + + public Task SetMuted(bool value) + { + if (avPlayer != null) + { + avPlayer.Muted = value; + } + + return Task.CompletedTask; + } + + public Task SetVolume(int value) + { + if (avPlayer != null) + { + avPlayer.Volume = value; + } + + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + avPlayer?.Dispose(); + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Mobile/Resources/Images/player_play.svg b/src/Lib/SharedMauiLib/Resources/Images/player_play.svg similarity index 100% rename from src/Mobile/Resources/Images/player_play.svg rename to src/Lib/SharedMauiLib/Resources/Images/player_play.svg diff --git a/src/Lib/SharedMauiLib/SharedMauiLib.csproj b/src/Lib/SharedMauiLib/SharedMauiLib.csproj new file mode 100644 index 0000000..5aa6c19 --- /dev/null +++ b/src/Lib/SharedMauiLib/SharedMauiLib.csproj @@ -0,0 +1,22 @@ + + + + + net6.0;net6.0-android;net6.0-ios;net6.0-maccatalyst + $(TargetFrameworks);net6.0-windows10.0.19041 + true + true + enable + + 14.2 + 14.0 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + + + + + + + diff --git a/src/Mobile/Microsoft.NetConf2021.Maui.csproj b/src/Mobile/Microsoft.NetConf2021.Maui.csproj index d146a6a..fb19f75 100644 --- a/src/Mobile/Microsoft.NetConf2021.Maui.csproj +++ b/src/Mobile/Microsoft.NetConf2021.Maui.csproj @@ -56,6 +56,7 @@ + diff --git a/src/Mobile/Platforms/Android/AudioService.cs b/src/Mobile/Platforms/Android/AudioService.cs deleted file mode 100644 index e4f4a11..0000000 --- a/src/Mobile/Platforms/Android/AudioService.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Android.Media; - -namespace Microsoft.NetConf2021.Maui.Platforms.Android; - -public class AudioService : IAudioService -{ - MainActivity instance; - - private MediaPlayer mediaPlayer => (instance != null && - instance.binder.GetMediaPlayerService() != null ) ? - instance.binder.GetMediaPlayerService().mediaPlayer : null; - - public bool IsPlaying => mediaPlayer?.IsPlaying ?? false; - - public double CurrentPosition => mediaPlayer?.CurrentPosition/1000 ?? 0; - - public async Task InitializeAsync(string audioURI) - { - if (this.instance == null) - { - this.instance = MainActivity.instance; - } - else - { - await this.instance.binder.GetMediaPlayerService().Stop(); - } - - this.instance.binder.GetMediaPlayerService().AudioUrl = audioURI; - } - - public Task PauseAsync() - { - if (IsPlaying) - { - return this.instance.binder.GetMediaPlayerService().Pause(); - } - - return Task.CompletedTask; - } - - public async Task PlayAsync(double position = 0) - { - await this.instance.binder.GetMediaPlayerService().Play(); - await this.instance.binder.GetMediaPlayerService().Seek((int)position * 1000); - } -} diff --git a/src/Mobile/Platforms/Android/MainActivity.cs b/src/Mobile/Platforms/Android/MainActivity.cs index 0816b9c..d4f084e 100644 --- a/src/Mobile/Platforms/Android/MainActivity.cs +++ b/src/Mobile/Platforms/Android/MainActivity.cs @@ -2,7 +2,8 @@ using Android.Content; using Android.Content.PM; using Android.OS; -using Microsoft.NetConf2021.Maui.Platforms.Android.Services; +using SharedMauiLib.Platforms.Android; +using SharedMauiLib.Platforms.Android.CurrentActivity; namespace Microsoft.NetConf2021.Maui; @@ -10,24 +11,21 @@ namespace Microsoft.NetConf2021.Maui; Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)] -public class MainActivity : MauiAppCompatActivity +public class MainActivity : MauiAppCompatActivity , IAudioActivity { - internal static MainActivity instance; - public MediaPlayerServiceBinder binder; MediaPlayerServiceConnection mediaPlayerServiceConnection; + public MediaPlayerServiceBinder Binder { get; set; } + public event StatusChangedEventHandler StatusChanged; - public event CoverReloadedEventHandler CoverReloaded; - public event PlayingEventHandler Playing; - public event BufferingEventHandler Buffering; protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); - instance = this; + CrossCurrentActivity.Current.Init(this, savedInstanceState); NotificationHelper.CreateNotificationChannel(ApplicationContext); if (mediaPlayerServiceConnection == null) InitializeMedia(); @@ -39,32 +37,4 @@ public class MainActivity : MauiAppCompatActivity var mediaPlayerServiceIntent = new Intent(ApplicationContext, typeof(MediaPlayerService)); BindService(mediaPlayerServiceIntent, mediaPlayerServiceConnection, Bind.AutoCreate); } - - class MediaPlayerServiceConnection : Java.Lang.Object, IServiceConnection - { - readonly MainActivity instance; - - public MediaPlayerServiceConnection(MainActivity mediaPlayer) - { - this.instance = mediaPlayer; - } - - public void OnServiceConnected(ComponentName name, IBinder service) - { - if (service is MediaPlayerServiceBinder binder) - { - instance.binder = binder; - - var mediaPlayerService = binder.GetMediaPlayerService(); - mediaPlayerService.CoverReloaded += (object sender, EventArgs e) => { instance.CoverReloaded?.Invoke(sender, e); }; - mediaPlayerService.StatusChanged += (object sender, EventArgs e) => { instance.StatusChanged?.Invoke(sender, e); }; - mediaPlayerService.Playing += (object sender, EventArgs e) => { instance.Playing?.Invoke(sender, e); }; - mediaPlayerService.Buffering += (object sender, EventArgs e) => { instance.Buffering?.Invoke(sender, e); }; - } - } - - public void OnServiceDisconnected(ComponentName name) - { - } - } } diff --git a/src/Mobile/Platforms/Android/Receivers/RemoteControlBroadcastReceiver.cs b/src/Mobile/Platforms/Android/Receivers/RemoteControlBroadcastReceiver.cs deleted file mode 100644 index 12d98ec..0000000 --- a/src/Mobile/Platforms/Android/Receivers/RemoteControlBroadcastReceiver.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Android.App; -using Android.Content; -using Android.Views; -using Microsoft.NetConf2021.Maui.Platforms.Android.Services; - -namespace Microsoft.NetConf2021.Maui.Platforms.Android.Receivers; - -[BroadcastReceiver(Exported = true)] -[IntentFilter(new[] { Intent.ActionMediaButton })] -public class RemoteControlBroadcastReceiver : BroadcastReceiver -{ - - /// - /// gets the class name for the component - /// - /// The name of the component. - public string ComponentName { get { return this.Class.Name; } } - - /// The Context in which the receiver is running. - /// - /// When we receive the action media button intent - /// parse the key event and tell our service what to do. - /// - /// Context. - /// Intent. - public override void OnReceive(Context context, Intent intent) - { - if (intent.Action != Intent.ActionMediaButton) - return; - - //The event will fire twice, up and down. - // we only want to handle the down event though. - var key = (KeyEvent)intent.GetParcelableExtra(Intent.ExtraKeyEvent); - if (key.Action != KeyEventActions.Down) - return; - - string action; - - switch (key.KeyCode) - { - case Keycode.Headsethook: - case Keycode.MediaPlayPause: - action = MediaPlayerService.ActionTogglePlayback; - break; - case Keycode.MediaPlay: - action = MediaPlayerService.ActionPlay; - break; - case Keycode.MediaPause: - action = MediaPlayerService.ActionPause; - break; - case Keycode.MediaStop: - action = MediaPlayerService.ActionStop; - break; - case Keycode.MediaNext: - action = MediaPlayerService.ActionNext; - break; - case Keycode.MediaPrevious: - action = MediaPlayerService.ActionPrevious; - break; - default: - return; - } - - var remoteIntent = new Intent(action); - context.StartService(remoteIntent); - } -} - - diff --git a/src/Mobile/Platforms/MacCatalyst/AudioService.cs b/src/Mobile/Platforms/MacCatalyst/AudioService.cs deleted file mode 100644 index c8c63e7..0000000 --- a/src/Mobile/Platforms/MacCatalyst/AudioService.cs +++ /dev/null @@ -1,43 +0,0 @@ -using AVFoundation; -using Foundation; - -namespace Microsoft.NetConf2021.Maui.Platforms.MacCatalyst; - -public class AudioService : IAudioService -{ - AVPlayer avPlayer; - string _uri; - - public bool IsPlaying => avPlayer != null - ? avPlayer.TimeControlStatus == AVPlayerTimeControlStatus.Playing - : false; //TODO - - public double CurrentPosition => avPlayer?.CurrentTime.Seconds ?? 0; - - public async Task InitializeAsync(string audioURI) - { - _uri = audioURI; - NSUrl fileURL = new NSUrl(_uri.ToString()); - - if (avPlayer != null) - { - await PauseAsync(); - } - - avPlayer = new AVPlayer(fileURL); - } - - public Task PauseAsync() - { - avPlayer?.Pause(); - - return Task.CompletedTask; - } - - public async Task PlayAsync(double position = 0) - { - await avPlayer.SeekAsync(new CoreMedia.CMTime((long)position, 1)); - avPlayer?.Play(); - - } -} diff --git a/src/Mobile/Platforms/Windows/AudioService.cs b/src/Mobile/Platforms/Windows/AudioService.cs deleted file mode 100644 index d97e4b0..0000000 --- a/src/Mobile/Platforms/Windows/AudioService.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Windows.Media.Core; -using Windows.Media.Playback; - -namespace Microsoft.NetConf2021.Maui.Platforms.Windows; - -public class AudioService : IAudioService -{ - string _uri; - MediaPlayer mediaPlayer; - - public bool IsPlaying => mediaPlayer != null - && mediaPlayer.CurrentState == MediaPlayerState.Playing; - - public double CurrentPosition => (long)mediaPlayer?.Position.TotalSeconds; - - public async Task InitializeAsync(string audioURI) - { - _uri = audioURI; - - if(this.mediaPlayer == null) - { - this.mediaPlayer = new MediaPlayer - { - Source = MediaSource.CreateFromUri(new Uri(_uri)), - AudioCategory = MediaPlayerAudioCategory.Media - }; - } - if (this.mediaPlayer != null) - { - await PauseAsync(); - this.mediaPlayer.Source = MediaSource.CreateFromUri(new Uri(_uri)); - } - - } - - public Task PauseAsync() - { - this.mediaPlayer?.Pause(); - return Task.CompletedTask; - } - - public Task PlayAsync(double position = 0) - { - if (this.mediaPlayer != null) - { - mediaPlayer.Position = TimeSpan.FromSeconds(position); - mediaPlayer.Play(); - } - - return Task.CompletedTask; - } -} diff --git a/src/Mobile/Podcasts.DotnetMaui.sln b/src/Mobile/Podcasts.DotnetMaui.sln index 7e09d5d..7267af5 100644 --- a/src/Mobile/Podcasts.DotnetMaui.sln +++ b/src/Mobile/Podcasts.DotnetMaui.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Components", "Components", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.Components", "..\Web\Components\Podcast.Components.csproj", "{39AD8B68-B8D3-43D4-86D4-3589E4AE81AD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedMauiLib", "..\Lib\SharedMauiLib\SharedMauiLib.csproj", "{772830C6-56B3-4A57-A8E1-50A715F149F5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +33,10 @@ Global {39AD8B68-B8D3-43D4-86D4-3589E4AE81AD}.Debug|Any CPU.Build.0 = Debug|Any CPU {39AD8B68-B8D3-43D4-86D4-3589E4AE81AD}.Release|Any CPU.ActiveCfg = Release|Any CPU {39AD8B68-B8D3-43D4-86D4-3589E4AE81AD}.Release|Any CPU.Build.0 = Release|Any CPU + {772830C6-56B3-4A57-A8E1-50A715F149F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {772830C6-56B3-4A57-A8E1-50A715F149F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {772830C6-56B3-4A57-A8E1-50A715F149F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {772830C6-56B3-4A57-A8E1-50A715F149F5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Mobile/Services/IAudioService.cs b/src/Mobile/Services/IAudioService.cs deleted file mode 100644 index 7191048..0000000 --- a/src/Mobile/Services/IAudioService.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Microsoft.NetConf2021.Maui.Services; - -public interface IAudioService -{ - Task InitializeAsync(string audioURI); - Task PlayAsync(double position = 0); - Task PauseAsync(); - bool IsPlaying { get; } - double CurrentPosition { get; } -} diff --git a/src/Mobile/Services/PlayerService.cs b/src/Mobile/Services/PlayerService.cs index 9f8f314..fd19915 100644 --- a/src/Mobile/Services/PlayerService.cs +++ b/src/Mobile/Services/PlayerService.cs @@ -1,8 +1,10 @@ -namespace Microsoft.NetConf2021.Maui.Services; +using SharedMauiLib; + +namespace Microsoft.NetConf2021.Maui.Services; public class PlayerService { - private readonly IAudioService audioService; + private readonly INativeAudioService audioService; private readonly WifiOptionsService wifiOptionsService; public Episode CurrentEpisode { get; set; } @@ -14,10 +16,16 @@ public class PlayerService public event EventHandler NewEpisodeAdded; public event EventHandler IsPlayingChanged; - public PlayerService(IAudioService audioService, WifiOptionsService wifiOptionsService) + public PlayerService(INativeAudioService audioService, WifiOptionsService wifiOptionsService) { this.audioService = audioService; this.wifiOptionsService = wifiOptionsService; + + this.audioService.IsPlayingChanged += (object sender, bool e) => + { + IsPlaying = e; + IsPlayingChanged?.Invoke(this, EventArgs.Empty); + }; } public async Task PlayAsync(Episode episode, Show show, bool isPlaying, double position = 0) @@ -39,27 +47,13 @@ public class PlayerService await audioService.InitializeAsync(CurrentEpisode.Url.ToString()); - if (isPlaying) - { - await InternalPlayAsync(initializePlayer: false, position); - } - else - { - await InternalPauseAsync(); - } + await InternalPlayPauseAsync(isPlaying, position); NewEpisodeAdded?.Invoke(this, EventArgs.Empty); } else { - if (isPlaying) - { - await InternalPlayAsync(initializePlayer: false, position); - } - else - { - await InternalPauseAsync(); - } + await InternalPlayPauseAsync(isPlaying, position); } IsPlayingChanged?.Invoke(this, EventArgs.Empty); @@ -75,13 +69,25 @@ public class PlayerService return PlayAsync(episode, show, isPlaying, position); } + private async Task InternalPlayPauseAsync(bool isPlaying, double position) + { + if (isPlaying) + { + await InternalPlayAsync(position); + } + else + { + await InternalPauseAsync(); + } + } + private async Task InternalPauseAsync() { await audioService.PauseAsync(); IsPlaying = false; } - private async Task InternalPlayAsync(bool initializePlayer = false, double position = 0) + private async Task InternalPlayAsync(double position = 0) { var canPlay = await wifiOptionsService.HasWifiOrCanPlayWithOutWifiAsync(); @@ -90,11 +96,6 @@ public class PlayerService return; } - if (initializePlayer) - { - await audioService.InitializeAsync(CurrentEpisode.Url.ToString()); - } - await audioService.PlayAsync(position); IsPlaying = true; } diff --git a/src/Mobile/Services/ServicesExtensions.cs b/src/Mobile/Services/ServicesExtensions.cs index b9d12c6..17015cd 100644 --- a/src/Mobile/Services/ServicesExtensions.cs +++ b/src/Mobile/Services/ServicesExtensions.cs @@ -11,14 +11,14 @@ public static class ServicesExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); #if WINDOWS - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); #elif ANDROID - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); #elif MACCATALYST - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton< Platforms.MacCatalyst.ConnectivityService>(); #elif IOS - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); #endif builder.Services.TryAddTransient(); diff --git a/src/MobileBlazor/Podcast.DotnetMaui.Blazor.sln b/src/MobileBlazor/Podcast.DotnetMaui.Blazor.sln index a7f2002..9003c5c 100644 --- a/src/MobileBlazor/Podcast.DotnetMaui.Blazor.sln +++ b/src/MobileBlazor/Podcast.DotnetMaui.Blazor.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.Pages", "..\Web\Pag EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.Components", "..\Web\Components\Podcast.Components.csproj", "{BFA06A28-839A-4324-87EE-2FF6B8D522A2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedMauiLib", "..\Lib\SharedMauiLib\SharedMauiLib.csproj", "{4A34B6DA-AE7F-4C6B-B8A8-CDC14A2CA178}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,10 @@ Global {BFA06A28-839A-4324-87EE-2FF6B8D522A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {BFA06A28-839A-4324-87EE-2FF6B8D522A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {BFA06A28-839A-4324-87EE-2FF6B8D522A2}.Release|Any CPU.Build.0 = Release|Any CPU + {4A34B6DA-AE7F-4C6B-B8A8-CDC14A2CA178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A34B6DA-AE7F-4C6B-B8A8-CDC14A2CA178}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A34B6DA-AE7F-4C6B-B8A8-CDC14A2CA178}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A34B6DA-AE7F-4C6B-B8A8-CDC14A2CA178}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MobileBlazor/mauiapp/MauiProgram.cs b/src/MobileBlazor/mauiapp/MauiProgram.cs index 524fdf4..ced6379 100644 --- a/src/MobileBlazor/mauiapp/MauiProgram.cs +++ b/src/MobileBlazor/mauiapp/MauiProgram.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Infrastructure; -using Microsoft.AspNetCore.Components.WebView.Maui; +using NetPodsMauiBlazor.Services; using Podcast.Components; using Podcast.Pages.Data; using Podcast.Shared; @@ -25,8 +25,19 @@ public static class MauiProgram { client.BaseAddress = new Uri(APIUrl); }); + +#if WINDOWS + builder.Services.AddSingleton(); +#elif ANDROID + builder.Services.AddSingleton(); +#elif MACCATALYST + builder.Services.AddSingleton(); +#elif IOS + builder.Services.AddSingleton(); +#endif + builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/MobileBlazor/mauiapp/NetPodsMauiBlazor.csproj b/src/MobileBlazor/mauiapp/NetPodsMauiBlazor.csproj index 09f01ab..6e07348 100644 --- a/src/MobileBlazor/mauiapp/NetPodsMauiBlazor.csproj +++ b/src/MobileBlazor/mauiapp/NetPodsMauiBlazor.csproj @@ -47,6 +47,7 @@ + diff --git a/src/MobileBlazor/mauiapp/Platforms/Android/AndroidManifest.xml b/src/MobileBlazor/mauiapp/Platforms/Android/AndroidManifest.xml index deea10d..21f29e5 100644 --- a/src/MobileBlazor/mauiapp/Platforms/Android/AndroidManifest.xml +++ b/src/MobileBlazor/mauiapp/Platforms/Android/AndroidManifest.xml @@ -4,4 +4,5 @@ + \ No newline at end of file diff --git a/src/MobileBlazor/mauiapp/Platforms/Android/MainActivity.cs b/src/MobileBlazor/mauiapp/Platforms/Android/MainActivity.cs index c3d3f91..b4dd99e 100644 --- a/src/MobileBlazor/mauiapp/Platforms/Android/MainActivity.cs +++ b/src/MobileBlazor/mauiapp/Platforms/Android/MainActivity.cs @@ -1,10 +1,37 @@ using Android.App; +using Android.Content; using Android.Content.PM; -using Microsoft.Maui; +using Android.OS; +using SharedMauiLib.Platforms.Android; +using SharedMauiLib.Platforms.Android.CurrentActivity; namespace NetPodsMauiBlazor; [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)] -public class MainActivity : MauiAppCompatActivity +public class MainActivity : MauiAppCompatActivity, IAudioActivity { + MediaPlayerServiceConnection mediaPlayerServiceConnection; + + public MediaPlayerServiceBinder Binder { get; set; } + + public event StatusChangedEventHandler StatusChanged; + public event CoverReloadedEventHandler CoverReloaded; + public event PlayingEventHandler Playing; + public event BufferingEventHandler Buffering; + + protected override void OnCreate(Bundle savedInstanceState) + { + base.OnCreate(savedInstanceState); + CrossCurrentActivity.Current.Init(this, savedInstanceState); + NotificationHelper.CreateNotificationChannel(ApplicationContext); + if (mediaPlayerServiceConnection == null) + InitializeMedia(); + } + + private void InitializeMedia() + { + mediaPlayerServiceConnection = new MediaPlayerServiceConnection(this); + var mediaPlayerServiceIntent = new Intent(ApplicationContext, typeof(MediaPlayerService)); + BindService(mediaPlayerServiceIntent, mediaPlayerServiceConnection, Bind.AutoCreate); + } } diff --git a/src/MobileBlazor/mauiapp/Services/AudioInteropService.cs b/src/MobileBlazor/mauiapp/Services/AudioInteropService.cs new file mode 100644 index 0000000..acc6a94 --- /dev/null +++ b/src/MobileBlazor/mauiapp/Services/AudioInteropService.cs @@ -0,0 +1,76 @@ +using SharedMauiLib; +using Microsoft.AspNetCore.Components; +using Podcast.Components; +using Podcast.Pages.Data; +using System.Timers; + +namespace NetPodsMauiBlazor.Services; + +internal class AudioInteropService : IAudioInterop +{ + private readonly INativeAudioService _nativeAudioService; + private readonly PlayerService _playerService; + private readonly System.Timers.Timer currentTimeTimer; + + public AudioInteropService( + INativeAudioService nativeAudioService, + PlayerService playerService) + { + _nativeAudioService = nativeAudioService; + _playerService = playerService; + + currentTimeTimer = new System.Timers.Timer(TimeSpan.FromSeconds(1).TotalMilliseconds); + currentTimeTimer.Elapsed += OnCurrentTimeEvent; + } + + public Task Pause(ElementReference element) + { + return _nativeAudioService.PauseAsync(); + } + + public async Task Play(ElementReference element) + { + await _nativeAudioService.PlayAsync(_nativeAudioService.CurrentPosition); + + } + + public async Task SetCurrentTime(ElementReference element, double value) + { + await _nativeAudioService.SetCurrentTime(value); + } + + public Task SetMuted(ElementReference element, bool value) + { + return _nativeAudioService.SetMuted(value); + } + + public void SetUri(string audioURI) + { + if (audioURI != null) + { + _nativeAudioService.InitializeAsync(audioURI).Wait(); + currentTimeTimer.Start(); + } + } + + public Task SetVolume(ElementReference element, int value) + { + return _nativeAudioService.SetVolume(value); + } + + public Task Stop(ElementReference element) + { + return _nativeAudioService.PauseAsync(); + } + + public ValueTask DisposeAsync() + { + currentTimeTimer.Dispose(); + return _nativeAudioService.DisposeAsync(); + } + + private void OnCurrentTimeEvent(Object source, ElapsedEventArgs e) + { + _playerService.CurrentTime = _nativeAudioService.CurrentPosition; + } +} diff --git a/src/Web/Client/Program.cs b/src/Web/Client/Program.cs index 5f1c7d4..957ed02 100644 --- a/src/Web/Client/Program.cs +++ b/src/Web/Client/Program.cs @@ -10,7 +10,7 @@ builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(builder.Configuration["PodcastApi:BaseAddress"]!); }); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/Web/Components/AudioInterop.cs b/src/Web/Components/AudioInterop.cs index cd825f7..be62500 100644 --- a/src/Web/Components/AudioInterop.cs +++ b/src/Web/Components/AudioInterop.cs @@ -3,7 +3,7 @@ using Microsoft.JSInterop; namespace Podcast.Components; -public class AudioInterop : IAsyncDisposable +public class AudioInterop : IAudioInterop ,IAsyncDisposable { private readonly Lazy> moduleTask; @@ -57,4 +57,9 @@ public class AudioInterop : IAsyncDisposable await module.DisposeAsync(); } } + + public void SetUri(string? audioURI) + { + //no action is necessary + } } \ No newline at end of file diff --git a/src/Web/Components/IAudioInterop.cs b/src/Web/Components/IAudioInterop.cs new file mode 100644 index 0000000..83cb1f6 --- /dev/null +++ b/src/Web/Components/IAudioInterop.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Components; + +namespace Podcast.Components; + +public interface IAudioInterop +{ + void SetUri(string? audioURI); + + Task Play(ElementReference element); + + Task Pause(ElementReference element); + + Task Stop(ElementReference element); + + Task SetMuted(ElementReference element, bool value); + + Task SetVolume(ElementReference element, int value); + + Task SetCurrentTime(ElementReference element, double value); + + ValueTask DisposeAsync(); +} diff --git a/src/Web/Pages/Shared/Player/Audio.razor b/src/Web/Pages/Shared/Player/Audio.razor index 5ea2cde..3e3552d 100644 --- a/src/Web/Pages/Shared/Player/Audio.razor +++ b/src/Web/Pages/Shared/Player/Audio.razor @@ -1,6 +1,6 @@ @implements IDisposable @inject PlayerService PlayerService -@inject AudioInterop AudioJsInterop +@inject IAudioInterop AudioJsInterop