使用绑定的前台服务保持音频播放

Keep audio playing with bound foreground service

我正在研究绑定服务、前台服务以及如何使用它们进行音频播放。

基于this example我设置了一个前台服务来播放音频。在我尝试绑定服务之前,这非常适合我的用例,因为我需要在 Activity 和服务之间来回传递一些数据,例如我要实现的搜索栏的播放位置。

我浏览了几篇 Whosebug 帖子,试图找到解决方案。我明白我应该在启动服务之前绑定它,这样它就不会连同它绑定的 Activity 一起被杀死。但这仍然会发生,只要我包含绑定机制。

当我让设备进入睡眠状态时,这是我在 logcat 中看到的唯一错误是:

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!

所以,我需要弄清楚频道被破坏的原因,但目前我不知道还能去哪里看。 …

这是我当前的 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();
    }
}
    }

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();
    }

您可以在 GitHub 上找到链接中的完整代码。

目的是让用户在收听时让他们的设备进入睡眠状态,或者让它自行进入睡眠状态。服务的绑定是必要的,以便在设备打开或 activity 被带到前台时显示播放进度(通过搜索栏和计数器)。

我哪里出错了或者我忽略了什么?或者我应该考虑更好的方法吗?在此先感谢您的指点和帮助。

预计到达时间: 我切换到未过滤的 logCat 并发现了一个新错误。我将开始粘贴保存外州和未绑定服务的位置。

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" 好像是key,但是我很纳闷是什么交易。因为我还没有在服务和 activity 之间进行通信。我开始怀疑服务中的 timerUpdateHandler。当设备进入睡眠状态时,我是否也应该停止通知?

问题不在于服务绑定,而在于触发 TansactionTooLargeExceptionOnSavedInstanceState。我会在这里提供我的解决方案,因为我怀疑其他人也可能 运行 遇到这个问题。

我需要为 Activity 重新训练的任何数据都会传送给服务。服务立即需要的是通过启动它的意图传递的。在我的例子中是音频 Uri 和标题。

 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();

Activity 需要在 UI 中显示的任何其他数据在我解除绑定时传递给服务。在我的例子中,coverArt 和 selected Object (selectedMed) 包含要显示的更多信息。

 //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");
    }

非常感谢@greeble31,感谢他仔细研究了所有的可能性和尝试,帮助我找到了根本原因!