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