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.audio_util.BTAudioEventLogger; 32 import com.android.internal.annotations.VisibleForTesting; 33 34 import java.util.HashMap; 35 import java.util.Map; 36 import java.util.Objects; 37 38 class AvrcpVolumeManager extends AudioDeviceCallback { 39 public static final String TAG = "AvrcpVolumeManager"; 40 public static final boolean DEBUG = true; 41 42 // All volumes are stored at system volume values, not AVRCP values 43 private static final String VOLUME_MAP = "bluetooth_volume_map"; 44 private static final String VOLUME_REJECTLIST = "absolute_volume_rejectlist"; 45 private static final String VOLUME_CHANGE_LOG_TITLE = "Volume Events"; 46 47 @VisibleForTesting 48 static final int AVRCP_MAX_VOL = 127; 49 private static final int STREAM_MUSIC = AudioManager.STREAM_MUSIC; 50 private static final int VOLUME_CHANGE_LOGGER_SIZE = 30; 51 private static int sDeviceMaxVolume = 0; 52 private static int sNewDeviceVolume = 0; 53 private final BTAudioEventLogger mVolumeEventLogger = new BTAudioEventLogger( 54 VOLUME_CHANGE_LOGGER_SIZE, VOLUME_CHANGE_LOG_TITLE); 55 56 Context mContext; 57 AudioManager mAudioManager; 58 AvrcpNativeInterface mNativeInterface; 59 60 HashMap<BluetoothDevice, Boolean> mDeviceMap = new HashMap(); 61 HashMap<BluetoothDevice, Integer> mVolumeMap = new HashMap(); 62 BluetoothDevice mCurrentDevice = null; 63 boolean mAbsoluteVolumeSupported = false; 64 avrcpToSystemVolume(int avrcpVolume)65 static int avrcpToSystemVolume(int avrcpVolume) { 66 return (int) Math.floor((double) avrcpVolume * sDeviceMaxVolume / AVRCP_MAX_VOL); 67 } 68 systemToAvrcpVolume(int deviceVolume)69 static int systemToAvrcpVolume(int deviceVolume) { 70 int avrcpVolume = (int) Math.ceil((double) deviceVolume 71 * AVRCP_MAX_VOL / sDeviceMaxVolume); 72 if (avrcpVolume > 127) avrcpVolume = 127; 73 return avrcpVolume; 74 } 75 getVolumeMap()76 private SharedPreferences getVolumeMap() { 77 return mContext.getSharedPreferences(VOLUME_MAP, Context.MODE_PRIVATE); 78 } 79 switchVolumeDevice(@onNull BluetoothDevice device)80 private void switchVolumeDevice(@NonNull BluetoothDevice device) { 81 // Inform the audio manager that the device has changed 82 d("switchVolumeDevice: Set Absolute volume support to " + mDeviceMap.get(device)); 83 mAudioManager.setDeviceVolumeBehavior(new AudioDeviceAttributes( 84 AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, 85 device.getAddress()), 86 mDeviceMap.get(device) ? AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE 87 : AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE); 88 89 // Get the current system volume and try to get the preference volume 90 int savedVolume = getVolume(device, sNewDeviceVolume); 91 92 d("switchVolumeDevice: savedVolume=" + savedVolume); 93 94 // If absolute volume for the device is supported, set the volume for the device 95 if (mDeviceMap.get(device)) { 96 int avrcpVolume = systemToAvrcpVolume(savedVolume); 97 mVolumeEventLogger.logd(TAG, 98 "switchVolumeDevice: Updating device volume: avrcpVolume=" + avrcpVolume); 99 mNativeInterface.sendVolumeChanged(device.getAddress(), avrcpVolume); 100 } 101 } 102 AvrcpVolumeManager(Context context, AudioManager audioManager, AvrcpNativeInterface nativeInterface)103 AvrcpVolumeManager(Context context, AudioManager audioManager, 104 AvrcpNativeInterface nativeInterface) { 105 mContext = context; 106 mAudioManager = audioManager; 107 mNativeInterface = nativeInterface; 108 sDeviceMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 109 sNewDeviceVolume = sDeviceMaxVolume / 2; 110 111 mAudioManager.registerAudioDeviceCallback(this, null); 112 113 // Load the stored volume preferences into a hash map since shared preferences are slow 114 // to poll and update. If the device has been unbonded since last start remove it from 115 // the map. 116 Map<String, ?> allKeys = getVolumeMap().getAll(); 117 SharedPreferences.Editor volumeMapEditor = getVolumeMap().edit(); 118 for (Map.Entry<String, ?> entry : allKeys.entrySet()) { 119 String key = entry.getKey(); 120 Object value = entry.getValue(); 121 BluetoothDevice d = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(key); 122 123 if (value instanceof Integer && d.getBondState() == BluetoothDevice.BOND_BONDED) { 124 mVolumeMap.put(d, (Integer) value); 125 } else { 126 d("Removing " + key + " from the volume map"); 127 volumeMapEditor.remove(key); 128 } 129 } 130 volumeMapEditor.apply(); 131 } 132 storeVolumeForDevice(@onNull BluetoothDevice device, int storeVolume)133 synchronized void storeVolumeForDevice(@NonNull BluetoothDevice device, int storeVolume) { 134 if (device.getBondState() != BluetoothDevice.BOND_BONDED) { 135 return; 136 } 137 SharedPreferences.Editor pref = getVolumeMap().edit(); 138 mVolumeEventLogger.logd(TAG, "storeVolume: Storing stream volume level for device " 139 + device + " : " + storeVolume); 140 mVolumeMap.put(device, storeVolume); 141 pref.putInt(device.getAddress(), storeVolume); 142 // Always use apply() since it is asynchronous, otherwise the call can hang waiting for 143 // storage to be written. 144 pref.apply(); 145 } 146 storeVolumeForDevice(@onNull BluetoothDevice device)147 synchronized void storeVolumeForDevice(@NonNull BluetoothDevice device) { 148 int storeVolume = mAudioManager.getLastAudibleStreamVolume(STREAM_MUSIC); 149 storeVolumeForDevice(device, storeVolume); 150 } 151 removeStoredVolumeForDevice(@onNull BluetoothDevice device)152 synchronized void removeStoredVolumeForDevice(@NonNull BluetoothDevice device) { 153 if (device.getBondState() != BluetoothDevice.BOND_NONE) { 154 return; 155 } 156 SharedPreferences.Editor pref = getVolumeMap().edit(); 157 mVolumeEventLogger.logd(TAG, 158 "RemoveStoredVolume: Remove stored stream volume level for device " + device); 159 mVolumeMap.remove(device); 160 pref.remove(device.getAddress()); 161 // Always use apply() since it is asynchronous, otherwise the call can hang waiting for 162 // storage to be written. 163 pref.apply(); 164 } 165 getVolume(@onNull BluetoothDevice device, int defaultValue)166 synchronized int getVolume(@NonNull BluetoothDevice device, int defaultValue) { 167 if (!mVolumeMap.containsKey(device)) { 168 Log.w(TAG, "getVolume: Couldn't find volume preference for device: " + device); 169 return defaultValue; 170 } 171 172 d("getVolume: Returning volume " + mVolumeMap.get(device)); 173 return mVolumeMap.get(device); 174 } 175 getNewDeviceVolume()176 public int getNewDeviceVolume() { 177 return sNewDeviceVolume; 178 } 179 setVolume(@onNull BluetoothDevice device, int avrcpVolume)180 void setVolume(@NonNull BluetoothDevice device, int avrcpVolume) { 181 int deviceVolume = 182 (int) Math.round((double) avrcpVolume * sDeviceMaxVolume / AVRCP_MAX_VOL); 183 mVolumeEventLogger.logd(DEBUG, TAG, "setVolume:" 184 + " device=" + device 185 + " avrcpVolume=" + avrcpVolume 186 + " deviceVolume=" + deviceVolume 187 + " sDeviceMaxVolume=" + sDeviceMaxVolume); 188 mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, deviceVolume, 189 (deviceVolume != getVolume(device, -1) ? AudioManager.FLAG_SHOW_UI : 0) 190 | AudioManager.FLAG_BLUETOOTH_ABS_VOLUME); 191 storeVolumeForDevice(device); 192 } 193 sendVolumeChanged(@onNull BluetoothDevice device, int deviceVolume)194 void sendVolumeChanged(@NonNull BluetoothDevice device, int deviceVolume) { 195 if (deviceVolume == getVolume(device, -1)) { 196 d("sendVolumeChanged: Skipping update volume to same as current."); 197 return; 198 } 199 int avrcpVolume = 200 (int) Math.round((double) deviceVolume * AVRCP_MAX_VOL / sDeviceMaxVolume); 201 if (avrcpVolume > 127) avrcpVolume = 127; 202 mVolumeEventLogger.logd(DEBUG, TAG, "sendVolumeChanged:" 203 + " device=" + device 204 + " avrcpVolume=" + avrcpVolume 205 + " deviceVolume=" + deviceVolume 206 + " sDeviceMaxVolume=" + sDeviceMaxVolume); 207 mNativeInterface.sendVolumeChanged(device.getAddress(), avrcpVolume); 208 storeVolumeForDevice(device); 209 } 210 211 /** 212 * True if remote device supported Absolute volume, false if remote device is not supported or 213 * not connected. 214 */ getAbsoluteVolumeSupported(BluetoothDevice device)215 boolean getAbsoluteVolumeSupported(BluetoothDevice device) { 216 if (mDeviceMap.containsKey(device)) { 217 return mDeviceMap.get(device); 218 } 219 return false; 220 } 221 222 @Override onAudioDevicesAdded(AudioDeviceInfo[] addedDevices)223 public synchronized void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { 224 if (mCurrentDevice == null) { 225 d("onAudioDevicesAdded: Not expecting device changed"); 226 return; 227 } 228 229 boolean foundDevice = false; 230 d("onAudioDevicesAdded: size: " + addedDevices.length); 231 for (int i = 0; i < addedDevices.length; i++) { 232 d("onAudioDevicesAdded: address=" + addedDevices[i].getAddress()); 233 if (addedDevices[i].getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP 234 && Objects.equals(addedDevices[i].getAddress(), mCurrentDevice.getAddress())) { 235 foundDevice = true; 236 break; 237 } 238 } 239 240 if (!foundDevice) { 241 d("Didn't find deferred device in list: device=" + mCurrentDevice); 242 return; 243 } 244 245 // A2DP can sometimes connect and set a device to active before AVRCP has determined if the 246 // device supports absolute volume. Defer switching the device until AVRCP returns the 247 // info. 248 if (!mDeviceMap.containsKey(mCurrentDevice)) { 249 Log.w(TAG, "volumeDeviceSwitched: Device isn't connected: " + mCurrentDevice); 250 return; 251 } 252 253 switchVolumeDevice(mCurrentDevice); 254 } 255 deviceConnected(@onNull BluetoothDevice device, boolean absoluteVolume)256 synchronized void deviceConnected(@NonNull BluetoothDevice device, boolean absoluteVolume) { 257 d("deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume); 258 259 mDeviceMap.put(device, absoluteVolume); 260 261 // AVRCP features lookup has completed after the device became active. Switch to the new 262 // device now. 263 if (device.equals(mCurrentDevice)) { 264 switchVolumeDevice(device); 265 } 266 } 267 volumeDeviceSwitched(@ullable BluetoothDevice device)268 synchronized void volumeDeviceSwitched(@Nullable BluetoothDevice device) { 269 d("volumeDeviceSwitched: mCurrentDevice=" + mCurrentDevice + " device=" + device); 270 271 if (Objects.equals(device, mCurrentDevice)) { 272 return; 273 } 274 275 // Wait until AudioManager informs us that the new device is connected 276 mCurrentDevice = device; 277 } 278 deviceDisconnected(@onNull BluetoothDevice device)279 synchronized void deviceDisconnected(@NonNull BluetoothDevice device) { 280 d("deviceDisconnected: device=" + device); 281 mDeviceMap.remove(device); 282 } 283 dump(StringBuilder sb)284 public void dump(StringBuilder sb) { 285 sb.append("AvrcpVolumeManager:\n"); 286 sb.append(" mCurrentDevice: " + mCurrentDevice + "\n"); 287 sb.append(" Current System Volume: " + mAudioManager.getStreamVolume(STREAM_MUSIC) + "\n"); 288 sb.append(" Device Volume Memory Map:\n"); 289 sb.append(String.format(" %-17s : %-14s : %3s : %s\n", 290 "Device Address", "Device Name", "Vol", "AbsVol")); 291 Map<String, ?> allKeys = getVolumeMap().getAll(); 292 for (Map.Entry<String, ?> entry : allKeys.entrySet()) { 293 Object value = entry.getValue(); 294 BluetoothDevice d = BluetoothAdapter.getDefaultAdapter() 295 .getRemoteDevice(entry.getKey()); 296 297 String deviceName = d.getName(); 298 if (deviceName == null) { 299 deviceName = ""; 300 } else if (deviceName.length() > 14) { 301 deviceName = deviceName.substring(0, 11).concat("..."); 302 } 303 304 String absoluteVolume = "NotConnected"; 305 if (mDeviceMap.containsKey(d)) { 306 absoluteVolume = mDeviceMap.get(d).toString(); 307 } 308 309 if (value instanceof Integer) { 310 sb.append(String.format(" %-17s : %-14s : %3d : %s\n", 311 d.getAddress(), deviceName, (Integer) value, absoluteVolume)); 312 } 313 } 314 315 StringBuilder tempBuilder = new StringBuilder(); 316 mVolumeEventLogger.dump(tempBuilder); 317 // Tab volume event logs over by two spaces 318 sb.append(tempBuilder.toString().replaceAll("(?m)^", " ")); 319 tempBuilder.append("\n"); 320 } 321 d(String msg)322 static void d(String msg) { 323 if (DEBUG) { 324 Log.d(TAG, msg); 325 } 326 } 327 } 328