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