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