Loading android/app/src/com/android/bluetooth/a2dp/A2dpService.java +5 −7 Original line number Diff line number Diff line Loading @@ -803,14 +803,12 @@ public class A2dpService extends ProfileService { Log.d(TAG, "broadcastActiveDevice(" + device + ")"); } // Currently the audio service can only remember the volume for a single device. We send // active device changed intent after informing AVRCP that the device switched so it can // set the stream volume to the new device before A2DP informs the audio service that the // device has changed. This is to avoid the indeterminate volume state that exists when // in the middle of switching devices. // We need to inform AVRCP that the device has switched before informing the audio service // so that AVRCP can prepare and wait on audio service connecting the new stream before // restoring the previous volume. Otherwise the updated volume could be applied to the // old active device before the switch has fully completed. if (AvrcpTargetService.get() != null) { AvrcpTargetService.get().volumeDeviceSwitched( device != null ? device.getAddress() : ""); AvrcpTargetService.get().volumeDeviceSwitched(device); } Intent intent = new Intent(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED); Loading android/app/src/com/android/bluetooth/newavrcp/AvrcpNativeInterface.java +14 −9 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.bluetooth.avrcp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.util.Log; import java.util.List; Loading Loading @@ -186,31 +188,34 @@ public class AvrcpNativeInterface { } void setActiveDevice(String bdaddr) { bdaddr = bdaddr.toUpperCase(); d("setActiveDevice: bdaddr=" + bdaddr); mAvrcpService.setActiveDevice(bdaddr); BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(bdaddr.toUpperCase()); d("setActiveDevice: device=" + device); mAvrcpService.setActiveDevice(device); } void deviceConnected(String bdaddr, boolean absoluteVolume) { bdaddr = bdaddr.toUpperCase(); d("deviceConnected: bdaddr=" + bdaddr + " absoluteVolume=" + absoluteVolume); BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(bdaddr.toUpperCase()); d("deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume); if (mAvrcpService == null) { Log.w(TAG, "deviceConnected: AvrcpTargetService is null"); return; } mAvrcpService.deviceConnected(bdaddr, absoluteVolume); mAvrcpService.deviceConnected(device, absoluteVolume); } void deviceDisconnected(String bdaddr) { bdaddr = bdaddr.toUpperCase(); d("deviceDisconnected: bdaddr=" + bdaddr); BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(bdaddr.toUpperCase()); d("deviceDisconnected: device=" + device); if (mAvrcpService == null) { Log.w(TAG, "deviceDisconnected: AvrcpTargetService is null"); return; } mAvrcpService.deviceDisconnected(bdaddr); mAvrcpService.deviceDisconnected(device); } void sendVolumeChanged(int volume) { Loading android/app/src/com/android/bluetooth/newavrcp/AvrcpTargetService.java +21 −20 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.bluetooth.avrcp; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.IBluetoothAvrcpTarget; import android.content.BroadcastReceiver; Loading Loading @@ -187,15 +186,15 @@ public class AvrcpTargetService extends ProfileService { private void init() { } void deviceConnected(String bdaddr, boolean absoluteVolume) { Log.i(TAG, "deviceConnected: bdaddr=" + bdaddr + " absoluteVolume=" + absoluteVolume); mVolumeManager.deviceConnected(bdaddr, absoluteVolume); void deviceConnected(BluetoothDevice device, boolean absoluteVolume) { Log.i(TAG, "deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume); mVolumeManager.deviceConnected(device, absoluteVolume); MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.AVRCP); } void deviceDisconnected(String bdaddr) { Log.i(TAG, "deviceDisconnected: bdaddr=" + bdaddr); mVolumeManager.deviceDisconnected(bdaddr); void deviceDisconnected(BluetoothDevice device) { Log.i(TAG, "deviceDisconnected: device=" + device); mVolumeManager.deviceDisconnected(device); } /** Loading @@ -203,11 +202,11 @@ public class AvrcpTargetService extends ProfileService { * for the old device is saved and the new device has its volume restored. If there is no * saved volume use the current system volume. */ public void volumeDeviceSwitched(String bdaddr) { public void volumeDeviceSwitched(BluetoothDevice device) { if (DEBUG) { Log.d(TAG, "volumeDeviceSwitched: bdaddr=" + bdaddr); Log.d(TAG, "volumeDeviceSwitched: device=" + device); } mVolumeManager.volumeDeviceSwitched(bdaddr); mVolumeManager.volumeDeviceSwitched(device); } // TODO (apanicke): Add checks to blacklist Absolute Volume devices if they behave poorly. Loading Loading @@ -293,14 +292,12 @@ public class AvrcpTargetService extends ProfileService { mMediaPlayerList.sendMediaKeyEvent(event, pushed); } void setActiveDevice(String address) { Log.i(TAG, "setActiveDevice: address=" + address); BluetoothDevice d = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address); if (d == null) { Log.wtfStack(TAG, "setActiveDevice: could not find device with address " + address); void setActiveDevice(BluetoothDevice device) { Log.i(TAG, "setActiveDevice: device=" + device); if (device == null) { Log.wtfStack(TAG, "setActiveDevice: could not find device " + device); } A2dpService.getA2dpService().setActiveDevice(d); A2dpService.getA2dpService().setActiveDevice(device); } /** Loading @@ -313,13 +310,17 @@ public class AvrcpTargetService extends ProfileService { return; } StringBuilder tempBuilder = new StringBuilder(); if (mMediaPlayerList != null) { mMediaPlayerList.dump(sb); mMediaPlayerList.dump(tempBuilder); } else { sb.append("\nMedia Player List is empty\n"); tempBuilder.append("\nMedia Player List is empty\n"); } mVolumeManager.dump(sb); mVolumeManager.dump(tempBuilder); // Tab everything over by two spaces sb.append(tempBuilder.toString().replaceAll("(?m)^", " ")); } private static class AvrcpTargetBinder extends IBluetoothAvrcpTarget.Stub Loading android/app/src/com/android/bluetooth/newavrcp/AvrcpVolumeManager.java +128 −66 Original line number Diff line number Diff line Loading @@ -16,15 +16,22 @@ package com.android.bluetooth.avrcp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.SharedPreferences; import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import java.util.HashMap; import java.util.Map; import java.util.Objects; class AvrcpVolumeManager { class AvrcpVolumeManager extends AudioDeviceCallback { public static final String TAG = "NewAvrcpVolumeManager"; public static final boolean DEBUG = true; Loading @@ -39,9 +46,10 @@ class AvrcpVolumeManager { AudioManager mAudioManager; AvrcpNativeInterface mNativeInterface; HashMap<String, Boolean> mDeviceMap = new HashMap<String, Boolean>(); HashMap<String, Integer> mVolumeMap = new HashMap<String, Integer>(); String mCurrentDeviceAddr = ""; HashMap<BluetoothDevice, Boolean> mDeviceMap = new HashMap(); HashMap<BluetoothDevice, Integer> mVolumeMap = new HashMap(); BluetoothDevice mDeferredDevice = null; BluetoothDevice mCurrentDevice = null; boolean mAbsoluteVolumeSupported = false; static int avrcpToSystemVolume(int avrcpVolume) { Loading @@ -59,43 +67,76 @@ class AvrcpVolumeManager { return mContext.getSharedPreferences(VOLUME_MAP, Context.MODE_PRIVATE); } private int getVolume(String bdaddr, int defaultValue) { if (!mVolumeMap.containsKey(bdaddr)) { Log.w(TAG, "getVolume: Couldn't find volume preference for device: " + bdaddr); private int getVolume(@NonNull BluetoothDevice device, int defaultValue) { if (!mVolumeMap.containsKey(device)) { Log.w(TAG, "getVolume: Couldn't find volume preference for device: " + device); return defaultValue; } return mVolumeMap.get(bdaddr); return mVolumeMap.get(device); } private void switchVolumeDevice(String bdaddr) { private void switchVolumeDevice(@NonNull BluetoothDevice device) { // Inform the audio manager that the device has changed mAudioManager.avrcpSupportsAbsoluteVolume(bdaddr, mDeviceMap.get(bdaddr)); mAudioManager.avrcpSupportsAbsoluteVolume(device.getAddress(), mDeviceMap.get(device)); // Get the current system volume and try to get the preference volume int currVolume = mAudioManager.getStreamVolume(STREAM_MUSIC); int savedVolume = getVolume(bdaddr, currVolume); int savedVolume = getVolume(device, currVolume); // If the preference volume isn't equal to the current stream volume then that means // we had a stored preference. if (DEBUG) { Log.d(TAG, "switchVolumeDevice: currVolume=" + currVolume + " savedVolume=" + savedVolume); } if (savedVolume != currVolume) { d("switchVolumeDevice: currVolume=" + currVolume + " savedVolume=" + savedVolume); Log.i(TAG, "switchVolumeDevice: restoring volume level " + savedVolume); mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, savedVolume, AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_BLUETOOTH_ABS_VOLUME); } // If absolute volume for the device is supported, set the volume for the device if (mDeviceMap.get(bdaddr)) { if (mDeviceMap.get(device)) { int avrcpVolume = systemToAvrcpVolume(savedVolume); Log.i(TAG, "switchVolumeDevice: Updating device volume: avrcpVolume=" + avrcpVolume); mNativeInterface.sendVolumeChanged(avrcpVolume); } } @Override public synchronized void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { if (mDeferredDevice == null) { d("onAudioDevicesAdded: Not expecting device changed"); return; } boolean foundDevice = false; d("onAudioDevicesAdded: size: " + addedDevices.length); for (int i = 0; i < addedDevices.length; i++) { d("onAudioDevicesAdded: address=" + addedDevices[i].getAddress()); if (addedDevices[i].getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP && Objects.equals(addedDevices[i].getAddress(), mDeferredDevice.getAddress())) { foundDevice = true; break; } } if (!foundDevice) { d("Didn't find deferred device in list: device=" + mDeferredDevice); return; } mCurrentDevice = mDeferredDevice; mDeferredDevice = null; // A2DP can sometimes connect and set a device to active before AVRCP has determined if the // device supports absolute volume. Defer switching the device until AVRCP returns the // info. if (!mDeviceMap.containsKey(mCurrentDevice)) { Log.w(TAG, "volumeDeviceSwitched: Device isn't connected: " + mCurrentDevice); return; } switchVolumeDevice(mCurrentDevice); } AvrcpVolumeManager(Context context, AudioManager audioManager, AvrcpNativeInterface nativeInterface) { mContext = context; Loading @@ -103,93 +144,114 @@ class AvrcpVolumeManager { mNativeInterface = nativeInterface; sDeviceMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); // Load the volume map into a hash map since shared preferences are slow to poll and update mAudioManager.registerAudioDeviceCallback(this, null); // Load the stored volume preferences into a hash map since shared preferences are slow // to poll and update. If the device has been unbonded since last start remove it from // the map. Map<String, ?> allKeys = getVolumeMap().getAll(); SharedPreferences.Editor volumeMapEditor = getVolumeMap().edit(); for (Map.Entry<String, ?> entry : allKeys.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); if (value instanceof Integer) { mVolumeMap.put(key, (Integer) value); BluetoothDevice d = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(key); if (value instanceof Integer && d.getBondState() == BluetoothDevice.BOND_BONDED) { mVolumeMap.put(d, (Integer) value); } else { d("Removing " + key + " from the volume map"); volumeMapEditor.remove(key); } } volumeMapEditor.apply(); } void storeVolume() { SharedPreferences.Editor pref = getVolumeMap().edit(); int storeVolume = mAudioManager.getStreamVolume(STREAM_MUSIC); Log.i(TAG, "storeVolume: Storing stream volume level for device " + mCurrentDeviceAddr Log.i(TAG, "storeVolume: Storing stream volume level for device " + mCurrentDevice + " : " + storeVolume); mVolumeMap.put(mCurrentDeviceAddr, storeVolume); pref.putInt(mCurrentDeviceAddr, storeVolume); mVolumeMap.put(mCurrentDevice, storeVolume); pref.putInt(mCurrentDevice.getAddress(), storeVolume); // Always use apply() since it is asynchronous, otherwise the call can hang waiting for // storage to be written. pref.apply(); } void deviceConnected(String bdaddr, boolean absoluteVolume) { if (DEBUG) { Log.d(TAG, "deviceConnected: bdaddr=" + bdaddr + " absoluteVolume=" + absoluteVolume); } synchronized void deviceConnected(@NonNull BluetoothDevice device, boolean absoluteVolume) { d("deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume); mDeviceMap.put(bdaddr, absoluteVolume); mDeviceMap.put(device, absoluteVolume); // AVRCP features lookup has completed after the device became active. Switch to the new // device now. if (bdaddr.equals(mCurrentDeviceAddr)) { switchVolumeDevice(bdaddr); if (device.equals(mCurrentDevice)) { switchVolumeDevice(device); } } void volumeDeviceSwitched(String bdaddr) { if (DEBUG) { Log.d(TAG, "volumeDeviceSwitched: mCurrentDeviceAddr=" + mCurrentDeviceAddr + " bdaddr=" + bdaddr); } synchronized void volumeDeviceSwitched(@Nullable BluetoothDevice device) { d("volumeDeviceSwitched: mCurrentDevice=" + mCurrentDevice + " device=" + device); if (bdaddr == null || bdaddr.equals(mCurrentDeviceAddr)) { if (Objects.equals(device, mCurrentDevice)) { return; } // Store the previous volume if a device was active. if (!mCurrentDeviceAddr.isEmpty()) { if (mCurrentDevice != null) { storeVolume(); } // Set the current volume device to the new device. mCurrentDeviceAddr = bdaddr; // Wait until AudioManager informs us that the new device is connected mDeferredDevice = device; // No new active device. if (bdaddr.isEmpty()) { return; // If device is null, that means that there is no active Bluetooth device if (device == null) { mCurrentDevice = null; } // A2DP can sometimes connect and set a device to active before AVRCP has determined if the // device supports absolute volume. Defer switching the device until AVRCP returns the // info. if (!mDeviceMap.containsKey(bdaddr)) { Log.w(TAG, "volumeDeviceSwitched: Device isn't connected: " + bdaddr); return; } switchVolumeDevice(bdaddr); } void deviceDisconnected(String bdaddr) { if (DEBUG) { Log.d(TAG, "deviceDisconnected: bdaddr=" + bdaddr); } mDeviceMap.remove(bdaddr); void deviceDisconnected(@NonNull BluetoothDevice device) { d("deviceDisconnected: device=" + device); mDeviceMap.remove(device); } public void dump(StringBuilder sb) { sb.append("Bluetooth Device Volume Map:\n"); sb.append(" Device Address : Volume\n"); sb.append("AvrcpVolumeManager:\n"); sb.append(" mCurrentDevice: " + mCurrentDevice + "\n"); sb.append(" mDeferredDevice: " + mDeferredDevice + "\n"); sb.append(" Device Volume Memory Map:\n"); sb.append(String.format(" %-17s : %-14s : %3s : %s\n", "Device Address", "Device Name", "Vol", "AbsVol")); Map<String, ?> allKeys = getVolumeMap().getAll(); for (Map.Entry<String, ?> entry : allKeys.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); BluetoothDevice d = BluetoothAdapter.getDefaultAdapter() .getRemoteDevice(entry.getKey()); String deviceName = d.getName(); if (deviceName == null) { deviceName = ""; } else if (deviceName.length() > 14) { deviceName = deviceName.substring(0, 11).concat("..."); } String absoluteVolume = "NotConnected"; if (mDeviceMap.containsKey(d)) { absoluteVolume = mDeviceMap.get(d).toString(); } if (value instanceof Integer) { sb.append(" " + key + " : " + (Integer) value + "\n"); mVolumeMap.put(key, (Integer) value); sb.append(String.format(" %-17s : %-14s : %3d : %s\n", d.getAddress(), deviceName, (Integer) value, absoluteVolume)); } } } static void d(String msg) { if (DEBUG) { Log.d(TAG, msg); } } } Loading
android/app/src/com/android/bluetooth/a2dp/A2dpService.java +5 −7 Original line number Diff line number Diff line Loading @@ -803,14 +803,12 @@ public class A2dpService extends ProfileService { Log.d(TAG, "broadcastActiveDevice(" + device + ")"); } // Currently the audio service can only remember the volume for a single device. We send // active device changed intent after informing AVRCP that the device switched so it can // set the stream volume to the new device before A2DP informs the audio service that the // device has changed. This is to avoid the indeterminate volume state that exists when // in the middle of switching devices. // We need to inform AVRCP that the device has switched before informing the audio service // so that AVRCP can prepare and wait on audio service connecting the new stream before // restoring the previous volume. Otherwise the updated volume could be applied to the // old active device before the switch has fully completed. if (AvrcpTargetService.get() != null) { AvrcpTargetService.get().volumeDeviceSwitched( device != null ? device.getAddress() : ""); AvrcpTargetService.get().volumeDeviceSwitched(device); } Intent intent = new Intent(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED); Loading
android/app/src/com/android/bluetooth/newavrcp/AvrcpNativeInterface.java +14 −9 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.bluetooth.avrcp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.util.Log; import java.util.List; Loading Loading @@ -186,31 +188,34 @@ public class AvrcpNativeInterface { } void setActiveDevice(String bdaddr) { bdaddr = bdaddr.toUpperCase(); d("setActiveDevice: bdaddr=" + bdaddr); mAvrcpService.setActiveDevice(bdaddr); BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(bdaddr.toUpperCase()); d("setActiveDevice: device=" + device); mAvrcpService.setActiveDevice(device); } void deviceConnected(String bdaddr, boolean absoluteVolume) { bdaddr = bdaddr.toUpperCase(); d("deviceConnected: bdaddr=" + bdaddr + " absoluteVolume=" + absoluteVolume); BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(bdaddr.toUpperCase()); d("deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume); if (mAvrcpService == null) { Log.w(TAG, "deviceConnected: AvrcpTargetService is null"); return; } mAvrcpService.deviceConnected(bdaddr, absoluteVolume); mAvrcpService.deviceConnected(device, absoluteVolume); } void deviceDisconnected(String bdaddr) { bdaddr = bdaddr.toUpperCase(); d("deviceDisconnected: bdaddr=" + bdaddr); BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(bdaddr.toUpperCase()); d("deviceDisconnected: device=" + device); if (mAvrcpService == null) { Log.w(TAG, "deviceDisconnected: AvrcpTargetService is null"); return; } mAvrcpService.deviceDisconnected(bdaddr); mAvrcpService.deviceDisconnected(device); } void sendVolumeChanged(int volume) { Loading
android/app/src/com/android/bluetooth/newavrcp/AvrcpTargetService.java +21 −20 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.bluetooth.avrcp; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.IBluetoothAvrcpTarget; import android.content.BroadcastReceiver; Loading Loading @@ -187,15 +186,15 @@ public class AvrcpTargetService extends ProfileService { private void init() { } void deviceConnected(String bdaddr, boolean absoluteVolume) { Log.i(TAG, "deviceConnected: bdaddr=" + bdaddr + " absoluteVolume=" + absoluteVolume); mVolumeManager.deviceConnected(bdaddr, absoluteVolume); void deviceConnected(BluetoothDevice device, boolean absoluteVolume) { Log.i(TAG, "deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume); mVolumeManager.deviceConnected(device, absoluteVolume); MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.AVRCP); } void deviceDisconnected(String bdaddr) { Log.i(TAG, "deviceDisconnected: bdaddr=" + bdaddr); mVolumeManager.deviceDisconnected(bdaddr); void deviceDisconnected(BluetoothDevice device) { Log.i(TAG, "deviceDisconnected: device=" + device); mVolumeManager.deviceDisconnected(device); } /** Loading @@ -203,11 +202,11 @@ public class AvrcpTargetService extends ProfileService { * for the old device is saved and the new device has its volume restored. If there is no * saved volume use the current system volume. */ public void volumeDeviceSwitched(String bdaddr) { public void volumeDeviceSwitched(BluetoothDevice device) { if (DEBUG) { Log.d(TAG, "volumeDeviceSwitched: bdaddr=" + bdaddr); Log.d(TAG, "volumeDeviceSwitched: device=" + device); } mVolumeManager.volumeDeviceSwitched(bdaddr); mVolumeManager.volumeDeviceSwitched(device); } // TODO (apanicke): Add checks to blacklist Absolute Volume devices if they behave poorly. Loading Loading @@ -293,14 +292,12 @@ public class AvrcpTargetService extends ProfileService { mMediaPlayerList.sendMediaKeyEvent(event, pushed); } void setActiveDevice(String address) { Log.i(TAG, "setActiveDevice: address=" + address); BluetoothDevice d = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address); if (d == null) { Log.wtfStack(TAG, "setActiveDevice: could not find device with address " + address); void setActiveDevice(BluetoothDevice device) { Log.i(TAG, "setActiveDevice: device=" + device); if (device == null) { Log.wtfStack(TAG, "setActiveDevice: could not find device " + device); } A2dpService.getA2dpService().setActiveDevice(d); A2dpService.getA2dpService().setActiveDevice(device); } /** Loading @@ -313,13 +310,17 @@ public class AvrcpTargetService extends ProfileService { return; } StringBuilder tempBuilder = new StringBuilder(); if (mMediaPlayerList != null) { mMediaPlayerList.dump(sb); mMediaPlayerList.dump(tempBuilder); } else { sb.append("\nMedia Player List is empty\n"); tempBuilder.append("\nMedia Player List is empty\n"); } mVolumeManager.dump(sb); mVolumeManager.dump(tempBuilder); // Tab everything over by two spaces sb.append(tempBuilder.toString().replaceAll("(?m)^", " ")); } private static class AvrcpTargetBinder extends IBluetoothAvrcpTarget.Stub Loading
android/app/src/com/android/bluetooth/newavrcp/AvrcpVolumeManager.java +128 −66 Original line number Diff line number Diff line Loading @@ -16,15 +16,22 @@ package com.android.bluetooth.avrcp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.SharedPreferences; import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import java.util.HashMap; import java.util.Map; import java.util.Objects; class AvrcpVolumeManager { class AvrcpVolumeManager extends AudioDeviceCallback { public static final String TAG = "NewAvrcpVolumeManager"; public static final boolean DEBUG = true; Loading @@ -39,9 +46,10 @@ class AvrcpVolumeManager { AudioManager mAudioManager; AvrcpNativeInterface mNativeInterface; HashMap<String, Boolean> mDeviceMap = new HashMap<String, Boolean>(); HashMap<String, Integer> mVolumeMap = new HashMap<String, Integer>(); String mCurrentDeviceAddr = ""; HashMap<BluetoothDevice, Boolean> mDeviceMap = new HashMap(); HashMap<BluetoothDevice, Integer> mVolumeMap = new HashMap(); BluetoothDevice mDeferredDevice = null; BluetoothDevice mCurrentDevice = null; boolean mAbsoluteVolumeSupported = false; static int avrcpToSystemVolume(int avrcpVolume) { Loading @@ -59,43 +67,76 @@ class AvrcpVolumeManager { return mContext.getSharedPreferences(VOLUME_MAP, Context.MODE_PRIVATE); } private int getVolume(String bdaddr, int defaultValue) { if (!mVolumeMap.containsKey(bdaddr)) { Log.w(TAG, "getVolume: Couldn't find volume preference for device: " + bdaddr); private int getVolume(@NonNull BluetoothDevice device, int defaultValue) { if (!mVolumeMap.containsKey(device)) { Log.w(TAG, "getVolume: Couldn't find volume preference for device: " + device); return defaultValue; } return mVolumeMap.get(bdaddr); return mVolumeMap.get(device); } private void switchVolumeDevice(String bdaddr) { private void switchVolumeDevice(@NonNull BluetoothDevice device) { // Inform the audio manager that the device has changed mAudioManager.avrcpSupportsAbsoluteVolume(bdaddr, mDeviceMap.get(bdaddr)); mAudioManager.avrcpSupportsAbsoluteVolume(device.getAddress(), mDeviceMap.get(device)); // Get the current system volume and try to get the preference volume int currVolume = mAudioManager.getStreamVolume(STREAM_MUSIC); int savedVolume = getVolume(bdaddr, currVolume); int savedVolume = getVolume(device, currVolume); // If the preference volume isn't equal to the current stream volume then that means // we had a stored preference. if (DEBUG) { Log.d(TAG, "switchVolumeDevice: currVolume=" + currVolume + " savedVolume=" + savedVolume); } if (savedVolume != currVolume) { d("switchVolumeDevice: currVolume=" + currVolume + " savedVolume=" + savedVolume); Log.i(TAG, "switchVolumeDevice: restoring volume level " + savedVolume); mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, savedVolume, AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_BLUETOOTH_ABS_VOLUME); } // If absolute volume for the device is supported, set the volume for the device if (mDeviceMap.get(bdaddr)) { if (mDeviceMap.get(device)) { int avrcpVolume = systemToAvrcpVolume(savedVolume); Log.i(TAG, "switchVolumeDevice: Updating device volume: avrcpVolume=" + avrcpVolume); mNativeInterface.sendVolumeChanged(avrcpVolume); } } @Override public synchronized void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { if (mDeferredDevice == null) { d("onAudioDevicesAdded: Not expecting device changed"); return; } boolean foundDevice = false; d("onAudioDevicesAdded: size: " + addedDevices.length); for (int i = 0; i < addedDevices.length; i++) { d("onAudioDevicesAdded: address=" + addedDevices[i].getAddress()); if (addedDevices[i].getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP && Objects.equals(addedDevices[i].getAddress(), mDeferredDevice.getAddress())) { foundDevice = true; break; } } if (!foundDevice) { d("Didn't find deferred device in list: device=" + mDeferredDevice); return; } mCurrentDevice = mDeferredDevice; mDeferredDevice = null; // A2DP can sometimes connect and set a device to active before AVRCP has determined if the // device supports absolute volume. Defer switching the device until AVRCP returns the // info. if (!mDeviceMap.containsKey(mCurrentDevice)) { Log.w(TAG, "volumeDeviceSwitched: Device isn't connected: " + mCurrentDevice); return; } switchVolumeDevice(mCurrentDevice); } AvrcpVolumeManager(Context context, AudioManager audioManager, AvrcpNativeInterface nativeInterface) { mContext = context; Loading @@ -103,93 +144,114 @@ class AvrcpVolumeManager { mNativeInterface = nativeInterface; sDeviceMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); // Load the volume map into a hash map since shared preferences are slow to poll and update mAudioManager.registerAudioDeviceCallback(this, null); // Load the stored volume preferences into a hash map since shared preferences are slow // to poll and update. If the device has been unbonded since last start remove it from // the map. Map<String, ?> allKeys = getVolumeMap().getAll(); SharedPreferences.Editor volumeMapEditor = getVolumeMap().edit(); for (Map.Entry<String, ?> entry : allKeys.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); if (value instanceof Integer) { mVolumeMap.put(key, (Integer) value); BluetoothDevice d = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(key); if (value instanceof Integer && d.getBondState() == BluetoothDevice.BOND_BONDED) { mVolumeMap.put(d, (Integer) value); } else { d("Removing " + key + " from the volume map"); volumeMapEditor.remove(key); } } volumeMapEditor.apply(); } void storeVolume() { SharedPreferences.Editor pref = getVolumeMap().edit(); int storeVolume = mAudioManager.getStreamVolume(STREAM_MUSIC); Log.i(TAG, "storeVolume: Storing stream volume level for device " + mCurrentDeviceAddr Log.i(TAG, "storeVolume: Storing stream volume level for device " + mCurrentDevice + " : " + storeVolume); mVolumeMap.put(mCurrentDeviceAddr, storeVolume); pref.putInt(mCurrentDeviceAddr, storeVolume); mVolumeMap.put(mCurrentDevice, storeVolume); pref.putInt(mCurrentDevice.getAddress(), storeVolume); // Always use apply() since it is asynchronous, otherwise the call can hang waiting for // storage to be written. pref.apply(); } void deviceConnected(String bdaddr, boolean absoluteVolume) { if (DEBUG) { Log.d(TAG, "deviceConnected: bdaddr=" + bdaddr + " absoluteVolume=" + absoluteVolume); } synchronized void deviceConnected(@NonNull BluetoothDevice device, boolean absoluteVolume) { d("deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume); mDeviceMap.put(bdaddr, absoluteVolume); mDeviceMap.put(device, absoluteVolume); // AVRCP features lookup has completed after the device became active. Switch to the new // device now. if (bdaddr.equals(mCurrentDeviceAddr)) { switchVolumeDevice(bdaddr); if (device.equals(mCurrentDevice)) { switchVolumeDevice(device); } } void volumeDeviceSwitched(String bdaddr) { if (DEBUG) { Log.d(TAG, "volumeDeviceSwitched: mCurrentDeviceAddr=" + mCurrentDeviceAddr + " bdaddr=" + bdaddr); } synchronized void volumeDeviceSwitched(@Nullable BluetoothDevice device) { d("volumeDeviceSwitched: mCurrentDevice=" + mCurrentDevice + " device=" + device); if (bdaddr == null || bdaddr.equals(mCurrentDeviceAddr)) { if (Objects.equals(device, mCurrentDevice)) { return; } // Store the previous volume if a device was active. if (!mCurrentDeviceAddr.isEmpty()) { if (mCurrentDevice != null) { storeVolume(); } // Set the current volume device to the new device. mCurrentDeviceAddr = bdaddr; // Wait until AudioManager informs us that the new device is connected mDeferredDevice = device; // No new active device. if (bdaddr.isEmpty()) { return; // If device is null, that means that there is no active Bluetooth device if (device == null) { mCurrentDevice = null; } // A2DP can sometimes connect and set a device to active before AVRCP has determined if the // device supports absolute volume. Defer switching the device until AVRCP returns the // info. if (!mDeviceMap.containsKey(bdaddr)) { Log.w(TAG, "volumeDeviceSwitched: Device isn't connected: " + bdaddr); return; } switchVolumeDevice(bdaddr); } void deviceDisconnected(String bdaddr) { if (DEBUG) { Log.d(TAG, "deviceDisconnected: bdaddr=" + bdaddr); } mDeviceMap.remove(bdaddr); void deviceDisconnected(@NonNull BluetoothDevice device) { d("deviceDisconnected: device=" + device); mDeviceMap.remove(device); } public void dump(StringBuilder sb) { sb.append("Bluetooth Device Volume Map:\n"); sb.append(" Device Address : Volume\n"); sb.append("AvrcpVolumeManager:\n"); sb.append(" mCurrentDevice: " + mCurrentDevice + "\n"); sb.append(" mDeferredDevice: " + mDeferredDevice + "\n"); sb.append(" Device Volume Memory Map:\n"); sb.append(String.format(" %-17s : %-14s : %3s : %s\n", "Device Address", "Device Name", "Vol", "AbsVol")); Map<String, ?> allKeys = getVolumeMap().getAll(); for (Map.Entry<String, ?> entry : allKeys.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); BluetoothDevice d = BluetoothAdapter.getDefaultAdapter() .getRemoteDevice(entry.getKey()); String deviceName = d.getName(); if (deviceName == null) { deviceName = ""; } else if (deviceName.length() > 14) { deviceName = deviceName.substring(0, 11).concat("..."); } String absoluteVolume = "NotConnected"; if (mDeviceMap.containsKey(d)) { absoluteVolume = mDeviceMap.get(d).toString(); } if (value instanceof Integer) { sb.append(" " + key + " : " + (Integer) value + "\n"); mVolumeMap.put(key, (Integer) value); sb.append(String.format(" %-17s : %-14s : %3d : %s\n", d.getAddress(), deviceName, (Integer) value, absoluteVolume)); } } } static void d(String msg) { if (DEBUG) { Log.d(TAG, msg); } } }