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.MediaMetadata;
28import android.media.browse.MediaBrowser.MediaItem;
29import android.media.session.MediaController;
30import android.media.session.MediaSession;
31import android.media.session.PlaybackState;
32import android.os.Bundle;
33import android.os.Handler;
34import android.os.Looper;
35import android.os.Message;
36import android.os.ResultReceiver;
37import android.service.media.MediaBrowserService;
38import android.util.Pair;
39import android.util.Log;
40
41import com.android.bluetooth.R;
42
43import java.lang.ref.WeakReference;
44import java.util.ArrayList;
45import java.util.List;
46
47public class A2dpMediaBrowserService extends MediaBrowserService {
48    private static final String TAG = "A2dpMediaBrowserService";
49    private static final String MEDIA_ID_ROOT = "__ROOT__";
50    private static final String UNKNOWN_BT_AUDIO = "__UNKNOWN_BT_AUDIO__";
51    private static final float PLAYBACK_SPEED = 1.0f;
52
53    // Message sent when A2DP device is disconnected.
54    private static final int MSG_DEVICE_DISCONNECT = 0;
55    // Message snet when the AVRCP profile is disconnected = 1;
56    private static final int MSG_PROFILE_DISCONNECT = 1;
57    // Message sent when A2DP device is connected.
58    private static final int MSG_DEVICE_CONNECT = 2;
59    // Message sent when AVRCP profile is connected (note AVRCP profile may be connected before or
60    // after A2DP device is connected).
61    private static final int MSG_PROFILE_CONNECT = 3;
62    // Message sent when we recieve a TRACK update from AVRCP profile over a connected A2DP device.
63    private static final int MSG_TRACK = 4;
64    // Internal message sent to trigger a AVRCP action.
65    private static final int MSG_AVRCP_PASSTHRU = 5;
66
67    private MediaSession mSession;
68    private MediaMetadata mA2dpMetadata;
69
70    private BluetoothAdapter mAdapter;
71    private BluetoothAvrcpController mAvrcpProfile;
72    private BluetoothDevice mA2dpDevice = null;
73    private Handler mAvrcpCommandQueue;
74
75    private long mTransportControlFlags = PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY
76            | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
77
78    private static final class AvrcpCommandQueueHandler extends Handler {
79        WeakReference<A2dpMediaBrowserService> mInst;
80
81        AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink) {
82            super(looper);
83            mInst = new WeakReference<A2dpMediaBrowserService>(sink);
84        }
85
86        @Override
87        public void handleMessage(Message msg) {
88            A2dpMediaBrowserService inst = mInst.get();
89            if (inst == null) {
90                Log.e(TAG, "Parent class has died; aborting.");
91                return;
92            }
93
94            switch (msg.what) {
95                case MSG_DEVICE_CONNECT:
96                    inst.msgDeviceConnect((BluetoothDevice) msg.obj);
97                    break;
98                case MSG_PROFILE_CONNECT:
99                    inst.msgProfileConnect((BluetoothProfile) msg.obj);
100                    break;
101                case MSG_DEVICE_DISCONNECT:
102                    inst.msgDeviceDisconnect((BluetoothDevice) msg.obj);
103                    break;
104                case MSG_PROFILE_DISCONNECT:
105                    inst.msgProfileDisconnect();
106                    break;
107                case MSG_TRACK:
108                    Pair<PlaybackState, MediaMetadata> pair =
109                        (Pair<PlaybackState, MediaMetadata>) (msg.obj);
110                    inst.msgTrack(pair.first, pair.second);
111                    break;
112                case MSG_AVRCP_PASSTHRU:
113                    inst.msgPassThru((int) msg.obj);
114                    break;
115            }
116        }
117    }
118
119    @Override
120    public void onCreate() {
121        Log.d(TAG, "onCreate");
122        super.onCreate();
123        mSession = new MediaSession(this, TAG);
124        setSessionToken(mSession.getSessionToken());
125        mSession.setCallback(mSessionCallbacks);
126        mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
127                MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
128        mAvrcpCommandQueue = new AvrcpCommandQueueHandler(Looper.getMainLooper(), this);
129
130        mAdapter = BluetoothAdapter.getDefaultAdapter();
131        mAdapter.getProfileProxy(this, mServiceListener, BluetoothProfile.AVRCP_CONTROLLER);
132
133        IntentFilter filter = new IntentFilter();
134        filter.addAction(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
135        filter.addAction(BluetoothAvrcpController.ACTION_TRACK_EVENT);
136        registerReceiver(mBtReceiver, filter);
137    }
138
139    @Override
140    public void onDestroy() {
141        Log.d(TAG, "onDestroy");
142        mSession.release();
143        unregisterReceiver(mBtReceiver);
144        super.onDestroy();
145    }
146
147    @Override
148    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
149        return new BrowserRoot(MEDIA_ID_ROOT, null);
150    }
151
152    @Override
153    public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
154        Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId);
155        List<MediaItem> items = new ArrayList<MediaItem>();
156        result.sendResult(items);
157    }
158
159    BluetoothProfile.ServiceListener mServiceListener = new BluetoothProfile.ServiceListener() {
160        public void onServiceConnected(int profile, BluetoothProfile proxy) {
161            Log.d(TAG, "onServiceConnected");
162            if (profile == BluetoothProfile.AVRCP_CONTROLLER) {
163                mAvrcpCommandQueue.obtainMessage(MSG_PROFILE_CONNECT, proxy).sendToTarget();
164                List<BluetoothDevice> devices = proxy.getConnectedDevices();
165                if (devices != null && devices.size() > 0) {
166                    BluetoothDevice device = devices.get(0);
167                    Log.d(TAG, "got AVRCP device " + device);
168                }
169            }
170        }
171
172        public void onServiceDisconnected(int profile) {
173            Log.d(TAG, "onServiceDisconnected " + profile);
174            if (profile == BluetoothProfile.AVRCP_CONTROLLER) {
175                mAvrcpProfile = null;
176                mAvrcpCommandQueue.obtainMessage(MSG_PROFILE_DISCONNECT).sendToTarget();
177            }
178        }
179    };
180
181    // Media Session Stuff.
182    private MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() {
183        @Override
184        public void onPlay() {
185            Log.d(TAG, "onPlay");
186            mAvrcpCommandQueue.obtainMessage(
187                MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_PLAY).sendToTarget();
188            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
189        }
190
191        @Override
192        public void onPause() {
193            Log.d(TAG, "onPause");
194            mAvrcpCommandQueue.obtainMessage(
195                MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_PAUSE).sendToTarget();
196            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
197        }
198
199        @Override
200        public void onSkipToNext() {
201            Log.d(TAG, "onSkipToNext");
202            mAvrcpCommandQueue.obtainMessage(
203                MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_FORWARD)
204                .sendToTarget();
205            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
206        }
207
208        @Override
209        public void onSkipToPrevious() {
210            Log.d(TAG, "onSkipToPrevious");
211
212            mAvrcpCommandQueue.obtainMessage(
213                MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_BACKWARD)
214                .sendToTarget();
215            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
216        }
217
218        // These are not yet supported.
219        @Override
220        public void onStop() {
221            Log.d(TAG, "onStop");
222        }
223
224        @Override
225        public void onCustomAction(String action, Bundle extras) {
226            Log.d(TAG, "onCustomAction action=" + action + " extras=" + extras);
227        }
228
229        @Override
230        public void onPlayFromSearch(String query, Bundle extras) {
231            Log.d(TAG, "playFromSearch not supported in AVRCP");
232        }
233
234        @Override
235        public void onCommand(String command, Bundle args, ResultReceiver cb) {
236            Log.d(TAG, "onCommand command=" + command + " args=" + args);
237        }
238
239        @Override
240        public void onSkipToQueueItem(long queueId) {
241            Log.d(TAG, "onSkipToQueueItem");
242        }
243
244        @Override
245        public void onPlayFromMediaId(String mediaId, Bundle extras) {
246            Log.d(TAG, "onPlayFromMediaId mediaId=" + mediaId + " extras=" + extras);
247        }
248
249    };
250
251    private BroadcastReceiver mBtReceiver = new BroadcastReceiver() {
252        @Override
253        public void onReceive(Context context, Intent intent) {
254            Log.d(TAG, "onReceive intent=" + intent);
255            String action = intent.getAction();
256            BluetoothDevice btDev =
257                    (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
258            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
259
260            if (BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
261                Log.d(TAG, "handleConnectionStateChange: newState="
262                        + state + " btDev=" + btDev);
263
264                // Connected state will be handled when AVRCP BluetoothProfile gets connected.
265                if (state == BluetoothProfile.STATE_CONNECTED) {
266                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_CONNECT, btDev).sendToTarget();
267                } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
268                    // Set the playback state to unconnected.
269                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_DISCONNECT, btDev).sendToTarget();
270                }
271            } else if (BluetoothAvrcpController.ACTION_TRACK_EVENT.equals(action)) {
272                PlaybackState pbb =
273                    intent.getParcelableExtra(BluetoothAvrcpController.EXTRA_PLAYBACK);
274                MediaMetadata mmd =
275                    intent.getParcelableExtra(BluetoothAvrcpController.EXTRA_METADATA);
276                mAvrcpCommandQueue.obtainMessage(
277                    MSG_TRACK, new Pair<PlaybackState, MediaMetadata>(pbb, mmd)).sendToTarget();
278            }
279        }
280    };
281
282    private void msgDeviceConnect(BluetoothDevice device) {
283        Log.d(TAG, "msgDeviceConnect");
284        // We are connected to a new device via A2DP now.
285        mA2dpDevice = device;
286        refreshInitialPlayingState();
287    }
288
289    private void msgProfileConnect(BluetoothProfile profile) {
290        Log.d(TAG, "msgProfileConnect");
291        if (profile != null) {
292            mAvrcpProfile = (BluetoothAvrcpController) profile;
293        }
294        refreshInitialPlayingState();
295    }
296
297    // Refresh the UI if we have a connected device and AVRCP is initialized.
298    private void refreshInitialPlayingState() {
299        if (mAvrcpProfile == null || mA2dpDevice == null) {
300            Log.d(TAG, "AVRCP Profile " + mAvrcpProfile + " device " + mA2dpDevice);
301            return;
302        }
303
304        List<BluetoothDevice> devices = mAvrcpProfile.getConnectedDevices();
305        if (devices.size() == 0) {
306            Log.w(TAG, "No devices connected yet");
307            return;
308        }
309
310        if (mA2dpDevice != null && !mA2dpDevice.equals(devices.get(0))) {
311            Log.e(TAG, "A2dp device : " + mA2dpDevice + " avrcp device " + devices.get(0));
312        }
313        mA2dpDevice = devices.get(0);
314
315        PlaybackState playbackState = mAvrcpProfile.getPlaybackState(mA2dpDevice);
316        // Add actions required for playback and rebuild the object.
317        PlaybackState.Builder pbb = new PlaybackState.Builder(playbackState);
318        playbackState = pbb.setActions(mTransportControlFlags).build();
319
320        MediaMetadata mediaMetadata = mAvrcpProfile.getMetadata(mA2dpDevice);
321        Log.d(TAG, "Media metadata " + mediaMetadata + " playback state " + playbackState);
322        mSession.setMetadata(mAvrcpProfile.getMetadata(mA2dpDevice));
323        mSession.setPlaybackState(playbackState);
324    }
325
326    private void msgDeviceDisconnect(BluetoothDevice device) {
327        Log.d(TAG, "msgDeviceDisconnect");
328        if (mA2dpDevice == null) {
329            Log.w(TAG, "Already disconnected - nothing to do here.");
330            return;
331        } else if (!mA2dpDevice.equals(device)) {
332            Log.e(TAG, "Not the right device to disconnect current " +
333                mA2dpDevice + " dc " + device);
334            return;
335        }
336
337        // Unset the session.
338        PlaybackState.Builder pbb = new PlaybackState.Builder();
339        pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN,
340                    PLAYBACK_SPEED)
341                .setActions(mTransportControlFlags)
342                .setErrorMessage(getString(R.string.bluetooth_disconnected));
343        mSession.setPlaybackState(pbb.build());
344    }
345
346    private void msgProfileDisconnect() {
347        Log.d(TAG, "msgProfileDisconnect");
348        // The profile is disconnected - even if the device is still connected we cannot really have
349        // a functioning UI so reset the session.
350        mAvrcpProfile = null;
351
352        // Unset the session.
353        PlaybackState.Builder pbb = new PlaybackState.Builder();
354        pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN,
355                    PLAYBACK_SPEED)
356                .setActions(mTransportControlFlags)
357                .setErrorMessage(getString(R.string.bluetooth_disconnected));
358        mSession.setPlaybackState(pbb.build());
359    }
360
361    private void msgTrack(PlaybackState pb, MediaMetadata mmd) {
362        Log.d(TAG, "msgTrack: playback: " + pb + " mmd: " + mmd);
363        // Log the current track position/content.
364        MediaController controller = mSession.getController();
365        PlaybackState prevPS = controller.getPlaybackState();
366        MediaMetadata prevMM = controller.getMetadata();
367
368        if (prevPS != null) {
369            Log.d(TAG, "prevPS " + prevPS);
370        }
371
372        if (prevMM != null) {
373            String title = prevMM.getString(MediaMetadata.METADATA_KEY_TITLE);
374            long trackLen = prevMM.getLong(MediaMetadata.METADATA_KEY_DURATION);
375            Log.d(TAG, "prev MM title " + title + " track len " + trackLen);
376        }
377
378        if (mmd != null) {
379            Log.d(TAG, "msgTrack() mmd " + mmd.getDescription());
380            mSession.setMetadata(mmd);
381        }
382
383        if (pb != null) {
384            Log.d(TAG, "msgTrack() playbackstate " + pb);
385            PlaybackState.Builder pbb = new PlaybackState.Builder(pb);
386            pb = pbb.setActions(mTransportControlFlags).build();
387            mSession.setPlaybackState(pb);
388        }
389    }
390
391    private void msgPassThru(int cmd) {
392        Log.d(TAG, "msgPassThru " + cmd);
393        if (mA2dpDevice == null) {
394            // We should have already disconnected - ignore this message.
395            Log.e(TAG, "Already disconnected ignoring.");
396            return;
397        }
398
399        if (mAvrcpProfile == null) {
400            // We may be disconnected with the profile but there is not much we can do for now but
401            // to wait for the profile to come back up.
402            Log.e(TAG, "Profile disconnected; ignoring.");
403            return;
404        }
405
406        // Send the pass through.
407        mAvrcpProfile.sendPassThroughCmd(
408            mA2dpDevice, cmd, BluetoothAvrcpController.KEY_STATE_PRESSED);
409        mAvrcpProfile.sendPassThroughCmd(
410            mA2dpDevice, cmd, BluetoothAvrcpController.KEY_STATE_RELEASED);
411    }
412}
413