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