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