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