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.annotation.Nullable; 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.content.Context; 24 import android.content.SharedPreferences; 25 import android.media.AudioDeviceAttributes; 26 import android.media.AudioDeviceCallback; 27 import android.media.AudioDeviceInfo; 28 import android.media.AudioManager; 29 import android.util.Log; 30 31 import com.android.bluetooth.BluetoothEventLogger; 32 import com.android.bluetooth.Utils; 33 import com.android.internal.annotations.VisibleForTesting; 34 35 import java.util.HashMap; 36 import java.util.Map; 37 import java.util.Objects; 38 import java.util.concurrent.CompletableFuture; 39 40 /** 41 * Handles volume changes from or to the remote device or system. 42 * 43 * <p>{@link AudioManager#setDeviceVolumeBehavior} is used to inform Media Framework of the current 44 * active {@link BluetoothDevice} and its absolute volume support. 45 * 46 * <p>When absolute volume is supported, the volume should be synced between Media framework and the 47 * remote device. Otherwise, only system volume is used. 48 * 49 * <p>AVRCP volume ranges from 0 to 127 which might not correspond to the system volume. As such, 50 * volume sent to either Media Framework or remote device is converted accordingly. 51 * 52 * <p>Volume changes are stored as system volume in {@link SharedPreferences} and retrieved at 53 * device connection. 54 */ 55 class AvrcpVolumeManager extends AudioDeviceCallback { 56 public static final String TAG = AvrcpVolumeManager.class.getSimpleName(); 57 58 // All volumes are stored at system volume values, not AVRCP values 59 private static final String VOLUME_MAP = "bluetooth_volume_map"; 60 private static final String VOLUME_CHANGE_LOG_TITLE = "BTAudio Volume Events"; 61 62 @VisibleForTesting static final int AVRCP_MAX_VOL = 127; 63 private static final int STREAM_MUSIC = AudioManager.STREAM_MUSIC; 64 private static final int VOLUME_CHANGE_LOGGER_SIZE = 30; 65 private static int sDeviceMaxVolume = 0; 66 private static int sNewDeviceVolume = 0; 67 private final BluetoothEventLogger mVolumeEventLogger = 68 new BluetoothEventLogger(VOLUME_CHANGE_LOGGER_SIZE, VOLUME_CHANGE_LOG_TITLE); 69 70 Context mContext; 71 AudioManager mAudioManager; 72 AvrcpNativeInterface mNativeInterface; 73 74 // Absolute volume support map. 75 HashMap<BluetoothDevice, Boolean> mDeviceMap = new HashMap(); 76 77 // Volume stored is system volume (0 - {@code sDeviceMaxVolume}). 78 HashMap<BluetoothDevice, Integer> mVolumeMap = new HashMap(); 79 80 BluetoothDevice mCurrentDevice = null; 81 boolean mAbsoluteVolumeSupported = false; 82 83 /** 84 * Converts given {@code avrcpVolume} (0 - 127) to equivalent in system volume (0 - {@code 85 * sDeviceMaxVolume}). 86 * 87 * <p>Max system volume is retrieved from {@link AudioManager}. 88 */ avrcpToSystemVolume(int avrcpVolume)89 static int avrcpToSystemVolume(int avrcpVolume) { 90 return (int) Math.round((double) avrcpVolume * sDeviceMaxVolume / AVRCP_MAX_VOL); 91 } 92 93 /** 94 * Converts given {@code deviceVolume} (0 - {@code sDeviceMaxVolume}) to equivalent in AVRCP 95 * volume (0 - 127). 96 * 97 * <p>Max system volume is retrieved from {@link AudioManager}. 98 */ systemToAvrcpVolume(int deviceVolume)99 static int systemToAvrcpVolume(int deviceVolume) { 100 int avrcpVolume = 101 (int) Math.round((double) deviceVolume * AVRCP_MAX_VOL / sDeviceMaxVolume); 102 if (avrcpVolume > 127) avrcpVolume = 127; 103 return avrcpVolume; 104 } 105 106 /** 107 * Retrieves the {@link SharedPreferences} of the map device / volume. 108 * 109 * <p>The map is read to retrieve the last volume set for a bonded {@link BluetoothDevice}. 110 * 111 * <p>The map is written each time a volume update occurs from or to the remote device. 112 */ getVolumeMap()113 private SharedPreferences getVolumeMap() { 114 return mContext.getSharedPreferences(VOLUME_MAP, Context.MODE_PRIVATE); 115 } 116 117 /** 118 * Informs {@link AudioManager} that a new {@link BluetoothDevice} has been connected and is the 119 * new desired audio output. 120 * 121 * <p>If AVRCP absolute volume is supported, this will also send the saved or new volume to the 122 * remote device. 123 * 124 * <p>Absolute volume support is conditional to its presence in the {@code mDeviceMap}. 125 */ switchVolumeDevice(@onNull BluetoothDevice device)126 private void switchVolumeDevice(@NonNull BluetoothDevice device) { 127 // Inform the audio manager that the device has changed 128 d("switchVolumeDevice: Set Absolute volume support to " + mDeviceMap.get(device)); 129 final AudioDeviceAttributes deviceAttributes = 130 new AudioDeviceAttributes( 131 AudioDeviceAttributes.ROLE_OUTPUT, 132 AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, 133 device.getAddress()); 134 final int deviceVolumeBehavior = 135 mDeviceMap.get(device) 136 ? AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE 137 : AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE; 138 139 CompletableFuture.runAsync( 140 () -> 141 mAudioManager.setDeviceVolumeBehavior( 142 deviceAttributes, deviceVolumeBehavior), 143 Utils.BackgroundExecutor) 144 .exceptionally( 145 e -> { 146 Log.e(TAG, "switchVolumeDevice has thrown an Exception", e); 147 return null; 148 }); 149 150 // Get the current system volume and try to get the preference volume 151 int savedVolume = getVolume(device, sNewDeviceVolume); 152 153 d("switchVolumeDevice: savedVolume=" + savedVolume); 154 155 // If absolute volume for the device is supported, set the volume for the device 156 if (mDeviceMap.get(device)) { 157 int avrcpVolume = systemToAvrcpVolume(savedVolume); 158 mVolumeEventLogger.logd( 159 TAG, "switchVolumeDevice: Updating device volume: avrcpVolume=" + avrcpVolume); 160 mNativeInterface.sendVolumeChanged(device, avrcpVolume); 161 } 162 } 163 164 /** 165 * Instantiates all class variables. 166 * 167 * <p>Fills {@code mVolumeMap} with content from {@link #getVolumeMap}, removing unbonded 168 * devices if necessary. 169 */ AvrcpVolumeManager( Context context, AudioManager audioManager, AvrcpNativeInterface nativeInterface)170 AvrcpVolumeManager( 171 Context context, AudioManager audioManager, AvrcpNativeInterface nativeInterface) { 172 mContext = context; 173 mAudioManager = audioManager; 174 mNativeInterface = nativeInterface; 175 sDeviceMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 176 sNewDeviceVolume = sDeviceMaxVolume / 2; 177 178 mAudioManager.registerAudioDeviceCallback(this, null); 179 180 // Load the stored volume preferences into a hash map since shared preferences are slow 181 // to poll and update. If the device has been unbonded since last start remove it from 182 // the map. 183 Map<String, ?> allKeys = getVolumeMap().getAll(); 184 SharedPreferences.Editor volumeMapEditor = getVolumeMap().edit(); 185 for (Map.Entry<String, ?> entry : allKeys.entrySet()) { 186 String key = entry.getKey(); 187 Object value = entry.getValue(); 188 BluetoothDevice d = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(key); 189 190 if (value instanceof Integer && d.getBondState() == BluetoothDevice.BOND_BONDED) { 191 mVolumeMap.put(d, (Integer) value); 192 } else { 193 d("Removing " + key + " from the volume map"); 194 volumeMapEditor.remove(key); 195 } 196 } 197 volumeMapEditor.apply(); 198 } 199 200 /** 201 * Stores system volume (0 - {@code sDeviceMaxVolume}) for device in {@code mVolumeMap} and 202 * writes the map in the {@link SharedPreferences}. 203 */ storeVolumeForDevice(@onNull BluetoothDevice device, int storeVolume)204 synchronized void storeVolumeForDevice(@NonNull BluetoothDevice device, int storeVolume) { 205 if (device.getBondState() != BluetoothDevice.BOND_BONDED) { 206 return; 207 } 208 SharedPreferences.Editor pref = getVolumeMap().edit(); 209 mVolumeEventLogger.logd( 210 TAG, 211 "storeVolume: Storing stream volume level for device " 212 + device 213 + " : " 214 + storeVolume); 215 mVolumeMap.put(device, storeVolume); 216 pref.putInt(device.getAddress(), storeVolume); 217 // Always use apply() since it is asynchronous, otherwise the call can hang waiting for 218 // storage to be written. 219 pref.apply(); 220 } 221 222 /** 223 * Retrieves system volume (0 - {@code sDeviceMaxVolume}) and calls {@link 224 * #storeVolumeForDevice(BluetoothDevice, int)} with {@code device}. 225 */ storeVolumeForDevice(@onNull BluetoothDevice device)226 synchronized void storeVolumeForDevice(@NonNull BluetoothDevice device) { 227 int storeVolume = mAudioManager.getLastAudibleStreamVolume(STREAM_MUSIC); 228 storeVolumeForDevice(device, storeVolume); 229 } 230 231 /** 232 * Removes the stored volume of a device from {@code mVolumeMap} and writes the map in the 233 * {@link SharedPreferences}. 234 */ removeStoredVolumeForDevice(@onNull BluetoothDevice device)235 synchronized void removeStoredVolumeForDevice(@NonNull BluetoothDevice device) { 236 if (device.getBondState() != BluetoothDevice.BOND_NONE) { 237 return; 238 } 239 SharedPreferences.Editor pref = getVolumeMap().edit(); 240 mVolumeEventLogger.logd( 241 TAG, "RemoveStoredVolume: Remove stored stream volume level for device " + device); 242 mVolumeMap.remove(device); 243 pref.remove(device.getAddress()); 244 // Always use apply() since it is asynchronous, otherwise the call can hang waiting for 245 // storage to be written. 246 pref.apply(); 247 } 248 249 /** 250 * Returns system volume (0 - {@code sDeviceMaxVolume}) stored in {@code mVolumeMap} for 251 * corresponding {@code device}. 252 * 253 * @param defaultValue Value to return if device is not in the map. 254 */ getVolume(@onNull BluetoothDevice device, int defaultValue)255 synchronized int getVolume(@NonNull BluetoothDevice device, int defaultValue) { 256 if (!mVolumeMap.containsKey(device)) { 257 Log.w(TAG, "getVolume: Couldn't find volume preference for device: " + device); 258 return defaultValue; 259 } 260 261 d("getVolume: Returning volume " + mVolumeMap.get(device)); 262 return mVolumeMap.get(device); 263 } 264 265 /** Returns the system volume (0 - {@code sDeviceMaxVolume}) applied to a new device */ getNewDeviceVolume()266 public int getNewDeviceVolume() { 267 return sNewDeviceVolume; 268 } 269 270 /** 271 * Informs {@link AudioManager} of a remote device volume change and stores it. 272 * 273 * <p>See {@link #avrcpToSystemVolume}. 274 * 275 * @param avrcpVolume in range (0 - 127) received from remote device. 276 */ setVolume(@onNull BluetoothDevice device, int avrcpVolume)277 void setVolume(@NonNull BluetoothDevice device, int avrcpVolume) { 278 int deviceVolume = avrcpToSystemVolume(avrcpVolume); 279 mVolumeEventLogger.logd( 280 TAG, 281 "setVolume:" 282 + " device=" 283 + device 284 + " avrcpVolume=" 285 + avrcpVolume 286 + " deviceVolume=" 287 + deviceVolume 288 + " sDeviceMaxVolume=" 289 + sDeviceMaxVolume); 290 mAudioManager.setStreamVolume( 291 AudioManager.STREAM_MUSIC, 292 deviceVolume, 293 (deviceVolume != getVolume(device, -1) ? AudioManager.FLAG_SHOW_UI : 0) 294 | AudioManager.FLAG_BLUETOOTH_ABS_VOLUME); 295 storeVolumeForDevice(device); 296 } 297 298 /** 299 * Informs remote device of a system volume change and stores it. 300 * 301 * <p>See {@link #systemToAvrcpVolume}. 302 * 303 * @param deviceVolume in range (0 - {@code sDeviceMaxVolume}) received from system. 304 */ sendVolumeChanged(@onNull BluetoothDevice device, int deviceVolume)305 void sendVolumeChanged(@NonNull BluetoothDevice device, int deviceVolume) { 306 if (deviceVolume == getVolume(device, -1)) { 307 d("sendVolumeChanged: Skipping update volume to same as current."); 308 return; 309 } 310 int avrcpVolume = systemToAvrcpVolume(deviceVolume); 311 mVolumeEventLogger.logd( 312 TAG, 313 "sendVolumeChanged:" 314 + " device=" 315 + device 316 + " avrcpVolume=" 317 + avrcpVolume 318 + " deviceVolume=" 319 + deviceVolume 320 + " sDeviceMaxVolume=" 321 + sDeviceMaxVolume); 322 mNativeInterface.sendVolumeChanged(device, avrcpVolume); 323 storeVolumeForDevice(device); 324 } 325 326 /** Returns whether absolute volume is supported by {@code device}. */ getAbsoluteVolumeSupported(BluetoothDevice device)327 boolean getAbsoluteVolumeSupported(BluetoothDevice device) { 328 if (mDeviceMap.containsKey(device)) { 329 return mDeviceMap.get(device); 330 } 331 return false; 332 } 333 334 /** 335 * Callback from Media Framework to indicate new audio device was added. 336 * 337 * <p>Checks if the current active device is in the {@code addedDevices} list in order to inform 338 * {@link AudioManager} to take it as selected audio output. See {@link #switchVolumeDevice}. 339 * 340 * <p>If the remote device absolute volume support hasn't been established yet or if the current 341 * active device is not in the {@code addedDevices} list, this doesn't inform {@link 342 * AudioManager}. See {@link #deviceConnected} and {@link #volumeDeviceSwitched}. 343 */ 344 @Override onAudioDevicesAdded(AudioDeviceInfo[] addedDevices)345 public synchronized void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { 346 if (mCurrentDevice == null) { 347 d("onAudioDevicesAdded: Not expecting device changed"); 348 return; 349 } 350 351 boolean foundDevice = false; 352 d("onAudioDevicesAdded: size: " + addedDevices.length); 353 for (int i = 0; i < addedDevices.length; i++) { 354 d("onAudioDevicesAdded: address=" + addedDevices[i].getAddress()); 355 if (addedDevices[i].getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP 356 && Objects.equals(addedDevices[i].getAddress(), mCurrentDevice.getAddress())) { 357 foundDevice = true; 358 break; 359 } 360 } 361 362 if (!foundDevice) { 363 d("Didn't find deferred device in list: device=" + mCurrentDevice); 364 return; 365 } 366 367 // A2DP can sometimes connect and set a device to active before AVRCP has determined if the 368 // device supports absolute volume. Defer switching the device until AVRCP returns the 369 // info. 370 if (!mDeviceMap.containsKey(mCurrentDevice)) { 371 Log.w(TAG, "volumeDeviceSwitched: Device isn't connected: " + mCurrentDevice); 372 return; 373 } 374 375 switchVolumeDevice(mCurrentDevice); 376 } 377 378 /** 379 * Stores absolute volume support for {@code device}. If the current active device is the same 380 * as {@code device}, calls {@link #switchVolumeDevice}. 381 */ deviceConnected(@onNull BluetoothDevice device, boolean absoluteVolume)382 synchronized void deviceConnected(@NonNull BluetoothDevice device, boolean absoluteVolume) { 383 d("deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume); 384 385 mDeviceMap.put(device, absoluteVolume); 386 387 // AVRCP features lookup has completed after the device became active. Switch to the new 388 // device now. 389 if (device.equals(mCurrentDevice)) { 390 switchVolumeDevice(device); 391 } 392 } 393 394 /** 395 * Called when the A2DP active device changed, this will call {@link #switchVolumeDevice} if we 396 * already know the absolute volume support of {@code device}. 397 */ volumeDeviceSwitched(@ullable BluetoothDevice device)398 synchronized void volumeDeviceSwitched(@Nullable BluetoothDevice device) { 399 d("volumeDeviceSwitched: mCurrentDevice=" + mCurrentDevice + " device=" + device); 400 401 if (Objects.equals(device, mCurrentDevice)) { 402 return; 403 } 404 405 mCurrentDevice = device; 406 if (!mDeviceMap.containsKey(device)) { 407 // Wait until AudioManager informs us that the new device is connected 408 return; 409 } 410 switchVolumeDevice(device); 411 } 412 deviceDisconnected(@onNull BluetoothDevice device)413 synchronized void deviceDisconnected(@NonNull BluetoothDevice device) { 414 d("deviceDisconnected: device=" + device); 415 mDeviceMap.remove(device); 416 } 417 dump(StringBuilder sb)418 public void dump(StringBuilder sb) { 419 sb.append("AvrcpVolumeManager:\n"); 420 sb.append(" mCurrentDevice: " + mCurrentDevice + "\n"); 421 sb.append(" Current System Volume: " + mAudioManager.getStreamVolume(STREAM_MUSIC) + "\n"); 422 sb.append(" Device Volume Memory Map:\n"); 423 sb.append( 424 String.format( 425 " %-17s : %-14s : %3s : %s\n", 426 "Device Address", "Device Name", "Vol", "AbsVol")); 427 Map<String, ?> allKeys = getVolumeMap().getAll(); 428 for (Map.Entry<String, ?> entry : allKeys.entrySet()) { 429 Object value = entry.getValue(); 430 BluetoothDevice d = 431 BluetoothAdapter.getDefaultAdapter().getRemoteDevice(entry.getKey()); 432 433 String deviceName = d.getName(); 434 if (deviceName == null) { 435 deviceName = ""; 436 } else if (deviceName.length() > 14) { 437 deviceName = deviceName.substring(0, 11).concat("..."); 438 } 439 440 String absoluteVolume = "NotConnected"; 441 if (mDeviceMap.containsKey(d)) { 442 absoluteVolume = mDeviceMap.get(d).toString(); 443 } 444 445 if (value instanceof Integer) { 446 sb.append( 447 String.format( 448 " %-17s : %-14s : %3d : %s\n", 449 d.getAddress(), deviceName, (Integer) value, absoluteVolume)); 450 } 451 } 452 453 StringBuilder tempBuilder = new StringBuilder(); 454 mVolumeEventLogger.dump(tempBuilder); 455 // Tab volume event logs over by two spaces 456 sb.append(tempBuilder.toString().replaceAll("(?m)^", " ")); 457 tempBuilder.append("\n"); 458 } 459 d(String msg)460 static void d(String msg) { 461 Log.d(TAG, msg); 462 } 463 } 464