javaandroidaudioforeground-servicebindservice

Keep audio playing with bound foreground service


I am getting my head around binding services, foreground services and how to use them for audio playback.

Based on this example I have set up a Foreground Service to play audio. This works perfectly for my use case until I try to bind the service, as I need to communicate some data back and forth between the Activity and the Service, e.g. playback position for the seekbar I want to implement.

I have gone over several StackOverflow posts trying to find the solution. I understand that I should bind the service before starting it so that it is not killed along with the Activity it is bound to. But that is still what happens, as soon as I include the binding mechanism.

When I put the device into sleep this is the only error I am seeing in the logcat is:

2019-11-12 10:49:24.553 812-871/? W/InputDispatcher: channel '7fc2cb2 com.example.android.meditationhub/com.example.android.meditationhub.ui.PlayActivity (server)' ~ Consumer closed input channel or an error occurred.  events=0x9
2019-11-12 10:49:24.553 812-871/? E/InputDispatcher: channel '7fc2cb2 com.example.android.meditationhub/com.example.android.meditationhub.ui.PlayActivity (server)' ~ Channel is unrecoverably broken and will be disposed!

So, I need to figure out why the channel is being broken, but am stumped at the moment where else to look. …

Here is my current setup of the PlayActivity:

//called from onCreate()
 private void initializeUI() {  
        playerBinding.playbackControlBt.setPlayListener(new AnimatePlayButton.OnButtonsListener() {
            @Override
            public boolean onPlayClick(View view) {
                mediaPlayerServiceInt = new Intent(PlayActivity.this, MediaPlayerService.class);
                mediaPlayerServiceInt.setAction(Constants.START_ACTION);
                mediaPlayerServiceInt.putExtra(Constants.URI, medUri);
                    startService(mediaPlayerServiceInt);
                    bindMediaPlayerService();
                return true;
            }

            @Override
            public boolean onPauseClick(View view) {
                Intent pausePlayback = new Intent(PlayActivity.this, MediaPlayerService.class);
                pausePlayback.setAction(Constants.PAUSE_ACTION);
                PendingIntent pendingPausePlayback = PendingIntent.getService(PlayActivity.this,
                        0, pausePlayback, PendingIntent.FLAG_UPDATE_CURRENT);
                try {
                    pendingPausePlayback.send();
                } catch (PendingIntent.CanceledException e) {
                    e.printStackTrace();
                }
                return true;
            }

            @Override
            public boolean onResumeClick(View view) {
                Intent resumePlayback = new Intent(PlayActivity.this, MediaPlayerService.class);
                resumePlayback.setAction(Constants.PLAY_ACTION);
                PendingIntent pendingResumePlayback = PendingIntent.getService(PlayActivity.this,
                        0, resumePlayback, PendingIntent.FLAG_UPDATE_CURRENT);
                try {
                    pendingResumePlayback.send();
                } catch (PendingIntent.CanceledException e) {
                    e.printStackTrace();
                }
                return true;
            }

            @Override
            public boolean onStopClick(View view) {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
                    stopService(mediaPlayerServiceInt);
                }
                return true;
            }
        });
    }

//monitor state of the service
    private ServiceConnection mediaPlayerConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mediaPlayerService = ((MediaPlayerService.MyBinder) service).getService();
            serviceIsBound = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mediaPlayerService = null;
            serviceIsBound = false;
        }
    };

    //unbind service so the audio continues to play
    private void unbindMediaPlayerService() {
        unbindService(mediaPlayerConnection);
        serviceIsBound = false;
        Timber.v("Service unbound");
    }

    //bind service so the display can follow the audio playback
    private void bindMediaPlayerService() {
        if (!serviceIsBound) {
            Intent bindInt = new Intent(this, MediaPlayerService.class);
            serviceIsBound = bindService(bindInt, mediaPlayerConnection, Context.BIND_AUTO_CREATE);
            Timber.v("Service bound");
        } else {
            Timber.v("no Service to bind");
        }
    }

@Override
    protected void onStart() {
        super.onStart();
        if (MediaPlayerService.getState() != Constants.STATE_NOT_INIT) {
            bindMediaPlayerService();
        }
@Override
protected void onStop() {
    super.onStop();
    if (MediaPlayerService.getState() != Constants.STATE_NOT_INIT) {
        unbindMediaPlayerService();
    }
}
    }

And the MediaPlayerService:

// Binder given to Activity
    private final IBinder binder = new MyBinder();

    /**
     * Class used for the client Binder. The Binder object is responsible for returning an instance
     * of {@link MediaPlayerService} to the client.
     */
    public class MyBinder extends Binder {
        public MediaPlayerService getService() {
            // Return this instance of MyService so clients can call public methods
            return MediaPlayerService.this;
        }
    }

    @Override
    public IBinder onBind(Intent arg0) {
        return binder;
    }

