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