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 
17 package com.android.bluetooth.a2dpsink.mbs;
18 
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothAvrcpController;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothProfile;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.media.MediaMetadata;
28 import android.media.browse.MediaBrowser.MediaItem;
29 import android.media.session.MediaController;
30 import android.media.session.MediaSession;
31 import android.media.session.PlaybackState;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.os.Message;
36 import android.os.ResultReceiver;
37 import android.service.media.MediaBrowserService;
38 import android.util.Pair;
39 import android.util.Log;
40 
41 import com.android.bluetooth.R;
42 
43 import java.lang.ref.WeakReference;
44 import java.util.ArrayList;
45 import java.util.List;
46 
47 public 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 
AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink)81         AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink) {
82             super(looper);
83             mInst = new WeakReference<A2dpMediaBrowserService>(sink);
84         }
85 
86         @Override
handleMessage(Message msg)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
onCreate()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
onDestroy()140     public void onDestroy() {
141         Log.d(TAG, "onDestroy");
142         mSession.release();
143         unregisterReceiver(mBtReceiver);
144         super.onDestroy();
145     }
146 
147     @Override
onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)148     public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
149         return new BrowserRoot(MEDIA_ID_ROOT, null);
150     }
151 
152     @Override
onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result)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 
msgDeviceConnect(BluetoothDevice device)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 
msgProfileConnect(BluetoothProfile profile)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.
refreshInitialPlayingState()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 
msgDeviceDisconnect(BluetoothDevice device)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 
msgProfileDisconnect()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 
msgTrack(PlaybackState pb, MediaMetadata mmd)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 
msgPassThru(int cmd)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