• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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