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 
37 import java.util.ArrayList;
38 import java.util.List;
39 
40 /**
41  * Implements the MediaBrowserService interface to AVRCP and A2DP
42  *
43  * This service provides a means for external applications to access A2DP and AVRCP.
44  * The applications are expected to use MediaBrowser (see API) and all the music
45  * browsing/playback/metadata can be controlled via MediaBrowser and MediaController.
46  *
47  * The current behavior of MediaSessionCompat exposed by this service is as follows:
48  * 1. MediaSessionCompat is active (i.e. SystemUI and other overview UIs can see updates) when
49  * device is connected and first starts playing. Before it starts playing we do not activate the
50  * session.
51  * 1.1 The session is active throughout the duration of connection.
52  * 2. The session is de-activated when the device disconnects. It will be connected again when (1)
53  * happens.
54  */
55 public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat {
56     private static final String TAG = "BluetoothMediaBrowserService";
57     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
58 
59     private static BluetoothMediaBrowserService sBluetoothMediaBrowserService;
60 
61     private MediaSessionCompat mSession;
62 
63     // Browsing related structures.
64     private List<MediaSessionCompat.QueueItem> mMediaQueue = new ArrayList<>();
65 
66     // Media Framework Content Style constants
67     private static final String CONTENT_STYLE_SUPPORTED =
68             "android.media.browse.CONTENT_STYLE_SUPPORTED";
69     public static final String CONTENT_STYLE_PLAYABLE_HINT =
70             "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT";
71     public static final String CONTENT_STYLE_BROWSABLE_HINT =
72             "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT";
73     public static final int CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1;
74     public static final int CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2;
75 
76     // Error messaging extras
77     public static final String ERROR_RESOLUTION_ACTION_INTENT =
78             "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT";
79     public static final String ERROR_RESOLUTION_ACTION_LABEL =
80             "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL";
81 
82     // Receiver for making sure our error message text matches the system locale
83     private class LocaleChangedReceiver extends BroadcastReceiver {
84         @Override
onReceive(Context context, Intent intent)85         public void onReceive(Context context, Intent intent) {
86             String action = intent.getAction();
87             if (action.equals(Intent.ACTION_LOCALE_CHANGED)) {
88                 if (sBluetoothMediaBrowserService == null) return;
89                 MediaSessionCompat session = sBluetoothMediaBrowserService.getSession();
90                 MediaControllerCompat controller = session.getController();
91                 PlaybackStateCompat playbackState =
92                         controller == null ? null : controller.getPlaybackState();
93                 if (playbackState != null && playbackState.getErrorMessage() != null) {
94                     setErrorPlaybackState();
95                 }
96             }
97         }
98     }
99 
100     private LocaleChangedReceiver mReceiver;
101 
102     /**
103      * Initialize this BluetoothMediaBrowserService, creating our MediaSessionCompat, MediaPlayer
104      * and MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService.
105      */
106     @Override
onCreate()107     public void onCreate() {
108         if (DBG) Log.d(TAG, "onCreate");
109         super.onCreate();
110 
111         // Create and configure the MediaSessionCompat
112         mSession = new MediaSessionCompat(this, TAG);
113         setSessionToken(mSession.getSessionToken());
114         mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
115                 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
116         mSession.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name));
117         mSession.setQueue(mMediaQueue);
118         setErrorPlaybackState();
119         sBluetoothMediaBrowserService = this;
120 
121         mReceiver = new LocaleChangedReceiver();
122         IntentFilter filter = new IntentFilter();
123         filter.addAction(Intent.ACTION_LOCALE_CHANGED);
124         registerReceiver(mReceiver, filter);
125     }
126 
127     @Override
onDestroy()128     public void onDestroy() {
129         unregisterReceiver(mReceiver);
130         mReceiver = null;
131     }
132 
getContents(final String parentMediaId)133     List<MediaItem> getContents(final String parentMediaId) {
134         AvrcpControllerService avrcpControllerService =
135                 AvrcpControllerService.getAvrcpControllerService();
136         if (avrcpControllerService == null) {
137             return new ArrayList(0);
138         } else {
139             return avrcpControllerService.getContents(parentMediaId);
140         }
141     }
142 
setErrorPlaybackState()143     private void setErrorPlaybackState() {
144         Bundle extras = new Bundle();
145         extras.putString(ERROR_RESOLUTION_ACTION_LABEL,
146                 getString(R.string.bluetooth_connect_action));
147         Intent launchIntent = new Intent();
148         launchIntent.setAction(BluetoothPrefs.BLUETOOTH_SETTING_ACTION);
149         launchIntent.addCategory(BluetoothPrefs.BLUETOOTH_SETTING_CATEGORY);
150         int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
151         PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0,
152                 launchIntent, flags);
153         extras.putParcelable(ERROR_RESOLUTION_ACTION_INTENT, pendingIntent);
154         PlaybackStateCompat errorState = new PlaybackStateCompat.Builder()
155                 .setErrorMessage(getString(R.string.bluetooth_disconnected))
156                 .setExtras(extras)
157                 .setState(PlaybackStateCompat.STATE_ERROR, 0, 0)
158                 .build();
159         mSession.setPlaybackState(errorState);
160     }
161 
getDefaultStyle()162     private Bundle getDefaultStyle() {
163         Bundle style = new Bundle();
164         style.putBoolean(CONTENT_STYLE_SUPPORTED, true);
165         style.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE);
166         style.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE);
167         return style;
168     }
169 
170     @Override
onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result)171     public synchronized void onLoadChildren(final String parentMediaId,
172             final Result<List<MediaItem>> result) {
173         if (DBG) Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId);
174         List<MediaItem> contents = getContents(parentMediaId);
175         if (contents == null) {
176             result.detach();
177         } else {
178             result.sendResult(contents);
179         }
180     }
181 
182     @Override
onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)183     public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
184         if (DBG) Log.d(TAG, "onGetRoot");
185         Bundle style = getDefaultStyle();
186         return new BrowserRoot(BrowseTree.ROOT, style);
187     }
188 
updateNowPlayingQueue(BrowseTree.BrowseNode node)189     private void updateNowPlayingQueue(BrowseTree.BrowseNode node) {
190         List<MediaItem> songList = node.getContents();
191         mMediaQueue.clear();
192         if (songList != null && songList.size() > 0) {
193             for (MediaItem song : songList) {
194                 mMediaQueue.add(new MediaSessionCompat.QueueItem(
195                         song.getDescription(),
196                         mMediaQueue.size()));
197             }
198             mSession.setQueue(mMediaQueue);
199         } else {
200             mSession.setQueue(null);
201         }
202     }
203 
clearNowPlayingQueue()204     private void clearNowPlayingQueue() {
205         mMediaQueue.clear();
206         mSession.setQueue(null);
207     }
208 
notifyChanged(BrowseTree.BrowseNode node)209     static synchronized void notifyChanged(BrowseTree.BrowseNode node) {
210         if (sBluetoothMediaBrowserService != null) {
211             if (node.getScope() == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
212                 sBluetoothMediaBrowserService.updateNowPlayingQueue(node);
213             } else {
214                 sBluetoothMediaBrowserService.notifyChildrenChanged(node.getID());
215             }
216         }
217     }
218 
addressedPlayerChanged(MediaSessionCompat.Callback callback)219     static synchronized void addressedPlayerChanged(MediaSessionCompat.Callback callback) {
220         if (sBluetoothMediaBrowserService != null) {
221             if (callback == null) {
222                 sBluetoothMediaBrowserService.setErrorPlaybackState();
223                 sBluetoothMediaBrowserService.clearNowPlayingQueue();
224             }
225             sBluetoothMediaBrowserService.mSession.setCallback(callback);
226         } else {
227             Log.w(TAG, "addressedPlayerChanged Unavailable");
228         }
229     }
230 
trackChanged(AvrcpItem track)231     static synchronized void trackChanged(AvrcpItem track) {
232         if (DBG) Log.d(TAG, "trackChanged setMetadata=" + track);
233         if (sBluetoothMediaBrowserService != null) {
234             if (track != null) {
235                 sBluetoothMediaBrowserService.mSession.setMetadata(track.toMediaMetadata());
236             } else {
237                 sBluetoothMediaBrowserService.mSession.setMetadata(null);
238             }
239 
240         } else {
241             Log.w(TAG, "trackChanged Unavailable");
242         }
243     }
244 
notifyChanged(PlaybackStateCompat playbackState)245     static synchronized void notifyChanged(PlaybackStateCompat playbackState) {
246         Log.d(TAG, "notifyChanged PlaybackState" + playbackState);
247         if (sBluetoothMediaBrowserService != null) {
248             sBluetoothMediaBrowserService.mSession.setPlaybackState(playbackState);
249         } else {
250             Log.w(TAG, "notifyChanged Unavailable");
251         }
252     }
253 
254     /**
255      * Send AVRCP Play command
256      */
play()257     public static synchronized void play() {
258         if (sBluetoothMediaBrowserService != null) {
259             sBluetoothMediaBrowserService.mSession.getController().getTransportControls().play();
260         } else {
261             Log.w(TAG, "play Unavailable");
262         }
263     }
264 
265     /**
266      * Send AVRCP Pause command
267      */
pause()268     public static synchronized void pause() {
269         if (sBluetoothMediaBrowserService != null) {
270             sBluetoothMediaBrowserService.mSession.getController().getTransportControls().pause();
271         } else {
272             Log.w(TAG, "pause Unavailable");
273         }
274     }
275 
276     /**
277      * Get playback state
278      */
getPlaybackState()279     public static synchronized int getPlaybackState() {
280         if (sBluetoothMediaBrowserService != null) {
281             PlaybackStateCompat currentPlaybackState =
282                     sBluetoothMediaBrowserService.mSession.getController().getPlaybackState();
283             if (currentPlaybackState != null) {
284                 return currentPlaybackState.getState();
285             }
286         }
287         return PlaybackStateCompat.STATE_ERROR;
288     }
289 
290     /**
291      * Get object for controlling playback
292      */
getTransportControls()293     public static synchronized MediaControllerCompat.TransportControls getTransportControls() {
294         if (sBluetoothMediaBrowserService != null) {
295             return sBluetoothMediaBrowserService.mSession.getController().getTransportControls();
296         } else {
297             Log.w(TAG, "transportControls Unavailable");
298             return null;
299         }
300     }
301 
302     /**
303      * Set Media session active whenever we have Focus of any kind
304      */
setActive(boolean active)305     public static synchronized void setActive(boolean active) {
306         if (sBluetoothMediaBrowserService != null) {
307             sBluetoothMediaBrowserService.mSession.setActive(active);
308         } else {
309             Log.w(TAG, "setActive Unavailable");
310         }
311     }
312 
313     /**
314      * Get Media session for updating state
315      */
getSession()316     public static synchronized MediaSessionCompat getSession() {
317         if (sBluetoothMediaBrowserService != null) {
318             return sBluetoothMediaBrowserService.mSession;
319         } else {
320             Log.w(TAG, "getSession Unavailable");
321             return null;
322         }
323     }
324 
325     /**
326      * Reset the state of BluetoothMediaBrowserService to that before a device connected
327      */
reset()328     public static synchronized void reset() {
329         if (sBluetoothMediaBrowserService != null) {
330             sBluetoothMediaBrowserService.clearNowPlayingQueue();
331             sBluetoothMediaBrowserService.mSession.setMetadata(null);
332             sBluetoothMediaBrowserService.setErrorPlaybackState();
333             sBluetoothMediaBrowserService.mSession.setCallback(null);
334             if (DBG) Log.d(TAG, "Service state has been reset");
335         } else {
336             Log.w(TAG, "reset unavailable");
337         }
338     }
339 
340     /**
341      * Get the state of the BluetoothMediaBrowserService as a debug string
342      */
dump()343     public static synchronized String dump() {
344         StringBuilder sb = new StringBuilder();
345         sb.append(TAG + ":");
346         if (sBluetoothMediaBrowserService != null) {
347             MediaSessionCompat session = sBluetoothMediaBrowserService.getSession();
348             MediaControllerCompat controller = session.getController();
349             MediaMetadataCompat metadata = controller == null ? null : controller.getMetadata();
350             PlaybackStateCompat playbackState =
351                     controller == null ? null : controller.getPlaybackState();
352             List<MediaSessionCompat.QueueItem> queue =
353                     controller == null ? null : controller.getQueue();
354             if (metadata != null) {
355                 sb.append("\n    track={");
356                 sb.append("title=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE));
357                 sb.append(", artist="
358                         + metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST));
359                 sb.append(", album=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM));
360                 sb.append(", track_number="
361                         + metadata.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER));
362                 sb.append(", total_tracks="
363                         + metadata.getLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS));
364                 sb.append(", genre=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE));
365                 sb.append(", album_art="
366                         + metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI));
367                 sb.append("}");
368             } else {
369                 sb.append("\n    track=" + metadata);
370             }
371             sb.append("\n    playbackState=" + playbackState);
372             sb.append("\n    queue=" + queue);
373             sb.append("\n    internal_queue=" + sBluetoothMediaBrowserService.mMediaQueue);
374         } else {
375             Log.w(TAG, "dump Unavailable");
376             sb.append(" null");
377         }
378         return sb.toString();
379     }
380 }
381