1 /* 2 * Copyright 2018 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.avrcp; 18 19 import android.annotation.RequiresPermission; 20 import android.bluetooth.BluetoothA2dp; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothProfile; 23 import android.bluetooth.IBluetoothAvrcpTarget; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.media.AudioManager; 29 import android.os.Looper; 30 import android.os.SystemProperties; 31 import android.os.UserManager; 32 import android.util.Log; 33 34 import com.android.bluetooth.BluetoothMetricsProto; 35 import com.android.bluetooth.R; 36 import com.android.bluetooth.Utils; 37 import com.android.bluetooth.a2dp.A2dpService; 38 import com.android.bluetooth.audio_util.BTAudioEventLogger; 39 import com.android.bluetooth.audio_util.MediaData; 40 import com.android.bluetooth.audio_util.MediaPlayerList; 41 import com.android.bluetooth.audio_util.MediaPlayerWrapper; 42 import com.android.bluetooth.audio_util.Metadata; 43 import com.android.bluetooth.audio_util.PlayStatus; 44 import com.android.bluetooth.audio_util.PlayerInfo; 45 import com.android.bluetooth.btservice.MetricsLogger; 46 import com.android.bluetooth.btservice.ProfileService; 47 import com.android.bluetooth.btservice.ServiceFactory; 48 import com.android.internal.annotations.VisibleForTesting; 49 50 import java.util.List; 51 import java.util.Objects; 52 53 /** 54 * Provides Bluetooth AVRCP Target profile as a service in the Bluetooth application. 55 * @hide 56 */ 57 public class AvrcpTargetService extends ProfileService { 58 private static final String TAG = "AvrcpTargetService"; 59 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 60 private static final String AVRCP_ENABLE_PROPERTY = "persist.bluetooth.enablenewavrcp"; 61 62 private static final int AVRCP_MAX_VOL = 127; 63 private static final int MEDIA_KEY_EVENT_LOGGER_SIZE = 20; 64 private static final String MEDIA_KEY_EVENT_LOGGER_TITLE = "Media Key Events"; 65 private static int sDeviceMaxVolume = 0; 66 private final BTAudioEventLogger mMediaKeyEventLogger = new BTAudioEventLogger( 67 MEDIA_KEY_EVENT_LOGGER_SIZE, MEDIA_KEY_EVENT_LOGGER_TITLE); 68 69 private AvrcpVersion mAvrcpVersion; 70 private MediaPlayerList mMediaPlayerList; 71 private AudioManager mAudioManager; 72 private AvrcpBroadcastReceiver mReceiver; 73 private AvrcpNativeInterface mNativeInterface; 74 private AvrcpVolumeManager mVolumeManager; 75 private ServiceFactory mFactory = new ServiceFactory(); 76 77 // Only used to see if the metadata has changed from its previous value 78 private MediaData mCurrentData; 79 80 // Cover Art Service (Storage + BIP Server) 81 private AvrcpCoverArtService mAvrcpCoverArtService = null; 82 83 private static AvrcpTargetService sInstance = null; 84 85 class ListCallback implements MediaPlayerList.MediaUpdateCallback { 86 @Override run(MediaData data)87 public void run(MediaData data) { 88 if (mNativeInterface == null) return; 89 90 boolean metadata = !Objects.equals(mCurrentData.metadata, data.metadata); 91 boolean state = !MediaPlayerWrapper.playstateEquals(mCurrentData.state, data.state); 92 boolean queue = !Objects.equals(mCurrentData.queue, data.queue); 93 94 if (DEBUG) { 95 Log.d(TAG, "onMediaUpdated: track_changed=" + metadata 96 + " state=" + state + " queue=" + queue); 97 } 98 mCurrentData = data; 99 100 mNativeInterface.sendMediaUpdate(metadata, state, queue); 101 } 102 103 @Override run(boolean availablePlayers, boolean addressedPlayers, boolean uids)104 public void run(boolean availablePlayers, boolean addressedPlayers, 105 boolean uids) { 106 if (mNativeInterface == null) return; 107 108 mNativeInterface.sendFolderUpdate(availablePlayers, addressedPlayers, uids); 109 } 110 } 111 112 private class AvrcpBroadcastReceiver extends BroadcastReceiver { 113 @Override onReceive(Context context, Intent intent)114 public void onReceive(Context context, Intent intent) { 115 String action = intent.getAction(); 116 if (action.equals(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) { 117 if (mNativeInterface == null) return; 118 119 // Update all the playback status info for each connected device 120 mNativeInterface.sendMediaUpdate(false, true, false); 121 } else if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { 122 if (mNativeInterface == null) return; 123 124 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 125 if (device == null) return; 126 127 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 128 if (state == BluetoothProfile.STATE_DISCONNECTED) { 129 // If there is no connection, disconnectDevice() will do nothing 130 if (mNativeInterface.disconnectDevice(device.getAddress())) { 131 Log.d(TAG, "request to disconnect device " + device); 132 } 133 } 134 } else if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) { 135 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); 136 if (streamType == AudioManager.STREAM_MUSIC) { 137 int volume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); 138 BluetoothDevice activeDevice = getA2dpActiveDevice(); 139 if (activeDevice != null 140 && !mVolumeManager.getAbsoluteVolumeSupported(activeDevice)) { 141 Log.d(TAG, "stream volume change to " + volume + " " + activeDevice); 142 mVolumeManager.storeVolumeForDevice(activeDevice, volume); 143 } 144 } 145 } 146 } 147 } 148 149 /** 150 * Set the AvrcpTargetService instance. 151 */ 152 @VisibleForTesting set(AvrcpTargetService instance)153 public static void set(AvrcpTargetService instance) { 154 sInstance = instance; 155 } 156 157 /** 158 * Get the AvrcpTargetService instance. Returns null if the service hasn't been initialized. 159 */ get()160 public static AvrcpTargetService get() { 161 return sInstance; 162 } 163 getCoverArtService()164 public AvrcpCoverArtService getCoverArtService() { 165 return mAvrcpCoverArtService; 166 } 167 168 @Override getName()169 public String getName() { 170 return TAG; 171 } 172 173 @Override initBinder()174 protected IProfileServiceBinder initBinder() { 175 return new AvrcpTargetBinder(this); 176 } 177 178 @Override setUserUnlocked(int userId)179 protected void setUserUnlocked(int userId) { 180 Log.i(TAG, "User unlocked, initializing the service"); 181 182 if (!SystemProperties.getBoolean(AVRCP_ENABLE_PROPERTY, true)) { 183 Log.w(TAG, "Skipping initialization of the new AVRCP Target Player List"); 184 sInstance = null; 185 return; 186 } 187 188 if (mMediaPlayerList != null) { 189 mMediaPlayerList.init(new ListCallback()); 190 } 191 } 192 193 @Override start()194 protected boolean start() { 195 if (sInstance != null) { 196 Log.wtf(TAG, "The service has already been initialized"); 197 return false; 198 } 199 200 Log.i(TAG, "Starting the AVRCP Target Service"); 201 mCurrentData = new MediaData(null, null, null); 202 203 if (!SystemProperties.getBoolean(AVRCP_ENABLE_PROPERTY, true)) { 204 Log.w(TAG, "Skipping initialization of the new AVRCP Target Service"); 205 sInstance = null; 206 return true; 207 } 208 209 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 210 sDeviceMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 211 212 mMediaPlayerList = new MediaPlayerList(Looper.myLooper(), this); 213 214 mNativeInterface = AvrcpNativeInterface.getInterface(); 215 mNativeInterface.init(AvrcpTargetService.this); 216 217 mAvrcpVersion = AvrcpVersion.getCurrentSystemPropertiesValue(); 218 219 mVolumeManager = new AvrcpVolumeManager(this, mAudioManager, mNativeInterface); 220 221 UserManager userManager = UserManager.get(getApplicationContext()); 222 if (userManager.isUserUnlocked()) { 223 mMediaPlayerList.init(new ListCallback()); 224 } 225 226 if (getResources().getBoolean(R.bool.avrcp_target_enable_cover_art)) { 227 if (mAvrcpVersion.isAtleastVersion(AvrcpVersion.AVRCP_VERSION_1_6)) { 228 mAvrcpCoverArtService = new AvrcpCoverArtService(this); 229 boolean started = mAvrcpCoverArtService.start(); 230 if (!started) { 231 Log.e(TAG, "Failed to start cover art service"); 232 mAvrcpCoverArtService = null; 233 } 234 } else { 235 Log.e(TAG, "Please use AVRCP version 1.6 to enable cover art"); 236 } 237 } 238 239 mReceiver = new AvrcpBroadcastReceiver(); 240 IntentFilter filter = new IntentFilter(); 241 filter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED); 242 filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); 243 filter.addAction(AudioManager.VOLUME_CHANGED_ACTION); 244 registerReceiver(mReceiver, filter); 245 246 // Only allow the service to be used once it is initialized 247 sInstance = this; 248 249 return true; 250 } 251 252 @Override stop()253 protected boolean stop() { 254 Log.i(TAG, "Stopping the AVRCP Target Service"); 255 256 if (sInstance == null) { 257 Log.w(TAG, "stop() called before start()"); 258 return true; 259 } 260 261 if (mAvrcpCoverArtService != null) { 262 mAvrcpCoverArtService.stop(); 263 } 264 mAvrcpCoverArtService = null; 265 266 sInstance = null; 267 unregisterReceiver(mReceiver); 268 269 // We check the interfaces first since they only get set on User Unlocked 270 if (mMediaPlayerList != null) mMediaPlayerList.cleanup(); 271 if (mNativeInterface != null) mNativeInterface.cleanup(); 272 273 mMediaPlayerList = null; 274 mNativeInterface = null; 275 mAudioManager = null; 276 mReceiver = null; 277 return true; 278 } 279 init()280 private void init() { 281 } 282 getA2dpActiveDevice()283 private BluetoothDevice getA2dpActiveDevice() { 284 A2dpService service = mFactory.getA2dpService(); 285 if (service == null) { 286 return null; 287 } 288 return service.getActiveDevice(); 289 } 290 setA2dpActiveDevice(BluetoothDevice device)291 private void setA2dpActiveDevice(BluetoothDevice device) { 292 A2dpService service = A2dpService.getA2dpService(); 293 if (service == null) { 294 Log.d(TAG, "setA2dpActiveDevice: A2dp service not found"); 295 return; 296 } 297 service.setActiveDevice(device); 298 } 299 deviceConnected(BluetoothDevice device, boolean absoluteVolume)300 void deviceConnected(BluetoothDevice device, boolean absoluteVolume) { 301 Log.i(TAG, "deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume); 302 mVolumeManager.deviceConnected(device, absoluteVolume); 303 MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.AVRCP); 304 } 305 deviceDisconnected(BluetoothDevice device)306 void deviceDisconnected(BluetoothDevice device) { 307 Log.i(TAG, "deviceDisconnected: device=" + device); 308 mVolumeManager.deviceDisconnected(device); 309 } 310 311 /** 312 * Signal to the service that the current audio out device has changed and to inform 313 * the audio service whether the new device supports absolute volume. If it does, also 314 * set the absolute volume level on the remote device. 315 */ volumeDeviceSwitched(BluetoothDevice device)316 public void volumeDeviceSwitched(BluetoothDevice device) { 317 if (DEBUG) { 318 Log.d(TAG, "volumeDeviceSwitched: device=" + device); 319 } 320 mVolumeManager.volumeDeviceSwitched(device); 321 } 322 323 /** 324 * Remove the stored volume for a device. 325 */ removeStoredVolumeForDevice(BluetoothDevice device)326 public void removeStoredVolumeForDevice(BluetoothDevice device) { 327 if (device == null) return; 328 329 mVolumeManager.removeStoredVolumeForDevice(device); 330 } 331 332 /** 333 * Retrieve the remembered volume for a device. Returns -1 if there is no volume for the 334 * device. 335 */ getRememberedVolumeForDevice(BluetoothDevice device)336 public int getRememberedVolumeForDevice(BluetoothDevice device) { 337 if (device == null) return -1; 338 339 return mVolumeManager.getVolume(device, mVolumeManager.getNewDeviceVolume()); 340 } 341 342 // TODO (apanicke): Add checks to rejectlist Absolute Volume devices if they behave poorly. setVolume(int avrcpVolume)343 void setVolume(int avrcpVolume) { 344 BluetoothDevice activeDevice = getA2dpActiveDevice(); 345 if (activeDevice == null) { 346 Log.d(TAG, "setVolume: no active device"); 347 return; 348 } 349 350 mVolumeManager.setVolume(activeDevice, avrcpVolume); 351 } 352 353 /** 354 * Set the volume on the remote device. Does nothing if the device doesn't support absolute 355 * volume. 356 */ sendVolumeChanged(int deviceVolume)357 public void sendVolumeChanged(int deviceVolume) { 358 BluetoothDevice activeDevice = getA2dpActiveDevice(); 359 if (activeDevice == null) { 360 Log.d(TAG, "sendVolumeChanged: no active device"); 361 return; 362 } 363 364 mVolumeManager.sendVolumeChanged(activeDevice, deviceVolume); 365 } 366 getCurrentSongInfo()367 Metadata getCurrentSongInfo() { 368 Metadata metadata = mMediaPlayerList.getCurrentSongInfo(); 369 if (mAvrcpCoverArtService != null && metadata.image != null) { 370 String imageHandle = mAvrcpCoverArtService.storeImage(metadata.image); 371 if (imageHandle != null) metadata.image.setImageHandle(imageHandle); 372 } 373 return metadata; 374 } 375 getPlayState()376 PlayStatus getPlayState() { 377 return PlayStatus.fromPlaybackState(mMediaPlayerList.getCurrentPlayStatus(), 378 Long.parseLong(getCurrentSongInfo().duration)); 379 } 380 getCurrentMediaId()381 String getCurrentMediaId() { 382 String id = mMediaPlayerList.getCurrentMediaId(); 383 if (id != null) return id; 384 385 Metadata song = getCurrentSongInfo(); 386 if (song != null) return song.mediaId; 387 388 // We always want to return something, the error string just makes debugging easier 389 return "error"; 390 } 391 getNowPlayingList()392 List<Metadata> getNowPlayingList() { 393 String currentMediaId = getCurrentMediaId(); 394 Metadata currentTrack = null; 395 String imageHandle = null; 396 List<Metadata> nowPlayingList = mMediaPlayerList.getNowPlayingList(); 397 if (mAvrcpCoverArtService != null) { 398 for (Metadata metadata : nowPlayingList) { 399 if (metadata.mediaId == currentMediaId) { 400 currentTrack = metadata; 401 } else if (metadata.image != null) { 402 imageHandle = mAvrcpCoverArtService.storeImage(metadata.image); 403 if (imageHandle != null) { 404 metadata.image.setImageHandle(imageHandle); 405 } 406 } 407 } 408 409 // Always store the current item from the queue last so we know the image is in storage 410 if (currentTrack != null) { 411 imageHandle = mAvrcpCoverArtService.storeImage(currentTrack.image); 412 if (imageHandle != null) { 413 currentTrack.image.setImageHandle(imageHandle); 414 } 415 } 416 } 417 return nowPlayingList; 418 } 419 getCurrentPlayerId()420 int getCurrentPlayerId() { 421 return mMediaPlayerList.getCurrentPlayerId(); 422 } 423 424 // TODO (apanicke): Have the Player List also contain info about the play state of each player getMediaPlayerList()425 List<PlayerInfo> getMediaPlayerList() { 426 return mMediaPlayerList.getMediaPlayerList(); 427 } 428 getPlayerRoot(int playerId, MediaPlayerList.GetPlayerRootCallback cb)429 void getPlayerRoot(int playerId, MediaPlayerList.GetPlayerRootCallback cb) { 430 mMediaPlayerList.getPlayerRoot(playerId, cb); 431 } 432 getFolderItems(int playerId, String mediaId, MediaPlayerList.GetFolderItemsCallback cb)433 void getFolderItems(int playerId, String mediaId, MediaPlayerList.GetFolderItemsCallback cb) { 434 mMediaPlayerList.getFolderItems(playerId, mediaId, cb); 435 } 436 playItem(int playerId, boolean nowPlaying, String mediaId)437 void playItem(int playerId, boolean nowPlaying, String mediaId) { 438 // NOTE: playerId isn't used if nowPlaying is true, since its assumed to be the current 439 // active player 440 mMediaPlayerList.playItem(playerId, nowPlaying, mediaId); 441 } 442 443 // TODO (apanicke): Handle key events here in the service. Currently it was more convenient to 444 // handle them there but logically they make more sense handled here. sendMediaKeyEvent(int event, boolean pushed)445 void sendMediaKeyEvent(int event, boolean pushed) { 446 BluetoothDevice activeDevice = getA2dpActiveDevice(); 447 MediaPlayerWrapper player = mMediaPlayerList.getActivePlayer(); 448 mMediaKeyEventLogger.logd(DEBUG, TAG, "getMediaKeyEvent:" + " device=" + activeDevice 449 + " event=" + event + " pushed=" + pushed 450 + " to " + (player == null ? null : player.getPackageName())); 451 mMediaPlayerList.sendMediaKeyEvent(event, pushed); 452 } 453 setActiveDevice(BluetoothDevice device)454 void setActiveDevice(BluetoothDevice device) { 455 Log.i(TAG, "setActiveDevice: device=" + device); 456 if (device == null) { 457 Log.wtf(TAG, "setActiveDevice: could not find device " + device); 458 } 459 setA2dpActiveDevice(device); 460 } 461 462 /** 463 * Dump debugging information to the string builder 464 */ dump(StringBuilder sb)465 public void dump(StringBuilder sb) { 466 sb.append("\nProfile: AvrcpTargetService:\n"); 467 if (sInstance == null) { 468 sb.append("AvrcpTargetService not running"); 469 return; 470 } 471 472 StringBuilder tempBuilder = new StringBuilder(); 473 tempBuilder.append("AVRCP version: " + mAvrcpVersion + "\n"); 474 475 if (mMediaPlayerList != null) { 476 mMediaPlayerList.dump(tempBuilder); 477 } else { 478 tempBuilder.append("\nMedia Player List is empty\n"); 479 } 480 481 mMediaKeyEventLogger.dump(tempBuilder); 482 tempBuilder.append("\n"); 483 mVolumeManager.dump(tempBuilder); 484 if (mAvrcpCoverArtService != null) { 485 tempBuilder.append("\n"); 486 mAvrcpCoverArtService.dump(tempBuilder); 487 } 488 489 // Tab everything over by two spaces 490 sb.append(tempBuilder.toString().replaceAll("(?m)^", " ")); 491 } 492 493 private static class AvrcpTargetBinder extends IBluetoothAvrcpTarget.Stub 494 implements IProfileServiceBinder { 495 private AvrcpTargetService mService; 496 AvrcpTargetBinder(AvrcpTargetService service)497 AvrcpTargetBinder(AvrcpTargetService service) { 498 mService = service; 499 } 500 501 @Override cleanup()502 public void cleanup() { 503 mService = null; 504 } 505 506 @Override sendVolumeChanged(int volume)507 public void sendVolumeChanged(int volume) { 508 if (!Utils.callerIsSystemOrActiveUser(TAG, "sendVolumeChanged")) { 509 return; 510 } 511 512 if (mService == null) { 513 return; 514 } 515 516 mService.sendVolumeChanged(volume); 517 } 518 } 519 } 520