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