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 * <p>This service provides a means for external applications to access A2DP and AVRCP. The 45 * 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 * <p>The current behavior of MediaSessionCompat exposed by this service is as follows: 1. 49 * MediaSessionCompat is active (i.e. SystemUI and other overview UIs can see updates) when device 50 * is connected and first starts playing. Before it starts playing we do not activate the session. 51 * 1.1 The session is active throughout the duration of connection. 2. The session is de-activated 52 * when the device disconnects. It will be connected again when (1) happens. 53 */ 54 public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat { 55 private static final String TAG = BluetoothMediaBrowserService.class.getSimpleName(); 56 57 private static BluetoothMediaBrowserService sBluetoothMediaBrowserService; 58 59 private MediaSessionCompat mSession; 60 61 // Browsing related structures. 62 private List<MediaSessionCompat.QueueItem> mMediaQueue = new ArrayList<>(); 63 64 // Media Framework Content Style constants 65 private static final String CONTENT_STYLE_SUPPORTED = 66 "android.media.browse.CONTENT_STYLE_SUPPORTED"; 67 public static final String CONTENT_STYLE_PLAYABLE_HINT = 68 "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT"; 69 public static final String CONTENT_STYLE_BROWSABLE_HINT = 70 "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT"; 71 public static final int CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1; 72 public static final int CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2; 73 74 // Error messaging extras 75 public static final String ERROR_RESOLUTION_ACTION_INTENT = 76 "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT"; 77 public static final String ERROR_RESOLUTION_ACTION_LABEL = 78 "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL"; 79 80 // Receiver for making sure our error message text matches the system locale 81 private class LocaleChangedReceiver extends BroadcastReceiver { 82 @Override onReceive(Context context, Intent intent)83 public void onReceive(Context context, Intent intent) { 84 String action = intent.getAction(); 85 if (action.equals(Intent.ACTION_LOCALE_CHANGED)) { 86 Log.d(TAG, "Locale has updated"); 87 if (sBluetoothMediaBrowserService == null) return; 88 MediaSessionCompat session = sBluetoothMediaBrowserService.getSession(); 89 90 // Update playback state error message under new locale, if applicable 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 // Update queue title under new locale 99 session.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name)); 100 } 101 } 102 } 103 104 private LocaleChangedReceiver mReceiver; 105 106 /** 107 * Initialize this BluetoothMediaBrowserService, creating our MediaSessionCompat, MediaPlayer 108 * and MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService. 109 */ 110 @Override onCreate()111 public void onCreate() { 112 Log.d(TAG, "Service Created"); 113 super.onCreate(); 114 115 // Create and configure the MediaSessionCompat 116 mSession = new MediaSessionCompat(this, TAG); 117 setSessionToken(mSession.getSessionToken()); 118 mSession.setFlags( 119 MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS 120 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); 121 mSession.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name)); 122 mSession.setQueue(mMediaQueue); 123 setErrorPlaybackState(); 124 sBluetoothMediaBrowserService = this; 125 126 mReceiver = new LocaleChangedReceiver(); 127 IntentFilter filter = new IntentFilter(); 128 filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); 129 filter.addAction(Intent.ACTION_LOCALE_CHANGED); 130 registerReceiver(mReceiver, filter); 131 } 132 133 @Override onDestroy()134 public void onDestroy() { 135 Log.d(TAG, "Service Destroyed"); 136 unregisterReceiver(mReceiver); 137 mReceiver = null; 138 } 139 140 /** 141 * BrowseResult is used to return the contents of a node along with a status. The status is used 142 * to indicate success, a pending download, or error conditions. BrowseResult is used in 143 * onLoadChildren() and getContents() in BluetoothMediaBrowserService and in getContents() in 144 * AvrcpControllerService. The following statuses have been implemented: 1. SUCCESS - Contents 145 * have been retrieved successfully. 2. DOWNLOAD_PENDING - Download is in progress and may or 146 * may not have contents to return. 3. NO_DEVICE_CONNECTED - If no device is connected there are 147 * no contents to be retrieved. 4. ERROR_MEDIA_ID_INVALID - Contents could not be retrieved as 148 * the media ID is invalid. 5. ERROR_NO_AVRCP_SERVICE - Contents could not be retrieved as 149 * AvrcpControllerService is not connected. 150 */ 151 public static class BrowseResult { 152 // Possible statuses for onLoadChildren 153 public static final byte SUCCESS = 0x00; 154 public static final byte DOWNLOAD_PENDING = 0x01; 155 public static final byte NO_DEVICE_CONNECTED = 0x02; 156 public static final byte ERROR_MEDIA_ID_INVALID = 0x03; 157 public static final byte ERROR_NO_AVRCP_SERVICE = 0x04; 158 159 private List<MediaItem> mResults; 160 private final byte mStatus; 161 getResults()162 List<MediaItem> getResults() { 163 return mResults; 164 } 165 getStatus()166 byte getStatus() { 167 return mStatus; 168 } 169 getStatusString()170 String getStatusString() { 171 switch (mStatus) { 172 case DOWNLOAD_PENDING: 173 return "DOWNLOAD_PENDING"; 174 case SUCCESS: 175 return "SUCCESS"; 176 case NO_DEVICE_CONNECTED: 177 return "NO_DEVICE_CONNECTED"; 178 case ERROR_MEDIA_ID_INVALID: 179 return "ERROR_MEDIA_ID_INVALID"; 180 case ERROR_NO_AVRCP_SERVICE: 181 return "ERROR_NO_AVRCP_SERVICE"; 182 default: 183 return "UNDEFINED_ERROR_CASE"; 184 } 185 } 186 BrowseResult(List<MediaItem> results, byte status)187 BrowseResult(List<MediaItem> results, byte status) { 188 mResults = results; 189 mStatus = status; 190 } 191 } 192 getContents(final String parentMediaId)193 BrowseResult getContents(final String parentMediaId) { 194 AvrcpControllerService avrcpControllerService = 195 AvrcpControllerService.getAvrcpControllerService(); 196 if (avrcpControllerService == null) { 197 Log.w(TAG, "getContents(id=" + parentMediaId + "): AVRCP Controller Service not ready"); 198 return new BrowseResult(new ArrayList(0), BrowseResult.ERROR_NO_AVRCP_SERVICE); 199 } else { 200 return avrcpControllerService.getContents(parentMediaId); 201 } 202 } 203 setErrorPlaybackState()204 private void setErrorPlaybackState() { 205 Bundle extras = new Bundle(); 206 extras.putString( 207 ERROR_RESOLUTION_ACTION_LABEL, getString(R.string.bluetooth_connect_action)); 208 Intent launchIntent = new Intent(); 209 launchIntent.setAction(BluetoothPrefs.BLUETOOTH_SETTING_ACTION); 210 launchIntent.addCategory(BluetoothPrefs.BLUETOOTH_SETTING_CATEGORY); 211 int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; 212 PendingIntent pendingIntent = 213 PendingIntent.getActivity(getApplicationContext(), 0, launchIntent, flags); 214 extras.putParcelable(ERROR_RESOLUTION_ACTION_INTENT, pendingIntent); 215 PlaybackStateCompat errorState = 216 new PlaybackStateCompat.Builder() 217 .setErrorMessage(getString(R.string.bluetooth_disconnected)) 218 .setExtras(extras) 219 .setState(PlaybackStateCompat.STATE_ERROR, 0, 0) 220 .build(); 221 mSession.setPlaybackState(errorState); 222 } 223 getDefaultStyle()224 private Bundle getDefaultStyle() { 225 Bundle style = new Bundle(); 226 style.putBoolean(CONTENT_STYLE_SUPPORTED, true); 227 style.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE); 228 style.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE); 229 return style; 230 } 231 232 @Override onLoadChildren( final String parentMediaId, final Result<List<MediaItem>> result)233 public synchronized void onLoadChildren( 234 final String parentMediaId, final Result<List<MediaItem>> result) { 235 Log.d(TAG, "Request for contents, id= " + parentMediaId); 236 BrowseResult contents = getContents(parentMediaId); 237 byte status = contents.getStatus(); 238 List<MediaItem> results = contents.getResults(); 239 if (status == BrowseResult.DOWNLOAD_PENDING && results == null) { 240 Log.i(TAG, "Download pending - no results, id= " + parentMediaId); 241 result.detach(); 242 } else { 243 Log.d( 244 TAG, 245 "Received Contents, id= " 246 + parentMediaId 247 + ", status= " 248 + contents.getStatusString() 249 + ", results=" 250 + results); 251 result.sendResult(results); 252 } 253 } 254 255 @Override onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)256 public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { 257 Log.i(TAG, "Browser Client Connection Request, client='" + clientPackageName + "')"); 258 Bundle style = getDefaultStyle(); 259 return new BrowserRoot(BrowseTree.ROOT, style); 260 } 261 updateNowPlayingQueue(BrowseTree.BrowseNode node)262 private void updateNowPlayingQueue(BrowseTree.BrowseNode node) { 263 List<MediaItem> songList = node.getContents(); 264 mMediaQueue.clear(); 265 if (songList != null && songList.size() > 0) { 266 for (MediaItem song : songList) { 267 mMediaQueue.add( 268 new MediaSessionCompat.QueueItem( 269 song.getDescription(), mMediaQueue.size())); 270 } 271 mSession.setQueue(mMediaQueue); 272 } else { 273 mSession.setQueue(null); 274 } 275 Log.d(TAG, "Now Playing List Changed, queue=" + mMediaQueue); 276 } 277 clearNowPlayingQueue()278 private void clearNowPlayingQueue() { 279 mMediaQueue.clear(); 280 mSession.setQueue(null); 281 } 282 notifyChanged(BrowseTree.BrowseNode node)283 static synchronized void notifyChanged(BrowseTree.BrowseNode node) { 284 if (sBluetoothMediaBrowserService != null) { 285 if (node.getScope() == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) { 286 sBluetoothMediaBrowserService.updateNowPlayingQueue(node); 287 } else { 288 Log.d(TAG, "Browse Node contents changed, node=" + node); 289 sBluetoothMediaBrowserService.notifyChildrenChanged(node.getID()); 290 } 291 } 292 } 293 addressedPlayerChanged(MediaSessionCompat.Callback callback)294 static synchronized void addressedPlayerChanged(MediaSessionCompat.Callback callback) { 295 if (sBluetoothMediaBrowserService != null) { 296 if (callback == null) { 297 sBluetoothMediaBrowserService.setErrorPlaybackState(); 298 sBluetoothMediaBrowserService.clearNowPlayingQueue(); 299 } 300 sBluetoothMediaBrowserService.mSession.setCallback(callback); 301 } else { 302 Log.w(TAG, "addressedPlayerChanged Unavailable"); 303 } 304 } 305 trackChanged(AvrcpItem track)306 static synchronized void trackChanged(AvrcpItem track) { 307 Log.d(TAG, "Track Changed, track=" + track); 308 if (sBluetoothMediaBrowserService != null) { 309 if (track != null) { 310 sBluetoothMediaBrowserService.mSession.setMetadata(track.toMediaMetadata()); 311 } else { 312 sBluetoothMediaBrowserService.mSession.setMetadata(null); 313 } 314 315 } else { 316 Log.w(TAG, "trackChanged Unavailable"); 317 } 318 } 319 notifyChanged(PlaybackStateCompat playbackState)320 static synchronized void notifyChanged(PlaybackStateCompat playbackState) { 321 Log.d( 322 TAG, 323 "Playback State Changed, state=" 324 + AvrcpControllerUtils.playbackStateCompatToString(playbackState)); 325 if (sBluetoothMediaBrowserService != null) { 326 sBluetoothMediaBrowserService.mSession.setPlaybackState(playbackState); 327 } else { 328 Log.w(TAG, "notifyChanged Unavailable"); 329 } 330 } 331 332 /** Send AVRCP Play command */ play()333 public static synchronized void play() { 334 if (sBluetoothMediaBrowserService != null) { 335 sBluetoothMediaBrowserService.mSession.getController().getTransportControls().play(); 336 } else { 337 Log.w(TAG, "play Unavailable"); 338 } 339 } 340 341 /** Send AVRCP Pause command */ pause()342 public static synchronized void pause() { 343 if (sBluetoothMediaBrowserService != null) { 344 sBluetoothMediaBrowserService.mSession.getController().getTransportControls().pause(); 345 } else { 346 Log.w(TAG, "pause Unavailable"); 347 } 348 } 349 350 /** Get playback state */ getPlaybackState()351 public static synchronized PlaybackStateCompat getPlaybackState() { 352 if (sBluetoothMediaBrowserService != null) { 353 MediaSessionCompat session = sBluetoothMediaBrowserService.getSession(); 354 if (session == null) return null; 355 MediaControllerCompat controller = session.getController(); 356 PlaybackStateCompat playbackState = 357 controller == null ? null : controller.getPlaybackState(); 358 return playbackState; 359 } 360 return null; 361 } 362 363 /** Get object for controlling playback */ getTransportControls()364 public static synchronized MediaControllerCompat.TransportControls getTransportControls() { 365 if (sBluetoothMediaBrowserService != null) { 366 return sBluetoothMediaBrowserService.mSession.getController().getTransportControls(); 367 } else { 368 Log.w(TAG, "transportControls Unavailable"); 369 return null; 370 } 371 } 372 373 /** Set Media session active whenever we have Focus of any kind */ setActive(boolean active)374 public static synchronized void setActive(boolean active) { 375 if (sBluetoothMediaBrowserService != null) { 376 Log.d(TAG, "Setting the session active state to:" + active); 377 sBluetoothMediaBrowserService.mSession.setActive(active); 378 } else { 379 Log.w(TAG, "setActive Unavailable"); 380 } 381 } 382 383 /** 384 * Checks if the media session is active or not. 385 * 386 * @return true if media session is active, false otherwise. 387 */ 388 @VisibleForTesting isActive()389 public static synchronized boolean isActive() { 390 if (sBluetoothMediaBrowserService != null) { 391 return sBluetoothMediaBrowserService.mSession.isActive(); 392 } 393 return false; 394 } 395 396 /** Get Media session for updating state */ getSession()397 public static synchronized MediaSessionCompat getSession() { 398 if (sBluetoothMediaBrowserService != null) { 399 return sBluetoothMediaBrowserService.mSession; 400 } else { 401 Log.w(TAG, "getSession Unavailable"); 402 return null; 403 } 404 } 405 406 /** Reset the state of BluetoothMediaBrowserService to that before a device connected */ reset()407 public static synchronized void reset() { 408 if (sBluetoothMediaBrowserService != null) { 409 sBluetoothMediaBrowserService.clearNowPlayingQueue(); 410 sBluetoothMediaBrowserService.mSession.setMetadata(null); 411 sBluetoothMediaBrowserService.setErrorPlaybackState(); 412 sBluetoothMediaBrowserService.mSession.setCallback(null); 413 Log.d(TAG, "Service state has been reset"); 414 } else { 415 Log.w(TAG, "reset unavailable"); 416 } 417 } 418 419 /** Get the state of the BluetoothMediaBrowserService as a debug string */ dump()420 public static synchronized String dump() { 421 StringBuilder sb = new StringBuilder(); 422 sb.append(TAG + ":"); 423 if (sBluetoothMediaBrowserService != null) { 424 MediaSessionCompat session = sBluetoothMediaBrowserService.getSession(); 425 MediaControllerCompat controller = session.getController(); 426 MediaMetadataCompat metadata = controller == null ? null : controller.getMetadata(); 427 PlaybackStateCompat playbackState = 428 controller == null ? null : controller.getPlaybackState(); 429 List<MediaSessionCompat.QueueItem> queue = 430 controller == null ? null : controller.getQueue(); 431 if (metadata != null) { 432 sb.append("\n track={"); 433 sb.append("title=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)); 434 sb.append( 435 ", artist=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST)); 436 sb.append(", album=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM)); 437 sb.append( 438 ", duration=" 439 + metadata.getString(MediaMetadataCompat.METADATA_KEY_DURATION)); 440 sb.append( 441 ", track_number=" 442 + metadata.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER)); 443 sb.append( 444 ", total_tracks=" 445 + metadata.getLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS)); 446 sb.append(", genre=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE)); 447 sb.append( 448 ", album_art=" 449 + metadata.getString( 450 MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI)); 451 sb.append("}"); 452 } else { 453 sb.append("\n track=" + metadata); 454 } 455 sb.append( 456 "\n playbackState=" 457 + AvrcpControllerUtils.playbackStateCompatToString(playbackState)); 458 sb.append("\n queue=" + queue); 459 sb.append("\n internal_queue=" + sBluetoothMediaBrowserService.mMediaQueue); 460 sb.append("\n session active state=").append(isActive()); 461 } else { 462 Log.w(TAG, "dump Unavailable"); 463 sb.append(" null"); 464 } 465 return sb.toString(); 466 } 467 } 468