Search code examples
c#androidxamarinandroid-intentandroid-activity

Xamarin. Having trouble correctly implementing AudioManager AudioFocus activity and listeners


I have a media podcast app built with xamarin forms and I am attempting to implement MediaManager's AudioFocus so my app can play well with other media apps. My main issue is being able to call RequestAudioFocus() when the user first clicks play and then also get the listener to work if another app starts playing media while my app is in the background. So both audios are not playing at the same time.

If I stick this code in MainActivity then the request works fine but also I don't really want the request to happen when the user opens the app. I want it to happen when the user clicks play.

I built a notification a while back that pops up a notification when the user first loads an episode that has a play/pause button in there as well that starts an activity to play/pause the episode. If I stick this code in that activity then everything works pretty well, even the listener in regard to other apps interfering. But again, I don't want to request audio focus only when they click the notification outside the app, I want it to start when they also click the play button inside the app.

So now I've built out another activity (different than the notification one). But I don't completely understand yet how to call this activity. If someone could point me in the right direction that would be great. I've been researching docs and other tutorials but I could use a nudge in the right direction. Thanks for your time.

One of the resources I am using can be found at https://www.sitepoint.com/managing-multiple-sound-sources-in-android-with-audio-focus/

This returns false every time. Doesn't get past the initialization of AudioManager.

 public bool RequestAudioFocus()
        {
            AudioManager audioManager = (AudioManager)GetSystemService(AudioService);
            AudioFocusRequest audioFocusRequest;
            if (Build.VERSION.SdkInt > BuildVersionCodes.O)
            {
                audioFocusRequest = audioManager.RequestAudioFocus(new AudioFocusRequestClass.Builder(AudioFocus.Gain)
                .SetAudioAttributes(new AudioAttributes.Builder().SetLegacyStreamType(Android.Media.Stream.Music).Build()).SetOnAudioFocusChangeListener(this)
                .Build());
            }
            else
            {
                audioFocusRequest = audioManager.RequestAudioFocus(this, Android.Media.Stream.Music, AudioFocus.Gain);
            }

            if (audioFocusRequest == AudioFocusRequest.Granted)
            {
                return true;
            }
            return false;
        }

        public void Init(DabPlayer Player, bool IntegrateWithLockScreen)
        {
            dabplayer = Player;
            var mSession = new MediaSessionCompat(Application.Context, "MusicService");
            mSession.SetFlags(MediaSessionCompat.FlagHandlesMediaButtons | MediaSessionCompat.FlagHandlesTransportControls);
            var controller = mSession.Controller;
            var description = GlobalResources.playerPodcast;

            if (IntegrateWithLockScreen)
            {
                /* SET UP LOCK SCREEN */
                CreateNotificationChannel();

                dabplayer.EpisodeDataChanged += (sender, e) =>
                {
                    bool focus = RequestAudioFocus();

                    if (focus)
                    {
                        // Set up an intent so that tapping the notifications returns to this app:
                        Intent intent = new Intent(Application.Context, typeof(MainActivity));
                        Intent playPauseIntent = new Intent(Application.Context, typeof(SecondActivity));
                        //Intent audioFocusIntent = new Intent(Application.Context, typeof(AudioFocusActivity));

                        // Create a PendingIntent; 
                        const int pendingIntentId = 0;
                        const int firstPendingIntentId = 1;
                        //const int audioFocusIntentId = 2;
                        //PendingIntent audioFocusPendingIntent =
                        //    PendingIntent.GetActivity(Application.Context, audioFocusIntentId, audioFocusIntent, 0);
                        PendingIntent firstPendingIntent =
                            PendingIntent.GetActivity(Application.Context, firstPendingIntentId, intent, 0);
                        PendingIntent pendingIntent =
                            PendingIntent.GetActivity(Application.Context, pendingIntentId, playPauseIntent, 0);

                        // Build the notification:
                        var builder = new NotificationCompat.Builder(Application.Context, CHANNEL_ID)
                                      .SetStyle(new Android.Support.V4.Media.App.NotificationCompat.MediaStyle()
                                                .SetMediaSession(mSession.SessionToken)
                                                .SetShowActionsInCompactView(0))
                                      .SetVisibility(NotificationCompat.VisibilityPublic)
                                      .SetContentIntent(firstPendingIntent) // Start up this activity when the user clicks the intent.
                                      .SetDeleteIntent(MediaButtonReceiver.BuildMediaButtonPendingIntent(Application.Context, PlaybackState.ActionStop))
                                      .SetSmallIcon(Resource.Drawable.app_icon) // This is the icon to display
                                      .AddAction(Resource.Drawable.ic_media_play_pause, "Play", pendingIntent)
                                      .SetContentText(GlobalResources.playerPodcast.EpisodeTitle)
                                      .SetContentTitle(GlobalResources.playerPodcast.ChannelTitle);

                        // Finally, publish the notification:
                        var notificationManager = NotificationManagerCompat.From(Application.Context);
                        notificationManager.Notify(NOTIFICATION_ID, builder.Build());


                        //StartActivity(audioFocusIntent);  causes trying to invoke virtual method on 
                        //null object reference when code and code above is uncommented
                    }
                };

                dabplayer.EpisodeProgressChanged += (object sender, EventArgs e) =>
                {

                };               
            }
        }

This is the activity that was originally just supposed to be used for the notification area

