• 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.AudioDeviceAttributes;
26 import android.media.AudioDeviceCallback;
27 import android.media.AudioDeviceInfo;
28 import android.media.AudioManager;
29 import android.util.Log;
30 
31 import com.android.bluetooth.BluetoothEventLogger;
32 import com.android.bluetooth.Utils;
33 import com.android.internal.annotations.VisibleForTesting;
34 
35 import java.util.HashMap;
36 import java.util.Map;
37 import java.util.Objects;
38 import java.util.concurrent.CompletableFuture;
39 
40 /**
41  * Handles volume changes from or to the remote device or system.
42  *
43  * <p>{@link AudioManager#setDeviceVolumeBehavior} is used to inform Media Framework of the current
44  * active {@link BluetoothDevice} and its absolute volume support.
45  *
46  * <p>When absolute volume is supported, the volume should be synced between Media framework and the
47  * remote device. Otherwise, only system volume is used.
48  *
49  * <p>AVRCP volume ranges from 0 to 127 which might not correspond to the system volume. As such,
50  * volume sent to either Media Framework or remote device is converted accordingly.
51  *
52  * <p>Volume changes are stored as system volume in {@link SharedPreferences} and retrieved at
53  * device connection.
54  */
55 class AvrcpVolumeManager extends AudioDeviceCallback {
56     public static final String TAG = AvrcpVolumeManager.class.getSimpleName();
57 
58     // All volumes are stored at system volume values, not AVRCP values
59     private static final String VOLUME_MAP = "bluetooth_volume_map";
60     private static final String VOLUME_CHANGE_LOG_TITLE = "BTAudio Volume Events";
61 
62     @VisibleForTesting static final int AVRCP_MAX_VOL = 127;
63     private static final int STREAM_MUSIC = AudioManager.STREAM_MUSIC;
64     private static final int VOLUME_CHANGE_LOGGER_SIZE = 30;
65     private static int sDeviceMaxVolume = 0;
66     private static int sNewDeviceVolume = 0;
67     private final BluetoothEventLogger mVolumeEventLogger =
68             new BluetoothEventLogger(VOLUME_CHANGE_LOGGER_SIZE, VOLUME_CHANGE_LOG_TITLE);
69 
70     Context mContext;
71     AudioManager mAudioManager;
72     AvrcpNativeInterface mNativeInterface;
73 
74     // Absolute volume support map.
75     HashMap<BluetoothDevice, Boolean> mDeviceMap = new HashMap();
76 
77     // Volume stored is system volume (0 - {@code sDeviceMaxVolume}).
78     HashMap<BluetoothDevice, Integer> mVolumeMap = new HashMap();
79 
80     BluetoothDevice mCurrentDevice = null;
81     boolean mAbsoluteVolumeSupported = false;
82 
83     /**
84      * Converts given {@code avrcpVolume} (0 - 127) to equivalent in system volume (0 - {@code
85      * sDeviceMaxVolume}).
86      *
87      * <p>Max system volume is retrieved from {@link AudioManager}.
88      */
avrcpToSystemVolume(int avrcpVolume)89     static int avrcpToSystemVolume(int avrcpVolume) {
90         return (int) Math.round((double) avrcpVolume * sDeviceMaxVolume / AVRCP_MAX_VOL);
91     }
92 
93     /**
94      * Converts given {@code deviceVolume} (0 - {@code sDeviceMaxVolume}) to equivalent in AVRCP
95      * volume (0 - 127).
96      *
97      * <p>Max system volume is retrieved from {@link AudioManager}.
98      */
systemToAvrcpVolume(int deviceVolume)99     static int systemToAvrcpVolume(int deviceVolume) {
100         int avrcpVolume =
101                 (int) Math.round((double) deviceVolume * AVRCP_MAX_VOL / sDeviceMaxVolume);
102         if (avrcpVolume > 127) avrcpVolume = 127;
103         return avrcpVolume;
104     }
105 
106     /**
107      * Retrieves the {@link SharedPreferences} of the map device / volume.
108      *
109      * <p>The map is read to retrieve the last volume set for a bonded {@link BluetoothDevice}.
110      *
111      * <p>The map is written each time a volume update occurs from or to the remote device.
112      */
getVolumeMap()113     private SharedPreferences getVolumeMap() {
114         return mContext.getSharedPreferences(VOLUME_MAP, Context.MODE_PRIVATE);
115     }
116 
117     /**
118      * Informs {@link AudioManager} that a new {@link BluetoothDevice} has been connected and is the
119      * new desired audio output.
120      *
121      * <p>If AVRCP absolute volume is supported, this will also send the saved or new volume to the
122      * remote device.
123      *
124      * <p>Absolute volume support is conditional to its presence in the {@code mDeviceMap}.
125      */
switchVolumeDevice(@onNull BluetoothDevice device)126     private void switchVolumeDevice(@NonNull BluetoothDevice device) {
127         // Inform the audio manager that the device has changed
128         d("switchVolumeDevice: Set Absolute volume support to " + mDeviceMap.get(device));
129         final AudioDeviceAttributes deviceAttributes =
130                 new AudioDeviceAttributes(
131                         AudioDeviceAttributes.ROLE_OUTPUT,
132                         AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
133                         device.getAddress());
134         final int deviceVolumeBehavior =
135                 mDeviceMap.get(device)
136                         ? AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE
137                         : AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE;
138 
139         CompletableFuture.runAsync(
140                         () ->
141                                 mAudioManager.setDeviceVolumeBehavior(
142                                         deviceAttributes, deviceVolumeBehavior),
143                         Utils.BackgroundExecutor)
144                 .exceptionally(
145                         e -> {
146                             Log.e(TAG, "switchVolumeDevice has thrown an Exception", e);
147                             return null;
148                         });
149 
150         // Get the current system volume and try to get the preference volume
151         int savedVolume = getVolume(device, sNewDeviceVolume);
152 
153         d("switchVolumeDevice: savedVolume=" + savedVolume);
154 
155         // If absolute volume for the device is supported, set the volume for the device
156         if (mDeviceMap.get(device)) {
157             int avrcpVolume = systemToAvrcpVolume(savedVolume);
158             mVolumeEventLogger.logd(
159                     TAG, "switchVolumeDevice: Updating device volume: avrcpVolume=" + avrcpVolume);
160             mNativeInterface.sendVolumeChanged(device, avrcpVolume);
161         }
162     }
163 
164     /**
165      * Instantiates all class variables.
166      *
167      * <p>Fills {@code mVolumeMap} with content from {@link #getVolumeMap}, removing unbonded
168      * devices if necessary.
169      */
AvrcpVolumeManager( Context context, AudioManager audioManager, AvrcpNativeInterface nativeInterface)170     AvrcpVolumeManager(
171             Context context, AudioManager audioManager, AvrcpNativeInterface nativeInterface) {
172         mContext = context;
173         mAudioManager = audioManager;
174         mNativeInterface = nativeInterface;
175         sDeviceMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
176         sNewDeviceVolume = sDeviceMaxVolume / 2;
177 
178         mAudioManager.registerAudioDeviceCallback(this, null);
179 
180         // Load the stored volume preferences into a hash map since shared preferences are slow
181         // to poll and update. If the device has been unbonded since last start remove it from
182         // the map.
183         Map<String, ?> allKeys = getVolumeMap().getAll();
184         SharedPreferences.Editor volumeMapEditor = getVolumeMap().edit();
185         for (Map.Entry<String, ?> entry : allKeys.entrySet()) {
186             String key = entry.getKey();
187             Object value = entry.getValue();
188             BluetoothDevice d = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(key);
189 
190             if (value instanceof Integer && d.getBondState() == BluetoothDevice.BOND_BONDED) {
191                 mVolumeMap.put(d, (Integer) value);
192             } else {
193                 d("Removing " + key + " from the volume map");
194                 volumeMapEditor.remove(key);
195             }
196         }
197         volumeMapEditor.apply();
198     }
199 
200     /**
201      * Stores system volume (0 - {@code sDeviceMaxVolume}) for device in {@code mVolumeMap} and
202      * writes the map in the {@link SharedPreferences}.
203      */
storeVolumeForDevice(@onNull BluetoothDevice device, int storeVolume)204     synchronized void storeVolumeForDevice(@NonNull BluetoothDevice device, int storeVolume) {
205         if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
206             return;
207         }
208         SharedPreferences.Editor pref = getVolumeMap().edit();
209         mVolumeEventLogger.logd(
210                 TAG,
211                 "storeVolume: Storing stream volume level for device "
212                         + device
213                         + " : "
214                         + storeVolume);
215         mVolumeMap.put(device, storeVolume);
216         pref.putInt(device.getAddress(), storeVolume);
217         // Always use apply() since it is asynchronous, otherwise the call can hang waiting for
218         // storage to be written.
219         pref.apply();
220     }
221 
222     /**
223      * Retrieves system volume (0 - {@code sDeviceMaxVolume}) and calls {@link
224      * #storeVolumeForDevice(BluetoothDevice, int)} with {@code device}.
225      */
storeVolumeForDevice(@onNull BluetoothDevice device)226     synchronized void storeVolumeForDevice(@NonNull BluetoothDevice device) {
227         int storeVolume = mAudioManager.getLastAudibleStreamVolume(STREAM_MUSIC);
228         storeVolumeForDevice(device, storeVolume);
229     }
230 
231     /**
232      * Removes the stored volume of a device from {@code mVolumeMap} and writes the map in the
233      * {@link SharedPreferences}.
234      */
removeStoredVolumeForDevice(@onNull BluetoothDevice device)235     synchronized void removeStoredVolumeForDevice(@NonNull BluetoothDevice device) {
236         if (device.getBondState() != BluetoothDevice.BOND_NONE) {
237             return;
238         }
239         SharedPreferences.Editor pref = getVolumeMap().edit();
240         mVolumeEventLogger.logd(
241                 TAG, "RemoveStoredVolume: Remove stored stream volume level for device " + device);
242         mVolumeMap.remove(device);
243         pref.remove(device.getAddress());
244         // Always use apply() since it is asynchronous, otherwise the call can hang waiting for
245         // storage to be written.
246         pref.apply();
247     }
248 
249     /**
250      * Returns system volume (0 - {@code sDeviceMaxVolume}) stored in {@code mVolumeMap} for
251      * corresponding {@code device}.
252      *
253      * @param defaultValue Value to return if device is not in the map.
254      */
getVolume(@onNull BluetoothDevice device, int defaultValue)255     synchronized int getVolume(@NonNull BluetoothDevice device, int defaultValue) {
256         if (!mVolumeMap.containsKey(device)) {
257             Log.w(TAG, "getVolume: Couldn't find volume preference for device: " + device);
258             return defaultValue;
259         }
260 
261         d("getVolume: Returning volume " + mVolumeMap.get(device));
262         return mVolumeMap.get(device);
263     }
264 
265     /** Returns the system volume (0 - {@code sDeviceMaxVolume}) applied to a new device */
getNewDeviceVolume()266     public int getNewDeviceVolume() {
267         return sNewDeviceVolume;
268     }
269 
270     /**
271      * Informs {@link AudioManager} of a remote device volume change and stores it.
272      *
273      * <p>See {@link #avrcpToSystemVolume}.
274      *
275      * @param avrcpVolume in range (0 - 127) received from remote device.
276      */
setVolume(@onNull BluetoothDevice device, int avrcpVolume)277     void setVolume(@NonNull BluetoothDevice device, int avrcpVolume) {
278         int deviceVolume = avrcpToSystemVolume(avrcpVolume);
279         mVolumeEventLogger.logd(
280                 TAG,
281                 "setVolume:"
282                         + " device="
283                         + device
284                         + " avrcpVolume="
285                         + avrcpVolume
286                         + " deviceVolume="
287                         + deviceVolume
288                         + " sDeviceMaxVolume="
289                         + sDeviceMaxVolume);
290         mAudioManager.setStreamVolume(
291                 AudioManager.STREAM_MUSIC,
292                 deviceVolume,
293                 (deviceVolume != getVolume(device, -1) ? AudioManager.FLAG_SHOW_UI : 0)
294                         | AudioManager.FLAG_BLUETOOTH_ABS_VOLUME);
295         storeVolumeForDevice(device);
296     }
297 
298     /**
299      * Informs remote device of a system volume change and stores it.
300      *
301      * <p>See {@link #systemToAvrcpVolume}.
302      *
303      * @param deviceVolume in range (0 - {@code sDeviceMaxVolume}) received from system.
304      */
sendVolumeChanged(@onNull BluetoothDevice device, int deviceVolume)305     void sendVolumeChanged(@NonNull BluetoothDevice device, int deviceVolume) {
306         if (deviceVolume == getVolume(device, -1)) {
307             d("sendVolumeChanged: Skipping update volume to same as current.");
308             return;
309         }
310         int avrcpVolume = systemToAvrcpVolume(deviceVolume);
311         mVolumeEventLogger.logd(
312                 TAG,
313                 "sendVolumeChanged:"
314                         + " device="
315                         + device
316                         + " avrcpVolume="
317                         + avrcpVolume
318                         + " deviceVolume="
319                         + deviceVolume
320                         + " sDeviceMaxVolume="
321                         + sDeviceMaxVolume);
322         mNativeInterface.sendVolumeChanged(device, avrcpVolume);
323         storeVolumeForDevice(device);
324     }
325 
326     /** Returns whether absolute volume is supported by {@code device}. */
getAbsoluteVolumeSupported(BluetoothDevice device)327     boolean getAbsoluteVolumeSupported(BluetoothDevice device) {
328         if (mDeviceMap.containsKey(device)) {
329             return mDeviceMap.get(device);
330         }
331         return false;
332     }
333 
334     /**
335      * Callback from Media Framework to indicate new audio device was added.
336      *
337      * <p>Checks if the current active device is in the {@code addedDevices} list in order to inform
338      * {@link AudioManager} to take it as selected audio output. See {@link #switchVolumeDevice}.
339      *
340      * <p>If the remote device absolute volume support hasn't been established yet or if the current
341      * active device is not in the {@code addedDevices} list, this doesn't inform {@link
342      * AudioManager}. See {@link #deviceConnected} and {@link #volumeDeviceSwitched}.
343      */
344     @Override
onAudioDevicesAdded(AudioDeviceInfo[] addedDevices)345     public synchronized void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
346         if (mCurrentDevice == null) {
347             d("onAudioDevicesAdded: Not expecting device changed");
348             return;
349         }
350 
351         boolean foundDevice = false;
352         d("onAudioDevicesAdded: size: " + addedDevices.length);
353         for (int i = 0; i < addedDevices.length; i++) {
354             d("onAudioDevicesAdded: address=" + addedDevices[i].getAddress());
355             if (addedDevices[i].getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
356                     && Objects.equals(addedDevices[i].getAddress(), mCurrentDevice.getAddress())) {
357                 foundDevice = true;
358                 break;
359             }
360         }
361 
362         if (!foundDevice) {
363             d("Didn't find deferred device in list: device=" + mCurrentDevice);
364             return;
365         }
366 
367         // A2DP can sometimes connect and set a device to active before AVRCP has determined if the
368         // device supports absolute volume. Defer switching the device until AVRCP returns the
369         // info.
370         if (!mDeviceMap.containsKey(mCurrentDevice)) {
371             Log.w(TAG, "volumeDeviceSwitched: Device isn't connected: " + mCurrentDevice);
372             return;
373         }
374 
375         switchVolumeDevice(mCurrentDevice);
376     }
377 
378     /**
379      * Stores absolute volume support for {@code device}. If the current active device is the same
380      * as {@code device}, calls {@link #switchVolumeDevice}.
381      */
deviceConnected(@onNull BluetoothDevice device, boolean absoluteVolume)382     synchronized void deviceConnected(@NonNull BluetoothDevice device, boolean absoluteVolume) {
383         d("deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume);
384 
385         mDeviceMap.put(device, absoluteVolume);
386 
387         // AVRCP features lookup has completed after the device became active. Switch to the new
388         // device now.
389         if (device.equals(mCurrentDevice)) {
390             switchVolumeDevice(device);
391         }
392     }
393 
394     /**
395      * Called when the A2DP active device changed, this will call {@link #switchVolumeDevice} if we
396      * already know the absolute volume support of {@code device}.
397      */
volumeDeviceSwitched(@ullable BluetoothDevice device)398     synchronized void volumeDeviceSwitched(@Nullable BluetoothDevice device) {
399         d("volumeDeviceSwitched: mCurrentDevice=" + mCurrentDevice + " device=" + device);
400 
401         if (Objects.equals(device, mCurrentDevice)) {
402             return;
403         }
404 
405         mCurrentDevice = device;
406         if (!mDeviceMap.containsKey(device)) {
407             // Wait until AudioManager informs us that the new device is connected
408             return;
409         }
410         switchVolumeDevice(device);
411     }
412 
deviceDisconnected(@onNull BluetoothDevice device)413     synchronized void deviceDisconnected(@NonNull BluetoothDevice device) {
414         d("deviceDisconnected: device=" + device);
415         mDeviceMap.remove(device);
416     }
417 
dump(StringBuilder sb)418     public void dump(StringBuilder sb) {
419         sb.append("AvrcpVolumeManager:\n");
420         sb.append("  mCurrentDevice: " + mCurrentDevice + "\n");
421         sb.append("  Current System Volume: " + mAudioManager.getStreamVolume(STREAM_MUSIC) + "\n");
422         sb.append("  Device Volume Memory Map:\n");
423         sb.append(
424                 String.format(
425                         "    %-17s : %-14s : %3s : %s\n",
426                         "Device Address", "Device Name", "Vol", "AbsVol"));
427         Map<String, ?> allKeys = getVolumeMap().getAll();
428         for (Map.Entry<String, ?> entry : allKeys.entrySet()) {
429             Object value = entry.getValue();
430             BluetoothDevice d =
431                     BluetoothAdapter.getDefaultAdapter().getRemoteDevice(entry.getKey());
432 
433             String deviceName = d.getName();
434             if (deviceName == null) {
435                 deviceName = "";
436             } else if (deviceName.length() > 14) {
437                 deviceName = deviceName.substring(0, 11).concat("...");
438             }
439 
440             String absoluteVolume = "NotConnected";
441             if (mDeviceMap.containsKey(d)) {
442                 absoluteVolume = mDeviceMap.get(d).toString();
443             }
444 
445             if (value instanceof Integer) {
446                 sb.append(
447                         String.format(
448                                 "    %-17s : %-14s : %3d : %s\n",
449                                 d.getAddress(), deviceName, (Integer) value, absoluteVolume));
450             }
451         }
452 
453         StringBuilder tempBuilder = new StringBuilder();
454         mVolumeEventLogger.dump(tempBuilder);
455         // Tab volume event logs over by two spaces
456         sb.append(tempBuilder.toString().replaceAll("(?m)^", "  "));
457         tempBuilder.append("\n");
458     }
459 
d(String msg)460     static void d(String msg) {
461         Log.d(TAG, msg);
462     }
463 }
464