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