@Override
    public void onCreate() {
        super.onCreate();
        (…)

        stateService = Constants.STATE_NOT_INIT;
        notMan = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    }

    @Override
    public int onStartCommand(final Intent intent, int flags, int startId) {

        if (intent == null || intent.getAction() == null) {
            stopForeground(true);
            stopSelf();
            return START_NOT_STICKY;
        }

        switch (intent.getAction()) {
            case Constants.START_ACTION:
                if(intent.getExtras() != null) {
                    medUri = (Uri) intent.getExtras().get(Constants.URI);
                }
                stateService = Constants.STATE_PREPARE;

                startForeground(Constants.NOTIFICATION_ID_FOREGROUND_SERVICE, prepareNotification());
                destroyPlayer();
                initPlayer();
                play();
                break;

            case Constants.PAUSE_ACTION:
                stateService = Constants.STATE_PAUSE;
                notMan.notify(Constants.NOTIFICATION_ID_FOREGROUND_SERVICE, prepareNotification());
                destroyPlayer();
                handler.postDelayed(delayedShutdown, Constants.DELAY_SHUTDOWN_FOREGROUND_SERVICE);
                break;

            case Constants.PLAY_ACTION:
                stateService = Constants.STATE_PREPARE;
                notMan.notify(Constants.NOTIFICATION_ID_FOREGROUND_SERVICE, prepareNotification());
                destroyPlayer();
                initPlayer();
                play();
                break;

            case Constants.STOP_ACTION:
                Timber.i("Received Stop Intent");
                destroyPlayer();
                stopForeground(true);
                stopSelf();
                break;

            default:
                stopForeground(true);
                stopSelf();
        }
        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        Timber.d("onDestroy()");
        destroyPlayer();
        stateService = Constants.STATE_NOT_INIT;
        try {
            timerUpdateHandler.removeCallbacksAndMessages(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        super.onDestroy();
    }

You kind find the full code on GitHub as linked.

The aim is to allow the user to put their device to sleep as they listen, or let it go to sleep of its own accord. The binding of the service is necessary so as to display the progress of the playback (via seekbar and counter) whenever the device is turned on or the activity is brought to the foreground.

Where am I going wrong or what have I overlooked? Or is there a better approach I should consider? Thanks in advance for any pointers and help.

ETA: I've switched to the unfiltered logCat and found a new error. I will start pasting where the outstates are saved and service is unbound.

2019-11-14 09:57:42.942 8326-8326/com.example.android.meditationhub V/PlayActivity: all outstates saved
2019-11-14 09:57:42.943 8326-8326/com.example.android.meditationhub V/PlayActivity: all outstates saved
2019-11-14 09:57:42.944 8326-8326/com.example.android.meditationhub V/PlayActivity: all outstates saved
2019-11-14 09:57:42.950 8326-8326/com.example.android.meditationhub V/PlayActivity: Service unbound
2019-11-14 09:57:42.951 8326-8326/com.example.android.meditationhub V/PlayActivity: Service unbound
2019-11-14 09:57:42.953 225-1957/? I/BufferQueueProducer: [ColorFade](this:0xaab2a000,id:5867,api:1,p:812,c:225) new GraphicBuffer needed
2019-11-14 09:57:42.954 225-1957/? I/[MALI][Gralloc]: usage1: 0xf02, format: 1 stride: 720 vertical_stride: 1280 size: 3686400
2019-11-14 09:57:42.954 8326-8326/com.example.android.meditationhub V/PlayActivity: Service unbound
2019-11-14 09:57:42.955 225-1957/? D/GraphicBuffer: alloc, handle(0xac1921e0) (w:720 h:1280 s:720 f:0x1 u:0x000f02) err(0)
2019-11-14 09:57:42.958 812-859/? D/GraphicBuffer: register, handle(0x88e7d160) (w:720 h:1280 s:720 f:0x1 u:0x000f02)
2019-11-14 09:57:42.979 225-369/? I/BufferQueueProducer: [ColorFade](this:0xaab2a000,id:5867,api:1,p:812,c:225) new GraphicBuffer needed
2019-11-14 09:57:42.979 225-369/? I/[MALI][Gralloc]: usage1: 0xf02, format: 1 stride: 720 vertical_stride: 1280 size: 3686400
2019-11-14 09:57:42.980 225-369/? D/GraphicBuffer: alloc, handle(0xac1922a0) (w:720 h:1280 s:720 f:0x1 u:0x000f02) err(0)
2019-11-14 09:57:42.983 812-859/? D/GraphicBuffer: register, handle(0x88e789a0) (w:720 h:1280 s:720 f:0x1 u:0x000f02)
2019-11-14 09:57:42.994 812-3148/? D/PowerManagerNotifier: onWakeLockReleased: flags=1, tag="ActivityManager-Sleep", packageName=android, ownerUid=1000, ownerPid=812, workSource=null
2019-11-14 09:57:42.995 258-748/? D/Sunwave: sw_sensor 896:send cancel sem
2019-11-14 09:57:42.995 258-747/? I/Sunwave: sw-hal 281:irq canceled!
2019-11-14 09:57:42.995 258-747/? D/Sunwave: IC8221 1237:wait irq cancel 
2019-11-14 09:57:42.995 258-747/? D/Sunwave: IC8221 712:soft reset
2019-11-14 09:57:42.998 258-733/? D/Sunwave: sw_sensor 225:irq receive wake, but g_bIrqEnable = 0
2019-11-14 09:57:43.000 258-747/? D/Sunwave: IC8221 2573:warning: fp irq canceled
2019-11-14 09:57:43.005 258-747/? D/Sunwave: FpFinger 2405:keyFinger cancel:1
2019-11-14 09:57:43.006 812-859/? D/DisplayPowerController: ABC configure: enabledInDoze=false, lowDimmingProtectionEnabled=false, adjustment=0.7919922, state=2, brightness=-1
2019-11-14 09:57:43.006 812-859/? D/AutomaticBrightnessController: getAutomaticScreenBrightness: brightness=255, dozing=false, factor=1.0
2019-11-14 09:57:43.007 812-859/? D/DisplayPowerController: Unfinished business...
2019-11-14 09:57:43.023 8326-8849/com.example.android.meditationhub D/FA: Logging event (FE): user_engagement(_e), Bundle[{ga_event_origin(_o)=auto, engagement_time_msec(_et)=5780, ga_screen_class(_sc)=PlayActivity, ga_screen_id(_si)=7992583200822585152}]
2019-11-14 09:57:43.024 8326-8326/com.example.android.meditationhub E/JavaBinder: !!! FAILED BINDER TRANSACTION !!!  (parcel size = 895384)
2019-11-14 09:57:43.025 8326-8326/com.example.android.meditationhub D/AndroidRuntime: Shutting down VM
2019-11-14 09:57:43.026 943-943/? D/SystemServicesProxy: getTopMostTask: tasks: 2600

    --------- beginning of crash
2019-11-14 09:57:43.027 8326-8326/com.example.android.meditationhub E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.android.meditationhub, PID: 8326
    java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 895384 bytes
        at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3865)
        at android.os.Handler.handleCallback(Handler.java:836)
        at android.os.Handler.dispatchMessage(Handler.java:103)
        at android.os.Looper.loop(Looper.java:203)
        at android.app.ActivityThread.main(ActivityThread.java:6251)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1063)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:924)
     Caused by: android.os.TransactionTooLargeException: data parcel size 895384 bytes
        at android.os.BinderProxy.transactNative(Native Method)
        at android.os.BinderProxy.transact(Binder.java:622)
        at android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3708)
        at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3857)
        at android.os.Handler.handleCallback(Handler.java:836) 
        at android.os.Handler.dispatchMessage(Handler.java:103) 
        at android.os.Looper.loop(Looper.java:203) 
        at android.app.ActivityThread.main(ActivityThread.java:6251) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1063) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:924) 
