Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5bf4d096 authored by Ajay Panicker's avatar Ajay Panicker Committed by Hansong Zhang
Browse files

Implement Bluetooth device volume memory

Bluetooth volume memory is now handled on a per device basis. The volume
is stored in a shared preference and is loaded every time the device is
active. Volume is persisted for devices that both do and don't support
absolute volume.

Bug: 33962015
Test: Connect to multiple devices and switch between devices. See that
the volume is restored each time a device becomes active.

Change-Id: I596a4f0d701a7c5b8c52285807970f8b172cc423
(cherry picked from commit 029dc111e40ec8603d51423592e7a3b1179c31a2)
parent e60b9771
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -432,6 +432,8 @@ public class A2dpService extends ProfileService {
            if (device == null) {
                // Clear the active device
                mActiveDevice = null;
                // This needs to happen before we inform the audio manager that the device
                // disconnected. Please see comment in broadcastActiveDevice() for why.
                broadcastActiveDevice(null);
                if (previousActiveDevice != null) {
                    // Make sure the Audio Manager knows the previous Active device is disconnected
@@ -467,6 +469,8 @@ public class A2dpService extends ProfileService {

            boolean deviceChanged = !Objects.equals(device, mActiveDevice);
            mActiveDevice = device;
            // This needs to happen before we inform the audio manager that the device
            // disconnected. Please see comment in broadcastActiveDevice() for why.
            broadcastActiveDevice(mActiveDevice);
            if (deviceChanged) {
                // Send an intent with the active device codec config
@@ -786,6 +790,16 @@ 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.
        if (AvrcpTargetService.get() != null) {
            AvrcpTargetService.get().volumeDeviceSwitched(
                    device != null ? device.getAddress() : "");
        }

        Intent intent = new Intent(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+27 −2
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ public class AvrcpTargetService extends ProfileService {
    private AudioManager mAudioManager;
    private AvrcpBroadcastReceiver mReceiver;
    private AvrcpNativeInterface mNativeInterface;
    private AvrcpVolumeManager mVolumeManager;

    // Only used to see if the metadata has changed from its previous value
    private MediaData mCurrentData;
@@ -157,6 +158,8 @@ public class AvrcpTargetService extends ProfileService {
        mNativeInterface = AvrcpNativeInterface.getInterface();
        mNativeInterface.init(AvrcpTargetService.this);

        mVolumeManager = new AvrcpVolumeManager(this, mAudioManager, mNativeInterface);

        // Only allow the service to be used once it is initialized
        sInstance = this;

@@ -186,12 +189,25 @@ public class AvrcpTargetService extends ProfileService {

    void deviceConnected(String bdaddr, boolean absoluteVolume) {
        Log.i(TAG, "deviceConnected: bdaddr=" + bdaddr + " absoluteVolume=" + absoluteVolume);
        mAudioManager.avrcpSupportsAbsoluteVolume(bdaddr, absoluteVolume);
        mVolumeManager.deviceConnected(bdaddr, absoluteVolume);
        MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.AVRCP);
    }

    void deviceDisconnected(String bdaddr) {
        // Do nothing
        Log.i(TAG, "deviceDisconnected: bdaddr=" + bdaddr);
        mVolumeManager.deviceDisconnected(bdaddr);
    }

    /**
     * Signal to the service that the current audio out device has changed. The current volume
     * 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) {
        if (DEBUG) {
            Log.d(TAG, "volumeDeviceSwitched: bdaddr=" + bdaddr);
        }
        mVolumeManager.volumeDeviceSwitched(bdaddr);
    }

    // TODO (apanicke): Add checks to blacklist Absolute Volume devices if they behave poorly.
@@ -291,11 +307,20 @@ public class AvrcpTargetService extends ProfileService {
     * Dump debugging information to the string builder
     */
    public void dump(StringBuilder sb) {
        sb.append("\nProfile: AvrcpTargetService:\n");
        if (sInstance == null) {
            sb.append("AvrcpTargetService not running");
            return;
        }

        if (mMediaPlayerList != null) {
            mMediaPlayerList.dump(sb);
        } else {
            sb.append("\nMedia Player List is empty\n");
        }

        mVolumeManager.dump(sb);
        sb.append("\n");
    }

    private static class AvrcpTargetBinder extends IBluetoothAvrcpTarget.Stub
+191 −0
Original line number Diff line number Diff line
/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.bluetooth.avrcp;

import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.util.Log;

import java.util.HashMap;
import java.util.Map;

class AvrcpVolumeManager {
    public static final String TAG = "NewAvrcpVolumeManager";
    public static final boolean DEBUG = true;

    // All volumes are stored at system volume values, not AVRCP values
    public static final String VOLUME_MAP = "bluetooth_volume_map";
    public static final String VOLUME_BLACKLIST = "absolute_volume_blacklist";
    public static final int AVRCP_MAX_VOL = 127;
    public static int sDeviceMaxVolume = 0;
    public static final int STREAM_MUSIC = AudioManager.STREAM_MUSIC;

    Context mContext;
    AudioManager mAudioManager;
    AvrcpNativeInterface mNativeInterface;

    HashMap<String, Boolean> mDeviceMap = new HashMap<String, Boolean>();
    HashMap<String, Integer> mVolumeMap = new HashMap<String, Integer>();
    String mCurrentDeviceAddr = "";
    boolean mAbsoluteVolumeSupported = false;

    int avrcpToSystemVolume(int avrcpVolume) {
        return (int) Math.floor((double) avrcpVolume * sDeviceMaxVolume / AVRCP_MAX_VOL);
    }

    int systemToAvrcpVolume(int deviceVolume) {
        int avrcpVolume = (int) Math.floor((double) deviceVolume
                * AVRCP_MAX_VOL / sDeviceMaxVolume);
        if (avrcpVolume > 127) avrcpVolume = 127;
        return avrcpVolume;
    }

    SharedPreferences getVolumeMap() {
        return mContext.getSharedPreferences(VOLUME_MAP, Context.MODE_PRIVATE);
    }

    AvrcpVolumeManager(Context context, AudioManager audioManager,
            AvrcpNativeInterface nativeInterface) {
        mContext = context;
        mAudioManager = audioManager;
        mNativeInterface = nativeInterface;
        sDeviceMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);

        // Load the volume map into a hash map since shared preferences are slow
        Map<String, ?> allKeys = getVolumeMap().getAll();
        for (Map.Entry<String, ?> entry : allKeys.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if (value instanceof Integer) {
                mVolumeMap.put(key, (Integer) value);
            }
        }
    }

    int getVolume(String bdaddr, int defaultValue) {
        if (!mVolumeMap.containsKey(bdaddr)) {
            Log.w(TAG, "getVolume: Couldn't find volume preference for device: " + bdaddr);
            return defaultValue;
        }

        return mVolumeMap.get(bdaddr);
    }

    void storeVolume() {
        SharedPreferences.Editor pref = getVolumeMap().edit();
        int storeVolume =  mAudioManager.getStreamVolume(STREAM_MUSIC);
        Log.i(TAG, "storeVolume: Storing stream volume level for device " + mCurrentDeviceAddr
                + " : " + storeVolume);
        mVolumeMap.put(mCurrentDeviceAddr, storeVolume);
        pref.putInt(mCurrentDeviceAddr, storeVolume);
        pref.apply();
    }

    void deviceConnected(String bdaddr, boolean absoluteVolume) {
        if (DEBUG) {
            Log.d(TAG, "deviceConnected: bdaddr=" + bdaddr + " absoluteVolume=" + absoluteVolume);
        }

        mDeviceMap.put(bdaddr.toUpperCase(), absoluteVolume);

        // AVRCP features lookup has completed after the device became active. Switch to the new
        // device now.
        if (bdaddr == mCurrentDeviceAddr) {
            switchVolumeDevice(bdaddr);
        }
    }

    void volumeDeviceSwitched(String bdaddr) {
        if (DEBUG) {
            Log.d(TAG, "activeDeviceChanged: mCurrentDeviceAddr=" + mCurrentDeviceAddr
                    + " bdaddr=" + bdaddr);
        }

        if (bdaddr == null || bdaddr.equals(mCurrentDeviceAddr)) {
            return;
        }

        // Store the previous volume if a device was active.
        if (!mCurrentDeviceAddr.isEmpty()) {
            storeVolume();
        }

        // Set the current volume device to the new device.
        mCurrentDeviceAddr = bdaddr;

        // No new active device.
        if (bdaddr.isEmpty()) {
            return;
        }

        // 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, "Device isn't connected: " + bdaddr);
            return;
        }

        switchVolumeDevice(bdaddr);
    }

    void switchVolumeDevice(String bdaddr) {
        // Inform the audio manager that the device has changed
        mAudioManager.avrcpSupportsAbsoluteVolume(bdaddr, mDeviceMap.get(bdaddr));

        // Get the current system volume and try to get the preference volume
        int currVolume = mAudioManager.getStreamVolume(STREAM_MUSIC);
        int savedVolume = getVolume(bdaddr, 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, "activeDeviceChanged: currVolume=" + currVolume
                    + " savedVolume=" + savedVolume);
        }
        if (savedVolume != currVolume) {
            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)) {
            int avrcpVolume = systemToAvrcpVolume(savedVolume);
            Log.e(TAG, "activeDeviceChanged: Updating device volume: avrcpVolume=" + avrcpVolume);
            mNativeInterface.sendVolumeChanged(avrcpVolume);
        }
    }

    void deviceDisconnected(String bdaddr) {
        Log.e(TAG, "deviceDisconnected: bdaddr=" + bdaddr);
        mDeviceMap.remove(bdaddr);
    }

    public void dump(StringBuilder sb) {
        sb.append("Bluetooth Device Volume Map:\n");
        Map<String, ?> allKeys = getVolumeMap().getAll();
        for (Map.Entry<String, ?> entry : allKeys.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if (value instanceof Integer) {
                sb.append("    " + key + " - " + (Integer) value + "\n");
                mVolumeMap.put(key, (Integer) value);
            }
        }
    }
}