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 37 import java.util.ArrayList; 38 import java.util.List; 39 40 /** 41 * Implements the MediaBrowserService interface to AVRCP and A2DP 42 * 43 * This service provides a means for external applications to access A2DP and AVRCP. 44 * The applications are expected to use MediaBrowser (see API) and all the music 45 * browsing/playback/metadata can be controlled via MediaBrowser and MediaController. 46 * 47 * The current behavior of MediaSessionCompat exposed by this service is as follows: 48 * 1. MediaSessionCompat is active (i.e. SystemUI and other overview UIs can see updates) when 49 * device is connected and first starts playing. Before it starts playing we do not activate the 50 * session. 51 * 1.1 The session is active throughout the duration of connection. 52 * 2. The session is de-activated when the device disconnects. It will be connected again when (1) 53 * happens. 54 */ 55 public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat { 56 private static final String TAG = "BluetoothMediaBrowserService"; 57 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 58 59 private static BluetoothMediaBrowserService sBluetoothMediaBrowserService; 60 61 private MediaSessionCompat mSession; 62 63 // Browsing related structures. 64 private List<MediaSessionCompat.QueueItem> mMediaQueue = new ArrayList<>(); 65 66 // Media Framework Content Style constants 67 private static final String CONTENT_STYLE_SUPPORTED = 68 "android.media.browse.CONTENT_STYLE_SUPPORTED"; 69 public static final String CONTENT_STYLE_PLAYABLE_HINT = 70 "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT"; 71 public static final String CONTENT_STYLE_BROWSABLE_HINT = 72 "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT"; 73 public static final int CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1; 74 public static final int CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2; 75 76 // Error messaging extras 77 public static final String ERROR_RESOLUTION_ACTION_INTENT = 78 "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT"; 79 public static final String ERROR_RESOLUTION_ACTION_LABEL = 80 "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL"; 81 82 // Receiver for making sure our error message text matches the system locale 83 private class LocaleChangedReceiver extends BroadcastReceiver { 84 @Override onReceive(Context context, Intent intent)85 public void onReceive(Context context, Intent intent) { 86 String action = intent.getAction(); 87 if (action.equals(Intent.ACTION_LOCALE_CHANGED)) { 88 if (sBluetoothMediaBrowserService == null) return; 89 MediaSessionCompat session = sBluetoothMediaBrowserService.getSession(); 90 MediaControllerCompat controller = session.getController(); 91 PlaybackStateCompat playbackState = 92 controller == null ? null : controller.getPlaybackState(); 93 if (playbackState != null && playbackState.getErrorMessage() != null) { 94 setErrorPlaybackState(); 95 } 96 } 97 } 98 } 99 100 private LocaleChangedReceiver mReceiver; 101 102 /** 103 * Initialize this BluetoothMediaBrowserService, creating our MediaSessionCompat, MediaPlayer 104 * and MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService. 105 */ 106 @Override onCreate()107 public void onCreate() { 108 if (DBG) Log.d(TAG, "onCreate"); 109 super.onCreate(); 110 111 // Create and configure the MediaSessionCompat 112 mSession = new MediaSessionCompat(this, TAG); 113 setSessionToken(mSession.getSessionToken()); 114 mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS 115 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); 116 mSession.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name)); 117 mSession.setQueue(mMediaQueue); 118 setErrorPlaybackState(); 119 sBluetoothMediaBrowserService = this; 120 121 mReceiver = new LocaleChangedReceiver(); 122 IntentFilter filter = new IntentFilter(); 123 filter.addAction(Intent.ACTION_LOCALE_CHANGED); 124 registerReceiver(mReceiver, filter); 125 } 126 127 @Override onDestroy()128 public void onDestroy() { 129 unregisterReceiver(mReceiver); 130 mReceiver = null; 131 } 132 getContents(final String parentMediaId)133 List<MediaItem> getContents(final String parentMediaId) { 134 AvrcpControllerService avrcpControllerService = 135 AvrcpControllerService.getAvrcpControllerService(); 136 if (avrcpControllerService == null) { 137 return new ArrayList(0); 138 } else { 139 return avrcpControllerService.getContents(parentMediaId); 140 } 141 } 142 setErrorPlaybackState()143 private void setErrorPlaybackState() { 144 Bundle extras = new Bundle(); 145 extras.putString(ERROR_RESOLUTION_ACTION_LABEL, 146 getString(R.string.bluetooth_connect_action)); 147 Intent launchIntent = new Intent(); 148 launchIntent.setAction(BluetoothPrefs.BLUETOOTH_SETTING_ACTION); 149 launchIntent.addCategory(BluetoothPrefs.BLUETOOTH_SETTING_CATEGORY); 150 int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; 151 PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, 152 launchIntent, flags); 153 extras.putParcelable(ERROR_RESOLUTION_ACTION_INTENT, pendingIntent); 154 PlaybackStateCompat errorState = new PlaybackStateCompat.Builder() 155 .setErrorMessage(getString(R.string.bluetooth_disconnected)) 156 .setExtras(extras) 157 .setState(PlaybackStateCompat.STATE_ERROR, 0, 0) 158 .build(); 159 mSession.setPlaybackState(errorState); 160 } 161 getDefaultStyle()162 private Bundle getDefaultStyle() { 163 Bundle style = new Bundle(); 164 style.putBoolean(CONTENT_STYLE_SUPPORTED, true); 165 style.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE); 166 style.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE); 167 return style; 168 } 169 170 @Override onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result)171 public synchronized void onLoadChildren(final String parentMediaId, 172 final Result<List<MediaItem>> result) { 173 if (DBG) Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId); 174 List<MediaItem> contents = getContents(parentMediaId); 175 if (contents == null) { 176 result.detach(); 177 } else { 178 result.sendResult(contents); 179 } 180 } 181 182 @Override onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)183 public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { 184 if (DBG) Log.d(TAG, "onGetRoot"); 185 Bundle style = getDefaultStyle(); 186 return new BrowserRoot(BrowseTree.ROOT, style); 187 } 188 updateNowPlayingQueue(BrowseTree.BrowseNode node)189 private void updateNowPlayingQueue(BrowseTree.BrowseNode node) { 190 List<MediaItem> songList = node.getContents(); 191 mMediaQueue.clear(); 192 if (songList != null && songList.size() > 0) { 193 for (MediaItem song : songList) { 194 mMediaQueue.add(new MediaSessionCompat.QueueItem( 195 song.getDescription(), 196 mMediaQueue.size())); 197 } 198 mSession.setQueue(mMediaQueue); 199 } else { 200 mSession.setQueue(null); 201 } 202 } 203 clearNowPlayingQueue()204 private void clearNowPlayingQueue() { 205 mMediaQueue.clear(); 206 mSession.setQueue(null); 207 } 208 notifyChanged(BrowseTree.BrowseNode node)209 static synchronized void notifyChanged(BrowseTree.BrowseNode node) { 210 if (sBluetoothMediaBrowserService != null) { 211 if (node.getScope() == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) { 212 sBluetoothMediaBrowserService.updateNowPlayingQueue(node); 213 } else { 214 sBluetoothMediaBrowserService.notifyChildrenChanged(node.getID()); 215 } 216 } 217 } 218 addressedPlayerChanged(MediaSessionCompat.Callback callback)219 static synchronized void addressedPlayerChanged(MediaSessionCompat.Callback callback) { 220 if (sBluetoothMediaBrowserService != null) { 221 if (callback == null) { 222 sBluetoothMediaBrowserService.setErrorPlaybackState(); 223 sBluetoothMediaBrowserService.clearNowPlayingQueue(); 224 } 225 sBluetoothMediaBrowserService.mSession.setCallback(callback); 226 } else { 227 Log.w(TAG, "addressedPlayerChanged Unavailable"); 228 } 229 } 230 trackChanged(AvrcpItem track)231 static synchronized void trackChanged(AvrcpItem track) { 232 if (DBG) Log.d(TAG, "trackChanged setMetadata=" + track); 233 if (sBluetoothMediaBrowserService != null) { 234 if (track != null) { 235 sBluetoothMediaBrowserService.mSession.setMetadata(track.toMediaMetadata()); 236 } else { 237 sBluetoothMediaBrowserService.mSession.setMetadata(null); 238 } 239 240 } else { 241 Log.w(TAG, "trackChanged Unavailable"); 242 } 243 } 244 notifyChanged(PlaybackStateCompat playbackState)245 static synchronized void notifyChanged(PlaybackStateCompat playbackState) { 246 Log.d(TAG, "notifyChanged PlaybackState" + playbackState); 247 if (sBluetoothMediaBrowserService != null) { 248 sBluetoothMediaBrowserService.mSession.setPlaybackState(playbackState); 249 } else { 250 Log.w(TAG, "notifyChanged Unavailable"); 251 } 252 } 253 254 /** 255 * Send AVRCP Play command 256 */ play()257 public static synchronized void play() { 258 if (sBluetoothMediaBrowserService != null) { 259 sBluetoothMediaBrowserService.mSession.getController().getTransportControls().play(); 260 } else { 261 Log.w(TAG, "play Unavailable"); 262 } 263 } 264 265 /** 266 * Send AVRCP Pause command 267 */ pause()268 public static synchronized void pause() { 269 if (sBluetoothMediaBrowserService != null) { 270 sBluetoothMediaBrowserService.mSession.getController().getTransportControls().pause(); 271 } else { 272 Log.w(TAG, "pause Unavailable"); 273 } 274 } 275 276 /** 277 * Get playback state 278 */ getPlaybackState()279 public static synchronized int getPlaybackState() { 280 if (sBluetoothMediaBrowserService != null) { 281 PlaybackStateCompat currentPlaybackState = 282 sBluetoothMediaBrowserService.mSession.getController().getPlaybackState(); 283 if (currentPlaybackState != null) { 284 return currentPlaybackState.getState(); 285 } 286 } 287 return PlaybackStateCompat.STATE_ERROR; 288 } 289 290 /** 291 * Get object for controlling playback 292 */ getTransportControls()293 public static synchronized MediaControllerCompat.TransportControls getTransportControls() { 294 if (sBluetoothMediaBrowserService != null) { 295 return sBluetoothMediaBrowserService.mSession.getController().getTransportControls(); 296 } else { 297 Log.w(TAG, "transportControls Unavailable"); 298 return null; 299 } 300 } 301 302 /** 303 * Set Media session active whenever we have Focus of any kind 304 */ setActive(boolean active)305 public static synchronized void setActive(boolean active) { 306 if (sBluetoothMediaBrowserService != null) { 307 sBluetoothMediaBrowserService.mSession.setActive(active); 308 } else { 309 Log.w(TAG, "setActive Unavailable"); 310 } 311 } 312 313 /** 314 * Get Media session for updating state 315 */ getSession()316 public static synchronized MediaSessionCompat getSession() { 317 if (sBluetoothMediaBrowserService != null) { 318 return sBluetoothMediaBrowserService.mSession; 319 } else { 320 Log.w(TAG, "getSession Unavailable"); 321 return null; 322 } 323 } 324 325 /** 326 * Reset the state of BluetoothMediaBrowserService to that before a device connected 327 */ reset()328 public static synchronized void reset() { 329 if (sBluetoothMediaBrowserService != null) { 330 sBluetoothMediaBrowserService.clearNowPlayingQueue(); 331 sBluetoothMediaBrowserService.mSession.setMetadata(null); 332 sBluetoothMediaBrowserService.setErrorPlaybackState(); 333 sBluetoothMediaBrowserService.mSession.setCallback(null); 334 if (DBG) Log.d(TAG, "Service state has been reset"); 335 } else { 336 Log.w(TAG, "reset unavailable"); 337 } 338 } 339 340 /** 341 * Get the state of the BluetoothMediaBrowserService as a debug string 342 */ dump()343 public static synchronized String dump() { 344 StringBuilder sb = new StringBuilder(); 345 sb.append(TAG + ":"); 346 if (sBluetoothMediaBrowserService != null) { 347 MediaSessionCompat session = sBluetoothMediaBrowserService.getSession(); 348 MediaControllerCompat controller = session.getController(); 349 MediaMetadataCompat metadata = controller == null ? null : controller.getMetadata(); 350 PlaybackStateCompat playbackState = 351 controller == null ? null : controller.getPlaybackState(); 352 List<MediaSessionCompat.QueueItem> queue = 353 controller == null ? null : controller.getQueue(); 354 if (metadata != null) { 355 sb.append("\n track={"); 356 sb.append("title=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)); 357 sb.append(", artist=" 358 + metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST)); 359 sb.append(", album=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM)); 360 sb.append(", track_number=" 361 + metadata.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER)); 362 sb.append(", total_tracks=" 363 + metadata.getLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS)); 364 sb.append(", genre=" + metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE)); 365 sb.append(", album_art=" 366 + metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI)); 367 sb.append("}"); 368 } else { 369 sb.append("\n track=" + metadata); 370 } 371 sb.append("\n playbackState=" + playbackState); 372 sb.append("\n queue=" + queue); 373 sb.append("\n internal_queue=" + sBluetoothMediaBrowserService.mMediaQueue); 374 } else { 375 Log.w(TAG, "dump Unavailable"); 376 sb.append(" null"); 377 } 378 return sb.toString(); 379 } 380 } 381