• 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.media.AudioManager;
25 import android.os.Bundle;
26 import android.support.v4.media.MediaBrowserCompat.MediaItem;
27 import android.support.v4.media.MediaMetadataCompat;
28 import android.support.v4.media.session.MediaControllerCompat;
29 import android.support.v4.media.session.MediaSessionCompat;
30 import android.support.v4.media.session.PlaybackStateCompat;
31 import android.util.Log;
32 
33 import androidx.media.MediaBrowserServiceCompat;
34 
35 import com.android.bluetooth.BluetoothPrefs;
36 import com.android.bluetooth.R;
37 import com.android.bluetooth.flags.Flags;
38 import com.android.internal.annotations.GuardedBy;
39 import com.android.internal.annotations.VisibleForTesting;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 
44 /**
45  * Implements the MediaBrowserService interface to AVRCP and A2DP
46  *
47  * <p>This service provides a means for external applications to access A2DP and AVRCP. The
48  * applications are expected to use MediaBrowser (see API) and all the music
49  * browsing/playback/metadata can be controlled via MediaBrowser and MediaController.
50  *
51  * <p>The current behavior of MediaSessionCompat exposed by this service is as follows: 1.
52  * MediaSessionCompat is active (i.e. SystemUI and other overview UIs can see updates) when device
53  * is connected and first starts playing. Before it starts playing we do not activate the session.
54  * 1.1 The session is active throughout the duration of connection. 2. The session is de-activated
55  * when the device disconnects. It will be connected again when (1) happens.
56  */
57 public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat {
58     private static final String TAG = BluetoothMediaBrowserService.class.getSimpleName();
59 
60     private static final Object INSTANCE_LOCK = new Object();
61 
62     @GuardedBy("INSTANCE_LOCK")
63     private static BluetoothMediaBrowserService sBluetoothMediaBrowserService;
64 
65     private MediaSessionCompat mSession;
66 
67     // Browsing related structures.
68     private final List<MediaSessionCompat.QueueItem> mMediaQueue = new ArrayList<>();
69 
70     // Media Framework Content Style constants
71     private static final String CONTENT_STYLE_SUPPORTED =
72             "android.media.browse.CONTENT_STYLE_SUPPORTED";
73     public static final String CONTENT_STYLE_PLAYABLE_HINT =
74             "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT";
75     public static final String CONTENT_STYLE_BROWSABLE_HINT =
76             "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT";
77     public static final int CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1;
78     public static final int CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2;
79 
80     // Error messaging extras
81     public static final String ERROR_RESOLUTION_ACTION_INTENT =
82             "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT";
83     public static final String ERROR_RESOLUTION_ACTION_LABEL =
84             "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL";
85 
86     // Receiver for making sure our error message text matches the system locale
87     private class LocaleChangedReceiver extends BroadcastReceiver {
88         @Override
onReceive(Context context, Intent intent)89         public void onReceive(Context context, Intent intent) {
90             String action = intent.getAction();
91             if (action.equals(Intent.ACTION_LOCALE_CHANGED)) {
92                 Log.d(TAG, "Locale has updated");
93 
94                 BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
95                 if (service == null) {
96                     Log.w(TAG, "onReceive(): Got locale update, but service isn't active");
97                     return;
98                 }
99 
100                 MediaSessionCompat session = service.getSession();
101 
102                 // Update playback state error message under new locale, if applicable
103                 MediaControllerCompat controller = session.getController();
104                 PlaybackStateCompat playbackState =
105                         controller == null ? null : controller.getPlaybackState();
106                 if (playbackState != null && playbackState.getErrorMessage() != null) {
107                     setErrorPlaybackState();
108                 }
109 
110                 // Update queue title under new locale
111                 session.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name));
112             }
113         }
114     }
115 
116     private LocaleChangedReceiver mReceiver;
117 
118     /**
119      * Set the BluetoothMediaBrowserService instance
120      *
121      * <p>This object is a singleton, as their can only be one service instance active for a process
122      * at a time.
123      */
setInstance(BluetoothMediaBrowserService service)124     private static void setInstance(BluetoothMediaBrowserService service) {
125         synchronized (INSTANCE_LOCK) {
126             sBluetoothMediaBrowserService = service;
127             Log.i(TAG, "Service set to " + service);
128         }
129     }
130 
131     /** Get the BluetoothMediaBrowserService instance */
132     @VisibleForTesting
getInstance()133     public static BluetoothMediaBrowserService getInstance() {
134         synchronized (INSTANCE_LOCK) {
135             return sBluetoothMediaBrowserService;
136         }
137     }
138 
139     /**
140      * Initialize this BluetoothMediaBrowserService, creating our MediaSessionCompat, MediaPlayer
141      * and MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService.
142      */
143     @Override
onCreate()144     public void onCreate() {
145         Log.d(TAG, "Service Created");
146         super.onCreate();
147 
148         // Create and configure the MediaSessionCompat
149         mSession = new MediaSessionCompat(this, TAG);
150         setSessionToken(mSession.getSessionToken());
151         mSession.setFlags(
152                 MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
153                         | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
154         mSession.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name));
155         mSession.setQueue(mMediaQueue);
156         setErrorPlaybackState();
157 
158         mReceiver = new LocaleChangedReceiver();
159         IntentFilter filter = new IntentFilter();
160         filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
161         filter.addAction(Intent.ACTION_LOCALE_CHANGED);
162         registerReceiver(mReceiver, filter);
163 
164         setInstance(this);
165     }
166 
167     @Override
onDestroy()168     public void onDestroy() {
169         Log.d(TAG, "Service Destroyed");
170         super.onDestroy();
171         unregisterReceiver(mReceiver);
172         mReceiver = null;
173         mSession.release();
174         mSession = null;
175         setInstance(null);
176     }
177 
178     /**
179      * BrowseResult is used to return the contents of a node along with a status. The status is used
180      * to indicate success, a pending download, or error conditions. BrowseResult is used in
181      * onLoadChildren() and getContents() in BluetoothMediaBrowserService and in getContents() in
182      * AvrcpControllerService. The following statuses have been implemented: 1. SUCCESS - Contents
183      * have been retrieved successfully. 2. DOWNLOAD_PENDING - Download is in progress and may or
184      * may not have contents to return. 3. NO_DEVICE_CONNECTED - If no device is connected there are
185      * no contents to be retrieved. 4. ERROR_MEDIA_ID_INVALID - Contents could not be retrieved as
186      * the media ID is invalid. 5. ERROR_NO_AVRCP_SERVICE - Contents could not be retrieved as
187      * AvrcpControllerService is not connected.
188      */
BrowseResult(List<MediaItem> results, byte status)189     record BrowseResult(List<MediaItem> results, byte status) {
190         // Possible statuses for onLoadChildren
191         public static final byte SUCCESS = 0x00;
192         public static final byte DOWNLOAD_PENDING = 0x01;
193         public static final byte NO_DEVICE_CONNECTED = 0x02;
194         public static final byte ERROR_MEDIA_ID_INVALID = 0x03;
195         public static final byte ERROR_NO_AVRCP_SERVICE = 0x04;
196 
197         String getStatusString() {
198             switch (status) {
199                 case DOWNLOAD_PENDING:
200                     return "DOWNLOAD_PENDING";
201                 case SUCCESS:
202                     return "SUCCESS";
203                 case NO_DEVICE_CONNECTED:
204                     return "NO_DEVICE_CONNECTED";
205                 case ERROR_MEDIA_ID_INVALID:
206                     return "ERROR_MEDIA_ID_INVALID";
207                 case ERROR_NO_AVRCP_SERVICE:
208                     return "ERROR_NO_AVRCP_SERVICE";
209                 default:
210                     return "UNDEFINED_ERROR_CASE";
211             }
212         }
213     }
214 
getContents(final String parentMediaId)215     BrowseResult getContents(final String parentMediaId) {
216         AvrcpControllerService avrcpControllerService =
217                 AvrcpControllerService.getAvrcpControllerService();
218         if (avrcpControllerService == null) {
219             Log.w(TAG, "getContents(id=" + parentMediaId + "): AVRCP Controller Service not ready");
220             return new BrowseResult(null, BrowseResult.ERROR_NO_AVRCP_SERVICE);
221         } else {
222             return avrcpControllerService.getContents(parentMediaId);
223         }
224     }
225 
setErrorPlaybackState()226     private void setErrorPlaybackState() {
227         Bundle extras = new Bundle();
228         extras.putString(
229                 ERROR_RESOLUTION_ACTION_LABEL, getString(R.string.bluetooth_connect_action));
230         Intent launchIntent = new Intent();
231         launchIntent.setAction(BluetoothPrefs.BLUETOOTH_SETTING_ACTION);
232         launchIntent.addCategory(BluetoothPrefs.BLUETOOTH_SETTING_CATEGORY);
233         PendingIntent pendingIntent =
234                 PendingIntent.getActivity(
235                         getApplicationContext(),
236                         0,
237                         launchIntent,
238                         PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
239         extras.putParcelable(ERROR_RESOLUTION_ACTION_INTENT, pendingIntent);
240         PlaybackStateCompat errorState =
241                 new PlaybackStateCompat.Builder()
242                         .setErrorMessage(getString(R.string.bluetooth_disconnected))
243                         .setExtras(extras)
244                         .setState(PlaybackStateCompat.STATE_ERROR, 0, 0)
245                         .build();
246         mSession.setPlaybackState(errorState);
247     }
248 
getDefaultStyle()249     private static Bundle getDefaultStyle() {
250         Bundle style = new Bundle();
251         style.putBoolean(CONTENT_STYLE_SUPPORTED, true);
252         style.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE);
253         style.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE);
254         return style;
255     }
256 
257     @Override
onLoadChildren( final String parentMediaId, final Result<List<MediaItem>> result)258     public synchronized void onLoadChildren(
259             final String parentMediaId, final Result<List<MediaItem>> result) {
260         Log.d(TAG, "Request for contents, id= " + parentMediaId);
261         BrowseResult contents = getContents(parentMediaId);
262         byte status = contents.status();
263         List<MediaItem> results = contents.results();
264         if (status == BrowseResult.DOWNLOAD_PENDING && results == null) {
265             Log.i(TAG, "Download pending - no results, id= " + parentMediaId);
266             result.detach();
267         } else {
268             Log.d(
269                     TAG,
270                     "Received Contents, id= "
271                             + parentMediaId
272                             + ", status= "
273                             + contents.getStatusString()
274                             + ", results="
275                             + results);
276             result.sendResult(results);
277         }
278     }
279 
280     @Override
onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)281     public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
282         Log.i(TAG, "Browser Client Connection Request, client='" + clientPackageName + "')");
283         Bundle style = getDefaultStyle();
284         return new BrowserRoot(BrowseTree.ROOT, style);
285     }
286 
onNowPlayingQueueChanged(BrowseTree.BrowseNode node)287     static synchronized void onNowPlayingQueueChanged(BrowseTree.BrowseNode node) {
288         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
289         if (service == null) {
290             Log.w(TAG, "onNowPlayingQueueChanged(node=" + node + "): Service not available");
291             return;
292         }
293 
294         if (node == null) {
295             Log.w(TAG, "Received now playing update for null node");
296             return;
297         }
298 
299         if (node.getScope() != AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
300             Log.w(TAG, "Received now playing update for node not in now playing scope.");
301             return;
302         }
303 
304         service.setNowPlayingQueue(node.getContents());
305     }
306 
setNowPlayingQueue(List<MediaItem> songList)307     private void setNowPlayingQueue(List<MediaItem> songList) {
308         mMediaQueue.clear();
309         if (songList != null && songList.size() > 0) {
310             for (MediaItem song : songList) {
311                 mMediaQueue.add(
312                         new MediaSessionCompat.QueueItem(
313                                 song.getDescription(), mMediaQueue.size()));
314             }
315             mSession.setQueue(mMediaQueue);
316         } else {
317             mSession.setQueue(null);
318         }
319         Log.d(TAG, "Now Playing List Changed, queue=" + mMediaQueue);
320     }
321 
clearNowPlayingQueue()322     private void clearNowPlayingQueue() {
323         mMediaQueue.clear();
324         mSession.setQueue(null);
325     }
326 
onBrowseNodeChanged(BrowseTree.BrowseNode node)327     static synchronized void onBrowseNodeChanged(BrowseTree.BrowseNode node) {
328         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
329         if (service == null) {
330             Log.w(TAG, "onBrowseNodeChanged(node=" + node + "): Service not available");
331             return;
332         }
333 
334         if (node == null) {
335             Log.w(TAG, "Received browse node update for null node");
336             return;
337         }
338 
339         Log.d(TAG, "Browse Node contents changed, node=" + node);
340 
341         int scope = node.getScope();
342         if (scope != AvrcpControllerService.BROWSE_SCOPE_VFS
343                 && scope != AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST) {
344             Log.w(TAG, "Received browse tree update for node outside of player or VFS scope");
345             return;
346         }
347         service.notifyChildrenChanged(node.getID());
348     }
349 
onAddressedPlayerChanged(MediaSessionCompat.Callback callback)350     static synchronized void onAddressedPlayerChanged(MediaSessionCompat.Callback callback) {
351         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
352         if (service == null) {
353             Log.w(TAG, "addressedPlayerChanged(callback=" + callback + "): Service not available");
354             return;
355         }
356 
357         if (callback == null) {
358             service.setErrorPlaybackState();
359             service.clearNowPlayingQueue();
360         }
361         service.mSession.setCallback(callback);
362     }
363 
onTrackChanged(AvrcpItem track)364     static synchronized void onTrackChanged(AvrcpItem track) {
365         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
366         if (service == null) {
367             Log.w(TAG, "trackChanged(track=" + track + "): Service not available");
368             return;
369         }
370 
371         Log.d(TAG, "Track Changed, track=" + track);
372         if (track != null) {
373             service.mSession.setMetadata(track.toMediaMetadata());
374         } else {
375             service.mSession.setMetadata(null);
376         }
377     }
378 
onPlaybackStateChanged(PlaybackStateCompat state)379     static synchronized void onPlaybackStateChanged(PlaybackStateCompat state) {
380         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
381         if (service == null) {
382             Log.w(TAG, "onPlaybackStateChanged(state=" + state + "): Service not available");
383             return;
384         }
385 
386         Log.d(
387                 TAG,
388                 "Playback State Changed, state="
389                         + AvrcpControllerUtils.playbackStateCompatToString(state));
390         service.mSession.setPlaybackState(state);
391     }
392 
393     /**
394      * Notify this MediaBrowserService of changes to audio focus state
395      *
396      * <p>Temporarily set state to "Connecting" to better interoperate with media center
397      * applications.
398      *
399      * <p>The "Connecting" state is considered an "active" playback state, which will cause clients
400      * that don't listen to the media framework's callback for media key events (whoever most
401      * recently requested focus + had playback) to think we're the application who most recently
402      * updated to an "active" playback state, which in turn will have them show us as the active app
403      * in the UI while we wait on the remote device to accept our playback command.
404      */
onAudioFocusStateChanged(int state)405     static synchronized void onAudioFocusStateChanged(int state) {
406         if (!Flags.signalConnectingOnFocusGain()) {
407             Log.w(TAG, "Feature 'signal_connecting_on_focus_gain' not enabled. Skip");
408             return;
409         }
410 
411         if (state != AudioManager.AUDIOFOCUS_GAIN) {
412             return;
413         }
414 
415         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
416         if (service == null) {
417             Log.w(TAG, "onAudioFocusStateChanged(state=" + state + "): Service not available");
418             return;
419         }
420 
421         Log.i(
422                 TAG,
423                 "onAudioFocusStateChanged(state="
424                         + state
425                         + "): Focus gained, briefly signal connecting");
426 
427         MediaSessionCompat session = service.getSession();
428         MediaControllerCompat controller = session.getController();
429         PlaybackStateCompat currentState =
430                 controller == null ? null : controller.getPlaybackState();
431 
432         PlaybackStateCompat connectingState = null;
433         if (currentState != null) {
434             connectingState =
435                     new PlaybackStateCompat.Builder(currentState)
436                             .setState(
437                                     PlaybackStateCompat.STATE_CONNECTING,
438                                     currentState.getPosition(),
439                                     currentState.getPlaybackSpeed())
440                             .build();
441             service.mSession.setPlaybackState(connectingState);
442             service.mSession.setPlaybackState(currentState);
443         } else {
444             Log.w(
445                     TAG,
446                     "onAudioFocusStateChanged(state="
447                             + state
448                             + "): current playback state is null");
449         }
450     }
451 
452     /** Get playback state */
getPlaybackState()453     public static synchronized PlaybackStateCompat getPlaybackState() {
454         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
455         if (service == null) {
456             Log.w(TAG, "getPlaybackState(): Service not available");
457             return null;
458         }
459 
460         MediaSessionCompat session = service.getSession();
461         if (session == null) return null;
462         MediaControllerCompat controller = session.getController();
463         PlaybackStateCompat playbackState =
464                 controller == null ? null : controller.getPlaybackState();
465         return playbackState;
466     }
467 
468     /** Get object for controlling playback */
getTransportControls()469     public static synchronized MediaControllerCompat.TransportControls getTransportControls() {
470         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
471         if (service == null) {
472             Log.w(TAG, "getTransportControls(): Service not available");
473             return null;
474         }
475         return service.mSession.getController().getTransportControls();
476     }
477 
478     /** Set Media session active whenever we have Focus of any kind */
setActive(boolean active)479     public static synchronized void setActive(boolean active) {
480         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
481         if (service == null) {
482             Log.w(TAG, "setActive(active=" + active + "): Service not available");
483             return;
484         }
485         Log.d(TAG, "Setting the session active state to:" + active);
486         service.mSession.setActive(active);
487     }
488 
489     /**
490      * Checks if the media session is active or not.
491      *
492      * @return true if media session is active, false otherwise.
493      */
494     @VisibleForTesting
isActive()495     public static synchronized boolean isActive() {
496         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
497         if (service == null) {
498             Log.w(TAG, "isActive(): Service not available");
499             return false;
500         }
501         return service.mSession.isActive();
502     }
503 
504     /** Get Media session for updating state */
getSession()505     public static synchronized MediaSessionCompat getSession() {
506         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
507         if (service == null) {
508             Log.w(TAG, "getSession(): Service not available");
509             return null;
510         }
511         return service.mSession;
512     }
513 
514     /** Reset the state of BluetoothMediaBrowserService to that before a device connected */
reset()515     public static synchronized void reset() {
516         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
517         if (service == null) {
518             Log.w(TAG, "reset(): Service not available");
519             return;
520         }
521 
522         service.clearNowPlayingQueue();
523         service.mSession.setMetadata(null);
524         service.setErrorPlaybackState();
525         service.mSession.setCallback(null);
526         Log.d(TAG, "Service state has been reset");
527     }
528 
529     /** Get the state of the BluetoothMediaBrowserService as a debug string */
dump()530     public static synchronized String dump() {
531         StringBuilder sb = new StringBuilder();
532         sb.append(TAG).append(":");
533         BluetoothMediaBrowserService service = BluetoothMediaBrowserService.getInstance();
534         if (service != null) {
535             MediaSessionCompat session = service.getSession();
536             MediaControllerCompat controller = session.getController();
537             MediaMetadataCompat metadata = controller == null ? null : controller.getMetadata();
538             PlaybackStateCompat playbackState =
539                     controller == null ? null : controller.getPlaybackState();
540             List<MediaSessionCompat.QueueItem> queue =
541                     controller == null ? null : controller.getQueue();
542             if (metadata != null) {
543                 sb.append("\n    track={");
544                 sb.append("title=")
545                         .append(metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE));
546                 sb.append(", artist=")
547                         .append(metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST));
548                 sb.append(", album=")
549                         .append(metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM));
550                 sb.append(", duration=")
551                         .append(metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION));
552                 sb.append(", track_number=")
553                         .append(metadata.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER));
554                 sb.append(", total_tracks=")
555                         .append(metadata.getLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS));
556                 sb.append(", genre=")
557                         .append(metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE));
558                 sb.append(", album_art=")
559                         .append(metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI));
560                 sb.append("}");
561             } else {
562                 sb.append("\n    track=").append(metadata);
563             }
564             sb.append("\n    playbackState=")
565                     .append(AvrcpControllerUtils.playbackStateCompatToString(playbackState));
566             sb.append("\n    queue=").append(queue);
567             sb.append("\n    internal_queue=").append(service.mMediaQueue);
568             sb.append("\n    session active state=").append(isActive());
569         } else {
570             Log.w(TAG, "dump Unavailable");
571             sb.append(" null");
572         }
573         return sb.toString();
574     }
575 }
576