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