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