2019-11-14 09:57:43.034 812-1424/? W/ActivityManager:   Force finishing activity com.example.android.meditationhub/.ui.PlayActivity

"Transaction too large" seems to be key, but I am puzzled over what transaction. As I am not communicating between service and activity as yet. I am starting suspected the timerUpdateHandler in the Service. Should I perhaps stop the notifications as well when the device goes to sleep?


Solution

  • The issue did not lie with the binding of the service but with OnSavedInstanceState, which was triggering the TansactionTooLargeException. I'll offer my solution here, as I suspect others might also run into this problem.

    Any data I need to retrain for the Activity is communicated to the Service. What the service needs immediately is passed via the intent that starts it. In my case that is the audio Uri and the title.

     mediaPlayerServiceInt = new Intent(PlayActivity.this, MediaPlayerService.class);
                    mediaPlayerServiceInt.setAction(Constants.START_ACTION);
                    mediaPlayerServiceInt.putExtra(Constants.URI, medUri);
                    mediaPlayerServiceInt.putExtra(Constants.TITLE, selectedMed.getTitle());
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        startForegroundService(mediaPlayerServiceInt);
                    } else {
                        startService(mediaPlayerServiceInt);
                    }
                    bindMediaPlayerService();
    

    Any other data the Activity needs to display in the UI is passed to the Service when I unbind it. In my case the coverArt and the the the selected Object (selectedMed) which holds further information to be displayed.

     //unbind service so the audio continues to play
        private void unbindMediaPlayerService() {
            mediaPlayerService.setCoverArt(coverArt);
            mediaPlayerService.setSelectedMed(selectedMed);
    
            unbindService(mediaPlayerConnection);
            serviceIsBound = false;
            Log.v(TAG, "Service unbound");
        }
    

    Big thank you to @greeble31 for going over all the possibilities and trials helping me whittle things down to the underlying cause!