A2dpMediaBrowserService.java revision ea14e69d8b1c7293869c0e4ae9180f36149a95b7
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.bluetooth.a2dpsink.mbs;
18
19import android.bluetooth.BluetoothAdapter;
20import android.bluetooth.BluetoothAvrcpController;
21import android.bluetooth.BluetoothDevice;
22import android.bluetooth.BluetoothProfile;
23import android.content.BroadcastReceiver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.media.browse.MediaBrowser;
28import android.media.browse.MediaBrowser.MediaItem;
29import android.media.MediaDescription;
30import android.media.MediaMetadata;
31import android.media.session.MediaController;
32import android.media.session.MediaSession;
33import android.media.session.PlaybackState;
34import android.os.Bundle;
35import android.os.Handler;
36import android.os.Looper;
37import android.os.Message;
38import android.os.Parcelable;
39import android.os.ResultReceiver;
40import android.service.media.MediaBrowserService;
41import android.util.Pair;
42import android.util.Log;
43
44import com.android.bluetooth.R;
45import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;
46import com.android.bluetooth.avrcpcontroller.BrowseTree;
47
48import java.lang.ref.WeakReference;
49import java.util.ArrayList;
50import java.util.HashMap;
51import java.util.List;
52import java.util.Map;
53
54/**
55 * Implements the MediaBrowserService interface to AVRCP and A2DP
56 *
57 * This service provides a means for external applications to access A2DP and AVRCP.
58 * The applications are expected to use MediaBrowser (see API) and all the music
59 * browsing/playback/metadata can be controlled via MediaBrowser and MediaController.
60 *
61 * The current behavior of MediaSession exposed by this service is as follows:
62 * 1. MediaSession is active (i.e. SystemUI and other overview UIs can see updates) when device is
63 * connected and first starts playing. Before it starts playing we do not active the session.
64 * 1.1 The session is active throughout the duration of connection.
65 * 2. The session is de-activated when the device disconnects. It will be connected again when (1)
66 * happens.
67 */
68public class A2dpMediaBrowserService extends MediaBrowserService {
69    private static final String TAG = "A2dpMediaBrowserService";
70    private static final String UNKNOWN_BT_AUDIO = "__UNKNOWN_BT_AUDIO__";
71    private static final float PLAYBACK_SPEED = 1.0f;
72
73    // Message sent when A2DP device is disconnected.
74    private static final int MSG_DEVICE_DISCONNECT = 0;
75    // Message sent when A2DP device is connected.
76    private static final int MSG_DEVICE_CONNECT = 2;
77    // Message sent when we recieve a TRACK update from AVRCP profile over a connected A2DP device.
78    private static final int MSG_TRACK = 4;
79    // Internal message sent to trigger a AVRCP action.
80    private static final int MSG_AVRCP_PASSTHRU = 5;
81    // Internal message to trigger a getplaystatus command to remote.
82    private static final int MSG_AVRCP_GET_PLAY_STATUS_NATIVE = 6;
83    // Message sent when AVRCP browse is connected.
84    private static final int MSG_DEVICE_BROWSE_CONNECT = 7;
85    // Message sent when AVRCP browse is disconnected.
86    private static final int MSG_DEVICE_BROWSE_DISCONNECT = 8;
87    // Message sent when folder list is fetched.
88    private static final int MSG_FOLDER_LIST = 9;
89
90    // Custom actions for PTS testing.
91    private String CUSTOM_ACTION_VOL_UP = "com.android.bluetooth.a2dpsink.mbs.CUSTOM_ACTION_VOL_UP";
92    private String CUSTOM_ACTION_VOL_DN = "com.android.bluetooth.a2dpsink.mbs.CUSTOM_ACTION_VOL_DN";
93    private String CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE =
94        "com.android.bluetooth.a2dpsink.mbs.CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE";
95
96    private MediaSession mSession;
97    private MediaMetadata mA2dpMetadata;
98
99    private AvrcpControllerService mAvrcpCtrlSrvc;
100    private boolean mBrowseConnected = false;
101    private BluetoothDevice mA2dpDevice = null;
102    private Handler mAvrcpCommandQueue;
103    private final Map<String, Result<List<MediaItem>>> mParentIdToRequestMap = new HashMap<>();
104    private static final List<MediaItem> mEmptyList = new ArrayList<MediaItem>();
105
106    // Browsing related structures.
107    private List<MediaItem> mNowPlayingList = null;
108
109    private long mTransportControlFlags = PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY
110            | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
111
112    private static final class AvrcpCommandQueueHandler extends Handler {
113        WeakReference<A2dpMediaBrowserService> mInst;
114
115        AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink) {
116            super(looper);
117            mInst = new WeakReference<A2dpMediaBrowserService>(sink);
118        }
119
120        @Override
121        public void handleMessage(Message msg) {
122            A2dpMediaBrowserService inst = mInst.get();
123            if (inst == null) {
124                Log.e(TAG, "Parent class has died; aborting.");
125                return;
126            }
127
128            switch (msg.what) {
129                case MSG_DEVICE_CONNECT:
130                    inst.msgDeviceConnect((BluetoothDevice) msg.obj);
131                    break;
132                case MSG_DEVICE_DISCONNECT:
133                    inst.msgDeviceDisconnect((BluetoothDevice) msg.obj);
134                    break;
135                case MSG_TRACK:
136                    Pair<PlaybackState, MediaMetadata> pair =
137                        (Pair<PlaybackState, MediaMetadata>) (msg.obj);
138                    inst.msgTrack(pair.first, pair.second);
139                    break;
140                case MSG_AVRCP_PASSTHRU:
141                    inst.msgPassThru((int) msg.obj);
142                    break;
143                case MSG_AVRCP_GET_PLAY_STATUS_NATIVE:
144                    inst.msgGetPlayStatusNative();
145                    break;
146                case MSG_DEVICE_BROWSE_CONNECT:
147                    inst.msgDeviceBrowseConnect((BluetoothDevice) msg.obj);
148                    break;
149                case MSG_DEVICE_BROWSE_DISCONNECT:
150                    inst.msgDeviceBrowseDisconnect((BluetoothDevice) msg.obj);
151                    break;
152                case MSG_FOLDER_LIST:
153                    inst.msgFolderList((Intent) msg.obj);
154                    break;
155                default:
156                    Log.e(TAG, "Message not handled " + msg);
157            }
158        }
159    }
160
161    @Override
162    public void onCreate() {
163        Log.d(TAG, "onCreate");
164        super.onCreate();
165
166        mSession = new MediaSession(this, TAG);
167        setSessionToken(mSession.getSessionToken());
168        mSession.setCallback(mSessionCallbacks);
169        mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
170                MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
171        mSession.setActive(true);
172        mAvrcpCommandQueue = new AvrcpCommandQueueHandler(Looper.getMainLooper(), this);
173
174        refreshInitialPlayingState();
175
176        IntentFilter filter = new IntentFilter();
177        filter.addAction(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
178        filter.addAction(AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED);
179        filter.addAction(AvrcpControllerService.ACTION_TRACK_EVENT);
180        filter.addAction(AvrcpControllerService.ACTION_FOLDER_LIST);
181        registerReceiver(mBtReceiver, filter);
182
183        synchronized (this) {
184            mParentIdToRequestMap.clear();
185        }
186    }
187
188    @Override
189    public void onDestroy() {
190        Log.d(TAG, "onDestroy");
191        mSession.release();
192        unregisterReceiver(mBtReceiver);
193        super.onDestroy();
194    }
195
196    @Override
197    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
198        return new BrowserRoot(BrowseTree.ROOT, null);
199    }
200
201    @Override
202    public synchronized void onLoadChildren(
203            final String parentMediaId, final Result<List<MediaItem>> result) {
204        if (mAvrcpCtrlSrvc == null) {
205            Log.e(TAG, "AVRCP not yet connected.");
206            result.sendResult(mEmptyList);
207            return;
208        }
209
210        Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId);
211        mAvrcpCtrlSrvc.getChildren(mA2dpDevice, parentMediaId, 0, 0xff);
212
213        // Since we are using this thread from a binder thread we should make sure that
214        // we synchronize against other such asynchronous calls.
215        synchronized (this) {
216            mParentIdToRequestMap.put(parentMediaId, result);
217        }
218        result.detach();
219    }
220
221    @Override
222    public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
223    }
224
225    // Media Session Stuff.
226    private MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() {
227        @Override
228        public void onPlay() {
229            Log.d(TAG, "onPlay");
230            mAvrcpCommandQueue.obtainMessage(
231                MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PLAY).sendToTarget();
232            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
233        }
234
235        @Override
236        public void onPause() {
237            Log.d(TAG, "onPause");
238            mAvrcpCommandQueue.obtainMessage(
239                MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE).sendToTarget();
240            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
241        }
242
243        @Override
244        public void onSkipToNext() {
245            Log.d(TAG, "onSkipToNext");
246            mAvrcpCommandQueue.obtainMessage(
247                MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD)
248                .sendToTarget();
249            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
250        }
251
252        @Override
253        public void onSkipToPrevious() {
254            Log.d(TAG, "onSkipToPrevious");
255
256            mAvrcpCommandQueue.obtainMessage(
257                MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD)
258                .sendToTarget();
259            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
260        }
261
262        @Override
263        public void onStop() {
264            Log.d(TAG, "onStop");
265            mAvrcpCommandQueue.obtainMessage(
266                    MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_STOP)
267                    .sendToTarget();
268        }
269
270        @Override
271        public void onRewind() {
272            Log.d(TAG, "onRewind");
273            mAvrcpCommandQueue.obtainMessage(
274                MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_REWIND).sendToTarget();
275            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
276        }
277
278        @Override
279        public void onFastForward() {
280            Log.d(TAG, "onFastForward");
281            mAvrcpCommandQueue.obtainMessage(
282                MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FF).sendToTarget();
283            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
284        }
285
286        @Override
287        public void onPlayFromMediaId(String mediaId, Bundle extras) {
288            synchronized (A2dpMediaBrowserService.this) {
289                // Play the item if possible.
290                mAvrcpCtrlSrvc.fetchAttrAndPlayItem(mA2dpDevice, mediaId);
291
292                // Since we request explicit playback here we should start the updates to UI.
293                mAvrcpCtrlSrvc.startAvrcpUpdates();
294            }
295
296            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
297        }
298
299        // Support VOL UP and VOL DOWN events for PTS testing.
300        @Override
301        public void onCustomAction(String action, Bundle extras) {
302            Log.d(TAG, "onCustomAction " + action);
303            if (CUSTOM_ACTION_VOL_UP.equals(action)) {
304                mAvrcpCommandQueue.obtainMessage(
305                    MSG_AVRCP_PASSTHRU,
306                    AvrcpControllerService.PASS_THRU_CMD_ID_VOL_UP).sendToTarget();
307            } else if (CUSTOM_ACTION_VOL_DN.equals(action)) {
308                mAvrcpCommandQueue.obtainMessage(
309                    MSG_AVRCP_PASSTHRU,
310                    AvrcpControllerService.PASS_THRU_CMD_ID_VOL_DOWN).sendToTarget();
311            } else if (CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE.equals(action)) {
312                mAvrcpCommandQueue.obtainMessage(
313                    MSG_AVRCP_GET_PLAY_STATUS_NATIVE).sendToTarget();
314            }else {
315                Log.w(TAG, "Custom action " + action + " not supported.");
316            }
317        }
318    };
319
320    private BroadcastReceiver mBtReceiver = new BroadcastReceiver() {
321        @Override
322        public void onReceive(Context context, Intent intent) {
323            Log.d(TAG, "onReceive intent=" + intent);
324            String action = intent.getAction();
325            BluetoothDevice btDev =
326                    (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
327            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
328
329            if (BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
330                Log.d(TAG, "handleConnectionStateChange: newState="
331                        + state + " btDev=" + btDev);
332
333                // Connected state will be handled when AVRCP BluetoothProfile gets connected.
334                if (state == BluetoothProfile.STATE_CONNECTED) {
335                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_CONNECT, btDev).sendToTarget();
336                } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
337                    // Set the playback state to unconnected.
338                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_DISCONNECT, btDev).sendToTarget();
339                    // If we have been pushing updates via the session then stop sending them since
340                    // we are not connected anymore.
341                    if (mSession.isActive()) {
342                        mSession.setActive(false);
343                    }
344                }
345            } else if (AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED.equals(
346                action)) {
347                if (state == BluetoothProfile.STATE_CONNECTED) {
348                    mAvrcpCommandQueue.obtainMessage(
349                        MSG_DEVICE_BROWSE_CONNECT, btDev).sendToTarget();
350                } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
351                    mAvrcpCommandQueue.obtainMessage(
352                        MSG_DEVICE_BROWSE_DISCONNECT, btDev).sendToTarget();
353                }
354            } else if (AvrcpControllerService.ACTION_TRACK_EVENT.equals(action)) {
355                PlaybackState pbb =
356                    intent.getParcelableExtra(AvrcpControllerService.EXTRA_PLAYBACK);
357                MediaMetadata mmd =
358                    intent.getParcelableExtra(AvrcpControllerService.EXTRA_METADATA);
359                mAvrcpCommandQueue.obtainMessage(
360                    MSG_TRACK, new Pair<PlaybackState, MediaMetadata>(pbb, mmd)).sendToTarget();
361            } else if (AvrcpControllerService.ACTION_FOLDER_LIST.equals(action)) {
362                mAvrcpCommandQueue.obtainMessage(MSG_FOLDER_LIST, intent).sendToTarget();
363            }
364        }
365    };
366
367    private synchronized void msgDeviceConnect(BluetoothDevice device) {
368        Log.d(TAG, "msgDeviceConnect");
369        // We are connected to a new device via A2DP now.
370        mA2dpDevice = device;
371        mAvrcpCtrlSrvc = AvrcpControllerService.getAvrcpControllerService();
372        if (mAvrcpCtrlSrvc == null) {
373            Log.e(TAG, "!!!AVRCP Controller cannot be null");
374            return;
375        }
376        refreshInitialPlayingState();
377    }
378
379
380    // Refresh the UI if we have a connected device and AVRCP is initialized.
381    private synchronized void refreshInitialPlayingState() {
382        if (mA2dpDevice == null) {
383            Log.d(TAG, "device " + mA2dpDevice);
384            return;
385        }
386
387        List<BluetoothDevice> devices = mAvrcpCtrlSrvc.getConnectedDevices();
388        if (devices.size() == 0) {
389            Log.w(TAG, "No devices connected yet");
390            return;
391        }
392
393        if (mA2dpDevice != null && !mA2dpDevice.equals(devices.get(0))) {
394            Log.e(TAG, "A2dp device : " + mA2dpDevice + " avrcp device " + devices.get(0));
395            return;
396        }
397        mA2dpDevice = devices.get(0);
398
399        PlaybackState playbackState = mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice);
400        // Add actions required for playback and rebuild the object.
401        PlaybackState.Builder pbb = new PlaybackState.Builder(playbackState);
402        playbackState = pbb.setActions(mTransportControlFlags).build();
403
404        MediaMetadata mediaMetadata = mAvrcpCtrlSrvc.getMetaData(mA2dpDevice);
405        Log.d(TAG, "Media metadata " + mediaMetadata + " playback state " + playbackState);
406        mSession.setMetadata(mAvrcpCtrlSrvc.getMetaData(mA2dpDevice));
407        mSession.setPlaybackState(playbackState);
408    }
409
410    private void msgDeviceDisconnect(BluetoothDevice device) {
411        Log.d(TAG, "msgDeviceDisconnect");
412        if (mA2dpDevice == null) {
413            Log.w(TAG, "Already disconnected - nothing to do here.");
414            return;
415        } else if (!mA2dpDevice.equals(device)) {
416            Log.e(TAG, "Not the right device to disconnect current " +
417                mA2dpDevice + " dc " + device);
418            return;
419        }
420
421        // Unset the session.
422        PlaybackState.Builder pbb = new PlaybackState.Builder();
423        pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN,
424                    PLAYBACK_SPEED)
425                .setActions(mTransportControlFlags)
426                .setErrorMessage(getString(R.string.bluetooth_disconnected));
427        mSession.setPlaybackState(pbb.build());
428
429        // Set device to null.
430        mA2dpDevice = null;
431        mBrowseConnected = false;
432    }
433
434    private void msgTrack(PlaybackState pb, MediaMetadata mmd) {
435        Log.d(TAG, "msgTrack: playback: " + pb + " mmd: " + mmd);
436        // Log the current track position/content.
437        MediaController controller = mSession.getController();
438        PlaybackState prevPS = controller.getPlaybackState();
439        MediaMetadata prevMM = controller.getMetadata();
440
441        if (prevPS != null) {
442            Log.d(TAG, "prevPS " + prevPS);
443        }
444
445        if (prevMM != null) {
446            String title = prevMM.getString(MediaMetadata.METADATA_KEY_TITLE);
447            long trackLen = prevMM.getLong(MediaMetadata.METADATA_KEY_DURATION);
448            Log.d(TAG, "prev MM title " + title + " track len " + trackLen);
449        }
450
451        if (mmd != null) {
452            Log.d(TAG, "msgTrack() mmd " + mmd.getDescription());
453            mSession.setMetadata(mmd);
454        }
455
456        if (pb != null) {
457            Log.d(TAG, "msgTrack() playbackstate " + pb);
458            PlaybackState.Builder pbb = new PlaybackState.Builder(pb);
459            pb = pbb.setActions(mTransportControlFlags).build();
460            mSession.setPlaybackState(pb);
461
462            // If we are now playing then we should start pushing updates via MediaSession so that
463            // external UI (such as SystemUI) can show the currently playing music.
464            if (pb.getState() == PlaybackState.STATE_PLAYING && !mSession.isActive()) {
465                mSession.setActive(true);
466            }
467        }
468    }
469
470    private synchronized void msgPassThru(int cmd) {
471        Log.d(TAG, "msgPassThru " + cmd);
472        if (mA2dpDevice == null) {
473            // We should have already disconnected - ignore this message.
474            Log.e(TAG, "Already disconnected ignoring.");
475            return;
476        }
477
478        // Send the pass through.
479        mAvrcpCtrlSrvc.sendPassThroughCmd(
480            mA2dpDevice, cmd, AvrcpControllerService.KEY_STATE_PRESSED);
481        mAvrcpCtrlSrvc.sendPassThroughCmd(
482            mA2dpDevice, cmd, AvrcpControllerService.KEY_STATE_RELEASED);
483    }
484
485    private synchronized void msgGetPlayStatusNative() {
486        Log.d(TAG, "msgGetPlayStatusNative");
487        if (mA2dpDevice == null) {
488            // We should have already disconnected - ignore this message.
489            Log.e(TAG, "Already disconnected ignoring.");
490            return;
491        }
492
493        // Ask for a non cached version.
494        mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice, false);
495    }
496
497    private void msgDeviceBrowseConnect(BluetoothDevice device) {
498        Log.d(TAG, "msgDeviceBrowseConnect device " + device);
499        // We should already be connected to this device over A2DP.
500        if (!device.equals(mA2dpDevice)) {
501            Log.e(TAG, "Browse connected over different device a2dp " + mA2dpDevice +
502                " browse " + device);
503            return;
504        }
505        mBrowseConnected = true;
506    }
507
508    private void msgFolderList(Intent intent) {
509        // Parse the folder list for children list and id.
510        List<Parcelable> extraParcelableList =
511            (ArrayList<Parcelable>) intent.getParcelableArrayListExtra(
512                AvrcpControllerService.EXTRA_FOLDER_LIST);
513        List<MediaItem> folderList = new ArrayList<MediaItem>();
514        for (Parcelable p : extraParcelableList) {
515            folderList.add((MediaItem) p);
516        }
517
518        String id = intent.getStringExtra(AvrcpControllerService.EXTRA_FOLDER_ID);
519        Log.d(TAG, "Parent: " + id + " Folder list: " + folderList);
520        synchronized (this) {
521            // If we have a result object then we should send the result back
522            // to client since it is blocking otherwise we may have gotten more items
523            // from remote device, hence let client know to fetch again.
524            Result<List<MediaItem>> results = mParentIdToRequestMap.remove(id);
525            if (results == null) {
526                Log.w(TAG, "Request no longer exists, notifying that children changed.");
527                notifyChildrenChanged(id);
528            } else {
529                results.sendResult(folderList);
530            }
531        }
532    }
533
534    private void msgDeviceBrowseDisconnect(BluetoothDevice device) {
535        Log.d(TAG, "msgDeviceBrowseDisconnect device " + device);
536        // Disconnect only if mA2dpDevice is non null
537        if (!device.equals(mA2dpDevice)) {
538            Log.w(TAG, "Browse disconnecting from different device a2dp " + mA2dpDevice +
539                " browse " + device);
540            return;
541        }
542        mBrowseConnected = false;
543    }
544}
545