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