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