[Activity]
    public class SecondActivity :  Activity, AudioManager.IOnAudioFocusChangeListener
    {
        DabPlayer player = GlobalResources.playerPodcast;
        EpisodeViewModel Episode;

        public bool RequestAudioFocus()
        {
            AudioManager audioManager = (AudioManager)GetSystemService(AudioService);
            AudioFocusRequest audioFocusRequest;
            if (Build.VERSION.SdkInt > BuildVersionCodes.O)
            {
                audioFocusRequest = audioManager.RequestAudioFocus(new AudioFocusRequestClass.Builder(AudioFocus.Gain)
                .SetAudioAttributes(new AudioAttributes.Builder().SetLegacyStreamType(Android.Media.Stream.Music).Build()).SetOnAudioFocusChangeListener(this)
                .Build());
            }
            else
            {
                audioFocusRequest = audioManager.RequestAudioFocus(this, Android.Media.Stream.Music, AudioFocus.Gain);
            }

            if (audioFocusRequest == AudioFocusRequest.Granted)
            {
                return true;
            }
            return false;
        }


        public void OnAudioFocusChange([GeneratedEnum] AudioFocus focusChange)
        {
            switch (focusChange)
            {
                case AudioFocus.Gain:
                    player.Play();
                    //Gain when other Music Player app releases the audio service   
                    break;
                case AudioFocus.Loss:
                    //We have lost focus stop!   
                    player.Stop();
                    break;
                case AudioFocus.LossTransient:
                    //We have lost focus for a short time, but likely to resume so pause   
                    player.Pause();
                    break;
                case AudioFocus.LossTransientCanDuck:
                    //We have lost focus but should till play at a muted 10% volume   
                    //player.SetVolume(.1);
                    break;
            }
        }

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);



            if (player.IsReady)
            {
                if (player.IsPlaying)
                {
                    player.Pause();
                }
                else
                {
                    if (RequestAudioFocus())
                    {
                        player.Play();
                    }
                }
            }
            else
            {
                if (player.Load(Episode.Episode))
                {
                    if (RequestAudioFocus())
                    {
                        player.Play();
                    }
                }
                else
                {
                    //DisplayAlert("Episode Unavailable", "The episode you are attempting to play is currently unavailable. Please try again later.", "OK");
                }

            }

            Finish();          
        }      
    }

This is the new activity I built and hoping to use when the user interacts with the player but I'm not sure how to call/implement it.

[Activity]
    public class AudioFocusActivity : Activity, AudioManager.IOnAudioFocusChangeListener
    {
        DabPlayer player = GlobalResources.playerPodcast;
        DroidDabNativePlayer droid = new DroidDabNativePlayer();
        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            RequestAudioFocus();
        }

        public bool RequestAudioFocus()
        {
            AudioManager audioManager = (AudioManager)GetSystemService(AudioService);
            AudioFocusRequest audioFocusRequest;
            if (Build.VERSION.SdkInt > BuildVersionCodes.O)
            {
                audioFocusRequest = audioManager.RequestAudioFocus(new AudioFocusRequestClass.Builder(AudioFocus.Gain)
                .SetAudioAttributes(new AudioAttributes.Builder().SetLegacyStreamType(Android.Media.Stream.Music).Build()).SetOnAudioFocusChangeListener(this)
                .Build());
            }
            else
            {
                audioFocusRequest = audioManager.RequestAudioFocus(this, Android.Media.Stream.Music, AudioFocus.Gain);
            }

            if (audioFocusRequest == AudioFocusRequest.Granted)
            {
                return true;
            }
            return false;
        }


        public void OnAudioFocusChange([GeneratedEnum] AudioFocus focusChange)
        {
            switch (focusChange)
            {
                case AudioFocus.Gain:
                    player.Play();
                    //Gain when other Music Player app releases the audio service   
                    break;
                case AudioFocus.Loss:
                    //We have lost focus stop!   
                    player.Stop();
                    break;
                case AudioFocus.LossTransient:
                    //We have lost focus for a short time, but likely to resume so pause   
                    player.Pause();
                    break;
                case AudioFocus.LossTransientCanDuck:
                    //We have lost focus but should till play at a muted 10% volume   
                    //player.SetVolume(.1);
                    break;
            }
        }
    }

This is just my play method in my androidplayer class

 /// Begin playback or resume if paused
        public void Play()
        {
            if (player == null)
                return;

            if (IsPlaying)
            {
                //Go back to the beginning (don't start playing)... not sure what this is here for if if it ever gets hit.
                Pause();
                Seek(0);
            }
            else if (player.CurrentPosition >= player.Duration)
            {
                //Start over from the beginning if at the end of the file
                player.Pause();
                Seek(0);
            }
            else
            {
                //Play from where we're at

            }


            player.Start();
        }

Solution

  • If I were you I would put both your Audio Focus code and your Audio Player code in a ForegroundService and not the Activity, this way you can navigate to different activities, without interrupting Audio Playback. You will need to do something like that anyways if you want playback when the App is in the background.

    Implementing a ForegroundService will also let you add buttons to your notification or if you are using one of the rich Audio Notification, then you can send intents directly to your ForegroundService from the buttons on the notification, rather than having to open up your App. So for your pending intents you could communicate directly with the service instead.

    Also when requesting audio focus, you need to respect whether the audio focus was granted or not. Right now you are just requesting it and throwing away the result. Only if the focus was granted you should start playing back the audio.