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.avrcpcontroller;
18 
19 import android.app.PendingIntent;
20 import android.content.BroadcastReceiver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.os.Bundle;
25 import android.support.v4.media.MediaBrowserCompat.MediaItem;
26 import android.support.v4.media.MediaMetadataCompat;
27 import android.support.v4.media.session.MediaControllerCompat;
28 import android.support.v4.media.session.MediaSessionCompat;
29 import android.support.v4.media.session.PlaybackStateCompat;
30 import android.util.Log;
31 
32 import androidx.media.MediaBrowserServiceCompat;
33 
34 import com.android.bluetooth.BluetoothPrefs;
35 import com.android.bluetooth.R;
36 import com.android.internal.annotations.VisibleForTesting;
37 
38 import java.util.ArrayList;
39 import java.util.List;
40 
41 /**
42  * Implements the MediaBrowserService interface to AVRCP and A2DP
43  *
44  * <p>This service provides a means for external applications to access A2DP and AVRCP. The
45  * applications are expected to use MediaBrowser (see API) and all the music
46  * browsing/playback/metadata can be controlled via MediaBrowser and MediaController.
47  *
48  * <p>The current behavior of MediaSessionCompat exposed by this service is as follows: 1.
49  * MediaSessionCompat is active (i.e. SystemUI and other overview UIs can see updates) when device
50  * is connected and first starts playing. Before it starts playing we do not activate the session.
51  * 1.1 The session is active throughout the duration of connection. 2. The session is de-activated
52  * when the device disconnects. It will be connected again when (1) happens.
53  */
54 public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat {
55     private static final String TAG = BluetoothMediaBrowserService.class.getSimpleName();
56 
57     private static BluetoothMediaBrowserService sBluetoothMediaBrowserService;
58 
59     private MediaSessionCompat mSession;
60 
61     // Browsing related structures.
62     private List<MediaSessionCompat.QueueItem> mMediaQueue = new ArrayList<>();
63 
64     // Media Framework Content Style constants
65     private static final String CONTENT_STYLE_SUPPORTED =
66             "android.media.browse.CONTENT_STYLE_SUPPORTED";
67     public static final String CONTENT_STYLE_PLAYABLE_HINT =
68             "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT";
69     public static final String CONTENT_STYLE_BROWSABLE_HINT =
70             "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT";
71     public static final int CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1;
72     public static final int CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2;
73 
74     // Error messaging extras
75     public static final String ERROR_RESOLUTION_ACTION_INTENT =
76             "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT";
77     public static final String ERROR_RESOLUTION_ACTION_LABEL =
78             "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL";
79 
80     // Receiver for making sure our error message text matches the system locale
81     private class LocaleChangedReceiver extends BroadcastReceiver {
82         @Override
onReceive(Context context, Intent intent)83         public void onReceive(Context context, Intent intent) {
84             String action = intent.getAction();
85             if (action.equals(Intent.ACTION_LOCALE_CHANGED)) {
86                 Log.d(TAG, "Locale has updated");
87                 if (sBluetoothMediaBrowserService == null) return;
88                 MediaSessionCompat session = sBluetoothMediaBrowserService.getSession();
89 
90                 // Update playback state error message under new locale, if applicable
91                 MediaControllerCompat controller = session.getController();
92                 PlaybackStateCompat playbackState =
93                         controller == null ? null : controller.getPlaybackState();
94                 if (playbackState != null && playbackState.getErrorMessage() != null) {
95                     setErrorPlaybackState();
96                 }
97 
98                 // Update queue title under new locale
99                 session.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name));
100             }
101         }
102     }
103 
104     private LocaleChangedReceiver mReceiver;
105 
106     /**
107      * Initialize this BluetoothMediaBrowserService, creating our MediaSessionCompat, MediaPlayer
108      * and MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService.
109      */
110     @Override
onCreate()111     public void onCreate() {
112         Log.d(TAG, "Service Created");
113         super.onCreate();
114 
115         // Create and configure the MediaSessionCompat
116         mSession = new MediaSessionCompat(this, TAG);
117         setSessionToken(mSession.getSessionToken());
118         mSession.setFlags(
119                 MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
120                         | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
121         mSession.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name));
122         mSession.setQueue(mMediaQueue);
123         setErrorPlaybackState();
124         sBluetoothMediaBrowserService = this;
125 
126         mReceiver = new LocaleChangedReceiver();
127         IntentFilter filter = new IntentFilter();
128         filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
129         filter.addAction(Intent.ACTION_LOCALE_CHANGED);
130         registerReceiver(mReceiver, filter);
131     }
132 
133     @Override
onDestroy()134     public void onDestroy() {
135         Log.d(TAG, "Service Destroyed");
136         unregisterReceiver(mReceiver);
137         mReceiver = null;
138     }
139 
140     /**
141      * BrowseResult is used to return the contents of a node along with a status. The status is used
142      * to indicate success, a pending download, or error conditions. BrowseResult is used in
143      * onLoadChildren() and getContents() in BluetoothMediaBrowserService and in getContents() in
144      * AvrcpControllerService. The following statuses have been implemented: 1. SUCCESS - Contents
145      * have been retrieved successfully. 2. DOWNLOAD_PENDING - Download is in progress and may or
146      * may not have contents to return. 3. NO_DEVICE_CONNECTED - If no device is connected there are
147      * no contents to be retrieved. 4. ERROR_MEDIA_ID_INVALID - Contents could not be retrieved as
148      * the media ID is invalid. 5. ERROR_NO_AVRCP_SERVICE - Contents could not be retrieved as
149      * AvrcpControllerService is not connected.
150      */
151     public static class BrowseResult {
152         // Possible statuses for onLoadChildren
153         public static final byte SUCCESS = 0x00;
154         public static final byte DOWNLOAD_PENDING = 0x01;
155         public static final byte NO_DEVICE_CONNECTED = 0x02;
156         public static final byte ERROR_MEDIA_ID_INVALID = 0x03;
157         public static final byte ERROR_NO_AVRCP_SERVICE = 0x04;
158 
159         private List<MediaItem> mResults;
160         private final byte mStatus;
161 
getResults()162         List<MediaItem> getResults() {
163             return mResults;
164         }
165 
getStatus()166         byte getStatus() {
167             return mStatus;
168         }
169 
getStatusString()170         String getStatusString() {
171             switch (mStatus) {
172                 case DOWNLOAD_PENDING:
173                     return "DOWNLOAD_PENDING";
174                 case SUCCESS:
175                     return "SUCCESS";
176                 case NO_DEVICE_CONNECTED:
177                     return "NO_DEVICE_CONNECTED";
178                 case ERROR_MEDIA_ID_INVALID:
179                     return "ERROR_MEDIA_ID_INVALID";
180                 case ERROR_NO_AVRCP_SERVICE:
181                     return "ERROR_NO_AVRCP_SERVICE";
182                 default:
183                     return "UNDEFINED_ERROR_CASE";
184             }
185         }
186 
BrowseResult(List<MediaItem> results, byte status)187         BrowseResult(List<MediaItem> results, byte status) {
188             mResults = results;
189             mStatus = status;
190         }
191     }
192 
getContents(final String parentMediaId)193     BrowseResult getContents(final String parentMediaId) {
194         AvrcpControllerService avrcpControllerService =
195                 AvrcpControllerService.getAvrcpControllerService();
196         if (avrcpControllerService == null) {
197             Log.w(TAG, "getContents(id=" + parentMediaId + "): AVRCP Controller Service not ready");
198             return new BrowseResult(new ArrayList(0), BrowseResult.ERROR_NO_AVRCP_SERVICE);
199         } else {
200             return avrcpControllerService.getContents(parentMediaId);
201         }
202     }
203 
setErrorPlaybackState()204     private void setErrorPlaybackState() {
205         Bundle extras = new Bundle();
206         extras.putString(
207                 ERROR_RESOLUTION_ACTION_LABEL, getString(R.string.bluetooth_connect_action));
208         Intent launchIntent = new Intent();
209         launchIntent.setAction(BluetoothPrefs.BLUETOOTH_SETTING_ACTION);
210         launchIntent.addCategory(BluetoothPrefs.BLUETOOTH_SETTING_CATEGORY);
211         int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
212         PendingIntent pendingIntent =
213                 PendingIntent.getActivity(getApplicationContext(), 0, launchIntent, flags);
214         extras.putParcelable(ERROR_RESOLUTION_ACTION_INTENT, pendingIntent);
215         PlaybackStateCompat errorState =
216                 new PlaybackStateCompat.Builder()
217                         .setErrorMessage(getString(R.string.bluetooth_disconnected))
218                         .setExtras(extras)
219                         .setState(PlaybackStateCompat.STATE_ERROR, 0, 0)
220                         .build();
221         mSession.setPlaybackState(errorState);
222     }
223 
getDefaultStyle()224     private Bundle getDefaultStyle() {
225         Bundle style = new Bundle();
226         style.putBoolean(CONTENT_STYLE_SUPPORTED, true);
227         style.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE);
228         style.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE);
229         return style;
230     }
231 
232     @Override
onLoadChildren( final String parentMediaId, final Result<List<MediaItem>> result)233     public synchronized void onLoadChildren(
234             final String parentMediaId, final Result<List<MediaItem>> result) {
235         Log.d(TAG, "Request for contents, id= " + parentMediaId);
236         BrowseResult contents = getContents(parentMediaId);
237         byte status = contents.getStatus();
238         List<MediaItem> results = contents.getResults();
239         if (status == BrowseResult.DOWNLOAD_PENDING && results == null) {
240             Log.i(TAG, "Download pending - no results, id= " + parentMediaId);
241             result.detach();
242         } else {
243             Log.d(
244                     TAG,
245                     "Received Contents, id= "
246                             + parentMediaId
247                             + ", status= "
248                             + contents.getStatusString()
249                             + ", results="
250                             + results);
251             result.sendResult(results);
252         }
253     }
254 
255     @Override
onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)256     public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
257         Log.i(TAG, "Browser Client Connection Request, client='" + clientPackageName + "')");
258         Bundle style = getDefaultStyle();
259         return new BrowserRoot(BrowseTree.ROOT, style);
260     }
261 
updateNowPlayingQueue(BrowseTree.BrowseNode node)262     private void updateNowPlayingQueue(BrowseTree.BrowseNode node) {
263         List<MediaItem> songList = node.getContents();
264         mMediaQueue.clear();
265         if (songList != null && songList.size() > 0) {
266             for (MediaItem song : songList) {
267                 mMediaQueue.add(
268                         new MediaSessionCompat.QueueItem(
269                                 song.getDescription(), mMediaQueue.size()));
270             }
271             mSession.setQueue(mMediaQueue);
272         } else {
273             mSession.setQueue(null);
274         }
275         Log.d(TAG, "Now Playing List Changed, queue=" + mMediaQueue);
276     }
277 
clearNowPlayingQueue()278     private void clearNowPlayingQueue() {
279         mMediaQueue.clear();
280         mSession.setQueue(null);
281     }
282 
notifyChanged(BrowseTree.BrowseNode node)283     static synchronized void notifyChanged(BrowseTree.BrowseNode node) {
284         if (sBluetoothMediaBrowserService != null) {
285             if (node.getScope() == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
286                 sBluetoothMediaBrowserService.updateNowPlayingQueue(node);
287             } else {
288                 Log.d(TAG, "Browse Node contents changed, node=" + node);
289                 sBluetoothMediaBrowserService.notifyChildrenChanged(node.getID());
290             }
291         }
292     }
293 
addressedPlayerChanged(MediaSessionCompat.Callback callback)294     static synchronized void addressedPlayerChanged(MediaSessionCompat.Callback callback) {
295         if (sBluetoothMediaBrowserService != null) {
296             if (callback == null) {
297                 sBluetoothMediaBrowserService.setErrorPlaybackState();
298                 sBluetoothMediaBrowserService.clearNowPlayingQueue();
299             }
300             sBluetoothMediaBrowserService.mSession.setCallback(callback);
301         } else {
302             Log.w(TAG, "addressedPlayerChanged Unavailable");
303         }
304     }
305 
trackChanged(AvrcpItem track)306     static synchronized void trackChanged(AvrcpItem track) {
307         Log.d(TAG, "Track Changed, track=" + track);
308         if (sBluetoothMediaBrowserService != null) {
309             if (track != null) {
310                 sBluetoothMediaBrowserService.mSession.setMetadata(track.toMediaMetadata());
311             } else {
312                 sBluetoothMediaBrowserService.mSession.setMetadata(null);
313             }
314 
315         } else {
316             Log.w(TAG, "trackChanged Unavailable");
317         }
318     }
319 
notifyChanged(PlaybackStateCompat playbackState)320     static synchronized void notifyChanged(PlaybackStateCompat playbackState) {
321         Log.d(
322                 TAG,
323                 "Playback State Changed, state="
324                         + AvrcpControllerUtils.playbackStateCompatToString(playbackState));
325         if (sBluetoothMediaBrowserService != null) {
326             sBluetoothMediaBrowserService.mSession.setPlaybackState(playbackState);
327         } else {
328             Log.w(TAG, "notifyChanged Unavailable");
329         }
330     }
331 
332     /** Send AVRCP Play command */
play()333     public static synchronized void play() {
334         if (sBluetoothMediaBrowserService != null) {
335             sBluetoothMediaBrowserService.mSession.getController().getTransportControls().play();
336         } else {
337             Log.w(TAG, "play Unavailable");
338         }
339     }
340 
341     /** Send AVRCP Pause command */
pause()342     public static synchronized void pause() {
343         if (sBluetoothMediaBrowserService != null) {
344             sBluetoothMediaBrowserService.mSession.getController().getTransportControls().pause();
345         } else {
346             Log.w(TAG, "pause Unavailable");
347         }
348     }
349 
350     /** Get playback state */
getPlaybackState()351     public static synchronized PlaybackStateCompat getPlaybackState() {
352         if (sBluetoothMediaBrowserService != null) {
353             MediaSessionCompat session = sBluetoothMediaBrowserService.getSession();
354             if (session == null) return null;
355             MediaControllerCompat controller = session.getController();
356             PlaybackStateCompat playbackState =
357                     controller == null ? null : controller.getPlaybackState();
358             return playbackState;
359         }
360         return null;
361     }
362 
363     /** Get object for controlling playback */
getTransportControls()364     public static synchronized MediaControllerCompat.TransportControls getTransportControls() {
365         if (sBluetoothMediaBrowserService != null) {
366             return sBluetoothMediaBrowserService.mSession.getController().getTransportControls();
367         } else {
368             Log.w(TAG, "transportControls Unavailable");
369             return null;
370         }
371     }
372 
373     /** Set Media session active whenever we have Focus of any kind */
setActive(boolean active)374     public static synchronized void setActive(boolean active) {
375         if (sBluetoothMediaBrowserService != null) {
376             Log.d(TAG, "Setting the session active state to:" + active);
377             sBluetoothMediaBrowserService.mSession.setActive(active);
378         } else {
379             Log.w(TAG, "setActive Unavailable");
380         }
381     }
382 
383     /**
384      * Checks if the media session is active or not.
385      *
386      * @return true if media session is active, false otherwise.
387      */
388     @VisibleForTesting
isActive()389     public static synchronized boolean isActive() {
390         if (sBluetoothMediaBrowserService != null) {
391             return sBluetoothMediaBrowserService.mSession.isActive();
392         }
393         return false;
394     }
395 
396     /** Get Media session for updating state */
getSession()397     public static synchronized MediaSessionCompat getSession() {
398         if (sBluetoothMediaBrowserService != null) {
399             return sBluetoothMediaBrowserService.mSession;
400         } else {
401             Log.w(TAG, "getSession Unavailable");
402             return null;
403         }
404     }
405 
406     /** Reset the state of BluetoothMediaBrowserService to that before a device connected */
reset()407     public static synchronized void reset() {
408         if (sBluetoothMediaBrowserService != null) {
409             sBluetoothMediaBrowserService.clearNowPlayingQueue();
410             sBluetoothMediaBrowserService.mSession.setMetadata(null);
411             sBluetoothMediaBrowserService.setErrorPlaybackState();
412             sBluetoothMediaBrowserService.mSession.setCallback(null);
413             Log.d(TAG, "Service state has been reset");
414         } else {
415             Log.w(TAG, "reset unavailable");
416         }
417     }
418 
419     /** Get the state of the BluetoothMediaBrowserService as a debug string */
dump()420     public static synchronized String dump() {
421         StringBuilder sb = new StringBuilder();
422         sb.append(TAG + ":");
423         if (sBluetoothMediaBrowserService != null) {
424             MediaSessionCompat session = sBluetoothMediaBrowserService.getSession();
425             MediaControllerCompat controller = session.getController();
426             MediaMetadataCompat metadata = controller == null ? null : controller.getMetadata();
427             PlaybackStateCompat playbackState =
428                     controller == null ? null : controller.getPlaybackState();
429             List<MediaSessionCompat.QueueItem> queue =
430                     controller == null ? null : controller.getQueue();
431             if (metadata != null) {
432                 sb.append("\n    track={");
433                 sb.append("title=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE));
434                 sb.append(
435                         ", artist=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST));
436                 sb.append(", album=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM));
437                 sb.append(
438                         ", duration="
439                                 + metadata.getString(MediaMetadataCompat.METADATA_KEY_DURATION));
440                 sb.append(
441                         ", track_number="
442                                 + metadata.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER));
443                 sb.append(
444                         ", total_tracks="
445                                 + metadata.getLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS));
446                 sb.append(", genre=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE));
447                 sb.append(
448                         ", album_art="
449                                 + metadata.getString(
450                                         MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI));
451                 sb.append("}");
452             } else {
453                 sb.append("\n    track=" + metadata);
454             }
455             sb.append(
456                     "\n    playbackState="
457                             + AvrcpControllerUtils.playbackStateCompatToString(playbackState));
458             sb.append("\n    queue=" + queue);
459             sb.append("\n    internal_queue=" + sBluetoothMediaBrowserService.mMediaQueue);
460             sb.append("\n    session active state=").append(isActive());
461         } else {
462             Log.w(TAG, "dump Unavailable");
463             sb.append(" null");
464         }
465         return sb.toString();
466     }
467 }
468