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