A2dpMediaBrowserService.java revision 4b491c2c874395c436949183bcbd84ebb2493131
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    // Message sent when AVRCP browse is connected.
82    private static final int MSG_DEVICE_BROWSE_CONNECT = 6;
83    // Message sent when AVRCP browse is disconnected.
84    private static final int MSG_DEVICE_BROWSE_DISCONNECT = 7;
85    // Message sent when folder list is fetched.
86    private static final int MSG_FOLDER_LIST = 9;
87
88    private MediaSession mSession;
89    private MediaMetadata mA2dpMetadata;
90
91    private AvrcpControllerService mAvrcpCtrlSrvc;
92    private boolean mBrowseConnected = false;
93    private BluetoothDevice mA2dpDevice = null;
94    private Handler mAvrcpCommandQueue;
95    private final Map<String, Result<List<MediaItem>>> mParentIdToRequestMap = new HashMap<>();
96    private static final List<MediaItem> mEmptyList = new ArrayList<MediaItem>();
97
98    // Browsing related structures.
99    private List<MediaItem> mNowPlayingList = null;
100
101    private long mTransportControlFlags = PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY
102            | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
103
104    private static final class AvrcpCommandQueueHandler extends Handler {
105        WeakReference<A2dpMediaBrowserService> mInst;
106
107        AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink) {
108            super(looper);
109            mInst = new WeakReference<A2dpMediaBrowserService>(sink);
110        }
111
112        @Override
113        public void handleMessage(Message msg) {
114            A2dpMediaBrowserService inst = mInst.get();
115            if (inst == null) {
116                Log.e(TAG, "Parent class has died; aborting.");
117                return;
118            }
119
120            switch (msg.what) {
121                case MSG_DEVICE_CONNECT:
122                    inst.msgDeviceConnect((BluetoothDevice) msg.obj);
123                    break;
124                case MSG_DEVICE_DISCONNECT:
125                    inst.msgDeviceDisconnect((BluetoothDevice) msg.obj);
126                    break;
127                case MSG_TRACK:
128                    Pair<PlaybackState, MediaMetadata> pair =
129                        (Pair<PlaybackState, MediaMetadata>) (msg.obj);
130                    inst.msgTrack(pair.first, pair.second);
131                    break;
132                case MSG_AVRCP_PASSTHRU:
133                    inst.msgPassThru((int) msg.obj);
134                    break;
135                case MSG_DEVICE_BROWSE_CONNECT:
136                    inst.msgDeviceBrowseConnect((BluetoothDevice) msg.obj);
137                    break;
138                case MSG_DEVICE_BROWSE_DISCONNECT:
139                    inst.msgDeviceBrowseDisconnect((BluetoothDevice) msg.obj);
140                    break;
141                case MSG_FOLDER_LIST:
142                    inst.msgFolderList((Intent) msg.obj);
143                    break;
144                default:
145                    Log.e(TAG, "Message not handled " + msg);
146            }
147        }
148    }
149
150    @Override
151    public void onCreate() {
152        Log.d(TAG, "onCreate");
153        super.onCreate();
154
155        mSession = new MediaSession(this, TAG);
156        setSessionToken(mSession.getSessionToken());
157        mSession.setCallback(mSessionCallbacks);
158        mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
159                MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
160        mAvrcpCommandQueue = new AvrcpCommandQueueHandler(Looper.getMainLooper(), this);
161
162        refreshInitialPlayingState();
163
164        IntentFilter filter = new IntentFilter();
165        filter.addAction(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
166        filter.addAction(AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED);
167        filter.addAction(AvrcpControllerService.ACTION_TRACK_EVENT);
168        filter.addAction(AvrcpControllerService.ACTION_FOLDER_LIST);
169        registerReceiver(mBtReceiver, filter);
170
171        synchronized (this) {
172            mParentIdToRequestMap.clear();
173        }
174    }
175
176    @Override
177    public void onDestroy() {
178        Log.d(TAG, "onDestroy");
179        mSession.release();
180        unregisterReceiver(mBtReceiver);
181        super.onDestroy();
182    }
183
184    @Override
185    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
186        return new BrowserRoot(BrowseTree.ROOT, null);
187    }
188
189    @Override
190    public synchronized void onLoadChildren(
191            final String parentMediaId, final Result<List<MediaItem>> result) {
192        if (mAvrcpCtrlSrvc == null) {
193            Log.e(TAG, "AVRCP not yet connected.");
194            result.sendResult(mEmptyList);
195            return;
196        }
197
198        Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId);
199        mAvrcpCtrlSrvc.getChildren(mA2dpDevice, parentMediaId, 0, 0xff);
200
201        // Since we are using this thread from a binder thread we should make sure that
202        // we synchronize against other such asynchronous calls.
203        synchronized (this) {
204            mParentIdToRequestMap.put(parentMediaId, result);
205        }
206        result.detach();
207    }
208
209    @Override
210    public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
211    }
212
213    // Media Session Stuff.
214    private MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() {
215        @Override
216        public void onPlay() {
217            Log.d(TAG, "onPlay");
218            mAvrcpCommandQueue.obtainMessage(
219                MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PLAY).sendToTarget();
220            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
221        }
222
223        @Override
224        public void onPause() {
225            Log.d(TAG, "onPause");
226            mAvrcpCommandQueue.obtainMessage(
227                MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE).sendToTarget();
228            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
229        }
230
231        @Override
232        public void onSkipToNext() {
233            Log.d(TAG, "onSkipToNext");
234            mAvrcpCommandQueue.obtainMessage(
235                MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD)
236                .sendToTarget();
237            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
238        }
239
240        @Override
241        public void onSkipToPrevious() {
242            Log.d(TAG, "onSkipToPrevious");
243
244            mAvrcpCommandQueue.obtainMessage(
245                MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD)
246                .sendToTarget();
247            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
248        }
249
250        // These are not yet supported.
251        @Override
252        public void onStop() {
253            Log.d(TAG, "onStop");
254        }
255
256        @Override
257        public void onCustomAction(String action, Bundle extras) {
258            Log.d(TAG, "onCustomAction action=" + action + " extras=" + extras);
259        }
260
261        @Override
262        public void onPlayFromSearch(String query, Bundle extras) {
263            Log.d(TAG, "playFromSearch not supported in AVRCP");
264        }
265
266        @Override
267        public void onCommand(String command, Bundle args, ResultReceiver cb) {
268            Log.d(TAG, "onCommand command=" + command + " args=" + args);
269        }
270
271        @Override
272        public void onSkipToQueueItem(long queueId) {
273            Log.d(TAG, "onSkipToQueueItem");
274        }
275
276        @Override
277        public void onPlayFromMediaId(String mediaId, Bundle extras) {
278            synchronized (A2dpMediaBrowserService.this) {
279                // Play the item if possible.
280                mAvrcpCtrlSrvc.fetchAttrAndPlayItem(mA2dpDevice, mediaId);
281
282                // Since we request explicit playback here we should start the updates to UI.
283                mAvrcpCtrlSrvc.startAvrcpUpdates();
284            }
285
286            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
287        }
288    };
289
290    private BroadcastReceiver mBtReceiver = new BroadcastReceiver() {
291        @Override
292        public void onReceive(Context context, Intent intent) {
293            Log.d(TAG, "onReceive intent=" + intent);
294            String action = intent.getAction();
295            BluetoothDevice btDev =
296                    (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
297            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
298
299            if (BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
300                Log.d(TAG, "handleConnectionStateChange: newState="
301                        + state + " btDev=" + btDev);
302
303                // Connected state will be handled when AVRCP BluetoothProfile gets connected.
304                if (state == BluetoothProfile.STATE_CONNECTED) {
305                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_CONNECT, btDev).sendToTarget();
306                } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
307                    // Set the playback state to unconnected.
308                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_DISCONNECT, btDev).sendToTarget();
309                    // If we have been pushing updates via the session then stop sending them since
310                    // we are not connected anymore.
311                    if (mSession.isActive()) {
312                        mSession.setActive(false);
313                    }
314                }
315            } else if (AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED.equals(
316                action)) {
317                if (state == BluetoothProfile.STATE_CONNECTED) {
318                    mAvrcpCommandQueue.obtainMessage(
319                        MSG_DEVICE_BROWSE_CONNECT, btDev).sendToTarget();
320                } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
321                    mAvrcpCommandQueue.obtainMessage(
322                        MSG_DEVICE_BROWSE_DISCONNECT, btDev).sendToTarget();
323                }
324            } else if (AvrcpControllerService.ACTION_TRACK_EVENT.equals(action)) {
325                PlaybackState pbb =
326                    intent.getParcelableExtra(AvrcpControllerService.EXTRA_PLAYBACK);
327                MediaMetadata mmd =
328                    intent.getParcelableExtra(AvrcpControllerService.EXTRA_METADATA);
329                mAvrcpCommandQueue.obtainMessage(
330                    MSG_TRACK, new Pair<PlaybackState, MediaMetadata>(pbb, mmd)).sendToTarget();
331            } else if (AvrcpControllerService.ACTION_FOLDER_LIST.equals(action)) {
332                mAvrcpCommandQueue.obtainMessage(MSG_FOLDER_LIST, intent).sendToTarget();
333            }
334        }
335    };
336
337    private synchronized void msgDeviceConnect(BluetoothDevice device) {
338        Log.d(TAG, "msgDeviceConnect");
339        // We are connected to a new device via A2DP now.
340        mA2dpDevice = device;
341        mAvrcpCtrlSrvc = AvrcpControllerService.getAvrcpControllerService();
342        if (mAvrcpCtrlSrvc == null) {
343            Log.e(TAG, "!!!AVRCP Controller cannot be null");
344            return;
345        }
346        refreshInitialPlayingState();
347    }
348
349
350    // Refresh the UI if we have a connected device and AVRCP is initialized.
351    private synchronized void refreshInitialPlayingState() {
352        if (mA2dpDevice == null) {
353            Log.d(TAG, "device " + mA2dpDevice);
354            return;
355        }
356
357        List<BluetoothDevice> devices = mAvrcpCtrlSrvc.getConnectedDevices();
358        if (devices.size() == 0) {
359            Log.w(TAG, "No devices connected yet");
360            return;
361        }
362
363        if (mA2dpDevice != null && !mA2dpDevice.equals(devices.get(0))) {
364            Log.e(TAG, "A2dp device : " + mA2dpDevice + " avrcp device " + devices.get(0));
365            return;
366        }
367        mA2dpDevice = devices.get(0);
368
369        PlaybackState playbackState = mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice);
370        // Add actions required for playback and rebuild the object.
371        PlaybackState.Builder pbb = new PlaybackState.Builder(playbackState);
372        playbackState = pbb.setActions(mTransportControlFlags).build();
373
374        MediaMetadata mediaMetadata = mAvrcpCtrlSrvc.getMetaData(mA2dpDevice);
375        Log.d(TAG, "Media metadata " + mediaMetadata + " playback state " + playbackState);
376        mSession.setMetadata(mAvrcpCtrlSrvc.getMetaData(mA2dpDevice));
377        mSession.setPlaybackState(playbackState);
378    }
379
380    private void msgDeviceDisconnect(BluetoothDevice device) {
381        Log.d(TAG, "msgDeviceDisconnect");
382        if (mA2dpDevice == null) {
383            Log.w(TAG, "Already disconnected - nothing to do here.");
384            return;
385        } else if (!mA2dpDevice.equals(device)) {
386            Log.e(TAG, "Not the right device to disconnect current " +
387                mA2dpDevice + " dc " + device);
388            return;
389        }
390
391        // Unset the session.
392        PlaybackState.Builder pbb = new PlaybackState.Builder();
393        pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN,
394                    PLAYBACK_SPEED)
395                .setActions(mTransportControlFlags)
396                .setErrorMessage(getString(R.string.bluetooth_disconnected));
397        mSession.setPlaybackState(pbb.build());
398
399        // Set device to null.
400        mA2dpDevice = null;
401        mBrowseConnected = false;
402    }
403
404    private void msgTrack(PlaybackState pb, MediaMetadata mmd) {
405        Log.d(TAG, "msgTrack: playback: " + pb + " mmd: " + mmd);
406        // Log the current track position/content.
407        MediaController controller = mSession.getController();
408        PlaybackState prevPS = controller.getPlaybackState();
409        MediaMetadata prevMM = controller.getMetadata();
410
411        if (prevPS != null) {
412            Log.d(TAG, "prevPS " + prevPS);
413        }
414
415        if (prevMM != null) {
416            String title = prevMM.getString(MediaMetadata.METADATA_KEY_TITLE);
417            long trackLen = prevMM.getLong(MediaMetadata.METADATA_KEY_DURATION);
418            Log.d(TAG, "prev MM title " + title + " track len " + trackLen);
419        }
420
421        if (mmd != null) {
422            Log.d(TAG, "msgTrack() mmd " + mmd.getDescription());
423            mSession.setMetadata(mmd);
424        }
425
426        if (pb != null) {
427            Log.d(TAG, "msgTrack() playbackstate " + pb);
428            PlaybackState.Builder pbb = new PlaybackState.Builder(pb);
429            pb = pbb.setActions(mTransportControlFlags).build();
430            mSession.setPlaybackState(pb);
431
432            // If we are now playing then we should start pushing updates via MediaSession so that
433            // external UI (such as SystemUI) can show the currently playing music.
434            if (pb.getState() == PlaybackState.STATE_PLAYING && !mSession.isActive()) {
435                mSession.setActive(true);
436            }
437        }
438    }
439
440    private synchronized void msgPassThru(int cmd) {
441        Log.d(TAG, "msgPassThru " + cmd);
442        if (mA2dpDevice == null) {
443            // We should have already disconnected - ignore this message.
444            Log.e(TAG, "Already disconnected ignoring.");
445            return;
446        }
447
448        // Send the pass through.
449        mAvrcpCtrlSrvc.sendPassThroughCmd(
450            mA2dpDevice, cmd, AvrcpControllerService.KEY_STATE_PRESSED);
451        mAvrcpCtrlSrvc.sendPassThroughCmd(
452            mA2dpDevice, cmd, AvrcpControllerService.KEY_STATE_RELEASED);
453    }
454
455    private void msgDeviceBrowseConnect(BluetoothDevice device) {
456        Log.d(TAG, "msgDeviceBrowseConnect device " + device);
457        // We should already be connected to this device over A2DP.
458        if (!device.equals(mA2dpDevice)) {
459            Log.e(TAG, "Browse connected over different device a2dp " + mA2dpDevice +
460                " browse " + device);
461            return;
462        }
463        mBrowseConnected = true;
464    }
465
466    private void msgFolderList(Intent intent) {
467        // Parse the folder list for children list and id.
468        List<Parcelable> extraParcelableList =
469            (ArrayList<Parcelable>) intent.getParcelableArrayListExtra(
470                AvrcpControllerService.EXTRA_FOLDER_LIST);
471        List<MediaItem> folderList = new ArrayList<MediaItem>();
472        for (Parcelable p : extraParcelableList) {
473            folderList.add((MediaItem) p);
474        }
475
476        String id = intent.getStringExtra(AvrcpControllerService.EXTRA_FOLDER_ID);
477        Log.d(TAG, "Parent: " + id + " Folder list: " + folderList);
478        synchronized (this) {
479            Result<List<MediaItem>> results = mParentIdToRequestMap.remove(id);
480            if (results == null) {
481                Log.w(TAG, "Request no longer exists, hence ignoring reply!");
482                return;
483            }
484            results.sendResult(folderList);
485        }
486    }
487
488    private void msgDeviceBrowseDisconnect(BluetoothDevice device) {
489        Log.d(TAG, "msgDeviceBrowseDisconnect device " + device);
490        // Disconnect only if mA2dpDevice is non null
491        if (!device.equals(mA2dpDevice)) {
492            Log.w(TAG, "Browse disconnecting from different device a2dp " + mA2dpDevice +
493                " browse " + device);
494            return;
495        }
496        mBrowseConnected = false;
497    }
498}
499