Home | History | Annotate | Download | only in newavrcp
      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 
     54     static int avrcpToSystemVolume(int avrcpVolume) {
     55         return (int) Math.floor((double) avrcpVolume * sDeviceMaxVolume / AVRCP_MAX_VOL);
     56     }
     57 
     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 
     65     private SharedPreferences getVolumeMap() {
     66         return mContext.getSharedPreferences(VOLUME_MAP, Context.MODE_PRIVATE);
     67     }
     68 
     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 
     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 
    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 
    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
    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 
    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 
    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 
    197     void deviceDisconnected(@NonNull BluetoothDevice device) {
    198         d("deviceDisconnected: device=" + device);
    199         mDeviceMap.remove(device);
    200     }
    201 
    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 
    234     static void d(String msg) {
    235         if (DEBUG) {
    236             Log.d(TAG, msg);
    237         }
    238     }
    239 }
    240