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