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.Intent;
21 import android.os.Bundle;
22 import android.support.v4.media.MediaBrowserCompat.MediaItem;
23 import android.support.v4.media.session.MediaControllerCompat;
24 import android.support.v4.media.session.MediaSessionCompat;
25 import android.support.v4.media.session.PlaybackStateCompat;
26 import android.util.Log;
27 
28 import androidx.media.MediaBrowserServiceCompat;
29 
30 import com.android.bluetooth.BluetoothPrefs;
31 import com.android.bluetooth.R;
32 
33 import java.util.ArrayList;
34 import java.util.List;
35 
36 /**
37  * Implements the MediaBrowserService interface to AVRCP and A2DP
38  *
39  * This service provides a means for external applications to access A2DP and AVRCP.
40  * The applications are expected to use MediaBrowser (see API) and all the music
41  * browsing/playback/metadata can be controlled via MediaBrowser and MediaController.
42  *
43  * The current behavior of MediaSessionCompat exposed by this service is as follows:
44  * 1. MediaSessionCompat is active (i.e. SystemUI and other overview UIs can see updates) when
45  * device is connected and first starts playing. Before it starts playing we do not activate the
46  * session.
47  * 1.1 The session is active throughout the duration of connection.
48  * 2. The session is de-activated when the device disconnects. It will be connected again when (1)
49  * happens.
50  */
51 public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat {
52     private static final String TAG = "BluetoothMediaBrowserService";
53     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
54 
55     private static BluetoothMediaBrowserService sBluetoothMediaBrowserService;
56 
57     private MediaSessionCompat mSession;
58 
59     // Browsing related structures.
60     private List<MediaSessionCompat.QueueItem> mMediaQueue = new ArrayList<>();
61 
62     // Media Framework Content Style constants
63     private static final String CONTENT_STYLE_SUPPORTED =
64             "android.media.browse.CONTENT_STYLE_SUPPORTED";
65     public static final String CONTENT_STYLE_PLAYABLE_HINT =
66             "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT";
67     public static final String CONTENT_STYLE_BROWSABLE_HINT =
68             "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT";
69     public static final int CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1;
70     public static final int CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2;
71 
72     // Error messaging extras
73     public static final String ERROR_RESOLUTION_ACTION_INTENT =
74             "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT";
75     public static final String ERROR_RESOLUTION_ACTION_LABEL =
76             "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL";
77 
78     /**
79      * Initialize this BluetoothMediaBrowserService, creating our MediaSessionCompat, MediaPlayer
80      * and MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService.
81      */
82     @Override
onCreate()83     public void onCreate() {
84         if (DBG) Log.d(TAG, "onCreate");
85         super.onCreate();
86 
87         // Create and configure the MediaSessionCompat
88         mSession = new MediaSessionCompat(this, TAG);
89         setSessionToken(mSession.getSessionToken());
90         mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
91                 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
92         mSession.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name));
93         mSession.setQueue(mMediaQueue);
94         setErrorPlaybackState();
95         sBluetoothMediaBrowserService = this;
96     }
97 
getContents(final String parentMediaId)98     List<MediaItem> getContents(final String parentMediaId) {
99         AvrcpControllerService avrcpControllerService =
100                 AvrcpControllerService.getAvrcpControllerService();
101         if (avrcpControllerService == null) {
102             return new ArrayList(0);
103         } else {
104             return avrcpControllerService.getContents(parentMediaId);
105         }
106     }
107 
setErrorPlaybackState()108     private void setErrorPlaybackState() {
109         Bundle extras = new Bundle();
110         extras.putString(ERROR_RESOLUTION_ACTION_LABEL,
111                 getString(R.string.bluetooth_connect_action));
112         Intent launchIntent = new Intent();
113         launchIntent.setAction(BluetoothPrefs.BLUETOOTH_SETTING_ACTION);
114         launchIntent.addCategory(BluetoothPrefs.BLUETOOTH_SETTING_CATEGORY);
115         PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0,
116                 launchIntent, PendingIntent.FLAG_UPDATE_CURRENT);
117         extras.putParcelable(ERROR_RESOLUTION_ACTION_INTENT, pendingIntent);
118         PlaybackStateCompat errorState = new PlaybackStateCompat.Builder()
119                 .setErrorMessage(getString(R.string.bluetooth_disconnected))
120                 .setExtras(extras)
121                 .setState(PlaybackStateCompat.STATE_ERROR, 0, 0)
122                 .build();
123         mSession.setPlaybackState(errorState);
124     }
125 
getDefaultStyle()126     private Bundle getDefaultStyle() {
127         Bundle style = new Bundle();
128         style.putBoolean(CONTENT_STYLE_SUPPORTED, true);
129         style.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE);
130         style.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE);
131         return style;
132     }
133 
134     @Override
onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result)135     public synchronized void onLoadChildren(final String parentMediaId,
136             final Result<List<MediaItem>> result) {
137         if (DBG) Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId);
138         List<MediaItem> contents = getContents(parentMediaId);
139         if (contents == null) {
140             result.detach();
141         } else {
142             result.sendResult(contents);
143         }
144     }
145 
146     @Override
onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)147     public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
148         if (DBG) Log.d(TAG, "onGetRoot");
149         Bundle style = getDefaultStyle();
150         return new BrowserRoot(BrowseTree.ROOT, style);
151     }
152 
updateNowPlayingQueue(BrowseTree.BrowseNode node)153     private void updateNowPlayingQueue(BrowseTree.BrowseNode node) {
154         List<MediaItem> songList = node.getContents();
155         mMediaQueue.clear();
156         if (songList != null) {
157             for (MediaItem song : songList) {
158                 mMediaQueue.add(new MediaSessionCompat.QueueItem(
159                         song.getDescription(),
160                         mMediaQueue.size()));
161             }
162         }
163         mSession.setQueue(mMediaQueue);
164     }
165 
clearNowPlayingQueue()166     private void clearNowPlayingQueue() {
167         mMediaQueue.clear();
168         mSession.setQueue(mMediaQueue);
169     }
170 
notifyChanged(BrowseTree.BrowseNode node)171     static synchronized void notifyChanged(BrowseTree.BrowseNode node) {
172         if (sBluetoothMediaBrowserService != null) {
173             if (node.getScope() == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
174                 sBluetoothMediaBrowserService.updateNowPlayingQueue(node);
175             } else {
176                 sBluetoothMediaBrowserService.notifyChildrenChanged(node.getID());
177             }
178         }
179     }
180 
addressedPlayerChanged(MediaSessionCompat.Callback callback)181     static synchronized void addressedPlayerChanged(MediaSessionCompat.Callback callback) {
182         if (sBluetoothMediaBrowserService != null) {
183             if (callback == null) {
184                 sBluetoothMediaBrowserService.setErrorPlaybackState();
185                 sBluetoothMediaBrowserService.clearNowPlayingQueue();
186             }
187             sBluetoothMediaBrowserService.mSession.setCallback(callback);
188         } else {
189             Log.w(TAG, "addressedPlayerChanged Unavailable");
190         }
191     }
192 
trackChanged(AvrcpItem track)193     static synchronized void trackChanged(AvrcpItem track) {
194         if (DBG) Log.d(TAG, "trackChanged setMetadata=" + track);
195         if (sBluetoothMediaBrowserService != null) {
196             if (track != null) {
197                 sBluetoothMediaBrowserService.mSession.setMetadata(track.toMediaMetadata());
198             } else {
199                 sBluetoothMediaBrowserService.mSession.setMetadata(null);
200             }
201 
202         } else {
203             Log.w(TAG, "trackChanged Unavailable");
204         }
205     }
206 
notifyChanged(PlaybackStateCompat playbackState)207     static synchronized void notifyChanged(PlaybackStateCompat playbackState) {
208         Log.d(TAG, "notifyChanged PlaybackState" + playbackState);
209         if (sBluetoothMediaBrowserService != null) {
210             sBluetoothMediaBrowserService.mSession.setPlaybackState(playbackState);
211         } else {
212             Log.w(TAG, "notifyChanged Unavailable");
213         }
214     }
215 
216     /**
217      * Send AVRCP Play command
218      */
play()219     public static synchronized void play() {
220         if (sBluetoothMediaBrowserService != null) {
221             sBluetoothMediaBrowserService.mSession.getController().getTransportControls().play();
222         } else {
223             Log.w(TAG, "play Unavailable");
224         }
225     }
226 
227     /**
228      * Send AVRCP Pause command
229      */
pause()230     public static synchronized void pause() {
231         if (sBluetoothMediaBrowserService != null) {
232             sBluetoothMediaBrowserService.mSession.getController().getTransportControls().pause();
233         } else {
234             Log.w(TAG, "pause Unavailable");
235         }
236     }
237 
238     /**
239      * Get playback state
240      */
getPlaybackState()241     public static synchronized int getPlaybackState() {
242         if (sBluetoothMediaBrowserService != null) {
243             PlaybackStateCompat currentPlaybackState =
244                     sBluetoothMediaBrowserService.mSession.getController().getPlaybackState();
245             if (currentPlaybackState != null) {
246                 return currentPlaybackState.getState();
247             }
248         }
249         return PlaybackStateCompat.STATE_ERROR;
250     }
251 
252     /**
253      * Get object for controlling playback
254      */
getTransportControls()255     public static synchronized MediaControllerCompat.TransportControls getTransportControls() {
256         if (sBluetoothMediaBrowserService != null) {
257             return sBluetoothMediaBrowserService.mSession.getController().getTransportControls();
258         } else {
259             Log.w(TAG, "transportControls Unavailable");
260             return null;
261         }
262     }
263 
264     /**
265      * Set Media session active whenever we have Focus of any kind
266      */
setActive(boolean active)267     public static synchronized void setActive(boolean active) {
268         if (sBluetoothMediaBrowserService != null) {
269             sBluetoothMediaBrowserService.mSession.setActive(active);
270         } else {
271             Log.w(TAG, "setActive Unavailable");
272         }
273     }
274 
275     /**
276      * Get Media session for updating state
277      */
getSession()278     public static synchronized MediaSessionCompat getSession() {
279         if (sBluetoothMediaBrowserService != null) {
280             return sBluetoothMediaBrowserService.mSession;
281         } else {
282             Log.w(TAG, "getSession Unavailable");
283             return null;
284         }
285     }
286 }
287