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

Commit cd7925fa authored by Hyundo Moon's avatar Hyundo Moon
Browse files

Suppress noisy intent when a fallback device exist (A2DP)

This CL suppress noisy intent when both of conditions met:
- Currently active A2DP device is disconnected.
- There is a fallback device.

The music will keep playing in this case.

Bug: 254569327
Test: 1) atest A2dpServiceTest ActiveDeviceManagerTest \
               A2dpServiceBinderTest;
      2) Manually tested disconnecting A2DP while another
         LE (or A2DP) headset is connected. Music kept playing via
         the remaining BT device.
      3) Manually tested selecting phone speaker while playing music
         through A2dp headset. Music kept playing.
Change-Id: Ifcb3fc27a3ecd7fd47bdbc5a657dba2105ae749e
parent c5a5191c
Loading
Loading
Loading
Loading
+31 −39
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission;
import static com.android.bluetooth.Utils.enforceCdmAssociation;
import static com.android.bluetooth.Utils.hasBluetoothPrivilegedPermission;

import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.app.compat.CompatChanges;
import android.bluetooth.BluetoothA2dp;
@@ -90,7 +91,8 @@ public class A2dpService extends ProfileService {
    A2dpNativeInterface mA2dpNativeInterface;
    @VisibleForTesting
    ServiceFactory mFactory = new ServiceFactory();
    private AudioManager mAudioManager;
    @VisibleForTesting
    AudioManager mAudioManager;
    private A2dpCodecConfig mA2dpCodecConfig;
    private CompanionDeviceManager mCompanionDeviceManager;

@@ -99,7 +101,7 @@ public class A2dpService extends ProfileService {
    private final ConcurrentMap<BluetoothDevice, A2dpStateMachine> mStateMachines =
            new ConcurrentHashMap<>();

    // Protect setActiveDevice() so all invoked is handled squentially
    // Protect setActiveDevice()/removeActiveDevice() so all invoked is handled sequentially
    private final Object mActiveSwitchingGuard = new Object();

    // Timeout for state machine thread join, to prevent potential ANR.
@@ -186,7 +188,7 @@ public class A2dpService extends ProfileService {
        mAdapterService.notifyActivityAttributionInfo(getAttributionSource(), deviceAddress);

        // Step 9: Clear active device
        setActiveDevice(null);
        removeActiveDevice(false);

        return true;
    }
@@ -487,38 +489,25 @@ public class A2dpService extends ProfileService {
        }
    }

    private void removeActiveDevice(boolean forceStopPlayingAudio) {
    /**
     * Removes the current active device.
     *
     * @param stopAudio whether the current media playback should be stopped.
     * @return true on success, otherwise false
     */
    public boolean removeActiveDevice(boolean stopAudio) {
        synchronized (mActiveSwitchingGuard) {
            BluetoothDevice previousActiveDevice = null;
            synchronized (mStateMachines) {
                if (mActiveDevice == null) return;
                if (mActiveDevice == null) return true;
                previousActiveDevice = mActiveDevice;
            }

            int prevActiveConnectionState = getConnectionState(previousActiveDevice);

            // As per b/202602952, if we remove the active device due to a disconnection,
            // we need to check if another device is connected and set it active instead.
            // Calling this before any other active related calls has the same effect as
            // a classic active device switch.
            BluetoothDevice fallbackdevice = getFallbackDevice();
            if (fallbackdevice != null && prevActiveConnectionState
                    != BluetoothProfile.STATE_CONNECTED) {
                setActiveDevice(fallbackdevice);
                return;
            }

            // This needs to happen before we inform the audio manager that the device
            // disconnected. Please see comment in updateAndBroadcastActiveDevice() for why.
            updateAndBroadcastActiveDevice(null);

            // Make sure the Audio Manager knows the previous Active device is disconnected.
            // However, if A2DP is still connected and not forcing stop audio for that remote
            // device, the user has explicitly switched the output to the local device and music
            // should continue playing. Otherwise, the remote device has been indeed disconnected
            // and audio should be suspended before switching the output to the local device.
            boolean stopAudio = forceStopPlayingAudio || (prevActiveConnectionState
                        != BluetoothProfile.STATE_CONNECTED);
            // Make sure the Audio Manager knows the previous active device is no longer active.
            mAudioManager.handleBluetoothActiveDeviceChanged(null, previousActiveDevice,
                    BluetoothProfileConnectionInfo.createA2dpInfo(!stopAudio, -1));

@@ -527,9 +516,11 @@ public class A2dpService extends ProfileService {
                if (!mA2dpNativeInterface.setActiveDevice(null)) {
                    Log.w(TAG, "setActiveDevice(null): Cannot remove active device in native "
                            + "layer");
                    return false;
                }
            }
        }
        return true;
    }

    /**
@@ -540,7 +531,7 @@ public class A2dpService extends ProfileService {
     * @return true on success, false on error
     */
    @VisibleForTesting
    public boolean setSilenceMode(BluetoothDevice device, boolean silence) {
    public boolean setSilenceMode(@NonNull BluetoothDevice device, boolean silence) {
        if (DBG) {
            Log.d(TAG, "setSilenceMode(" + device + "): " + silence);
        }
@@ -560,17 +551,16 @@ public class A2dpService extends ProfileService {
    /**
     * Set the active device.
     *
     * @param device the active device
     * @param device the active device. Should not be null.
     * @return true on success, otherwise false
     */
    public boolean setActiveDevice(BluetoothDevice device) {
        synchronized (mActiveSwitchingGuard) {
    public boolean setActiveDevice(@NonNull BluetoothDevice device) {
        if (device == null) {
                // Remove active device and continue playing audio only if necessary.
                removeActiveDevice(false);
                return true;
            Log.e(TAG, "device should not be null!");
            return false;
        }

        synchronized (mActiveSwitchingGuard) {
            A2dpStateMachine sm = null;
            BluetoothDevice previousActiveDevice = null;
            synchronized (mStateMachines) {
@@ -1256,10 +1246,8 @@ public class A2dpService extends ProfileService {
        if (toState == BluetoothProfile.STATE_CONNECTED && (mMaxConnectedAudioDevices == 1)) {
            setActiveDevice(device);
        }
        // Check if the active device is not connected anymore
        if (isActiveDevice(device) && (fromState == BluetoothProfile.STATE_CONNECTED)) {
            setActiveDevice(null);
        }
        // When disconnected, ActiveDeviceManager will call setActiveDevice(null)

        // Check if the device is disconnected - if unbond, remove the state machine
        if (toState == BluetoothProfile.STATE_DISCONNECTED) {
            if (mAdapterService.getBondState(device) == BluetoothDevice.BOND_NONE) {
@@ -1393,8 +1381,12 @@ public class A2dpService extends ProfileService {
                A2dpService service = getService(source);
                boolean result = false;
                if (service != null) {
                    if (device == null) {
                        result = service.removeActiveDevice(false);
                    } else {
                        result = service.setActiveDevice(device);
                    }
                }
                receiver.send(result);
            } catch (RuntimeException e) {
                receiver.propagateException(e);
+3 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.bluetooth.avrcp;

import android.annotation.NonNull;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
@@ -292,7 +293,7 @@ public class AvrcpTargetService extends ProfileService {
        return service.getActiveDevice();
    }

    private void setA2dpActiveDevice(BluetoothDevice device) {
    private void setA2dpActiveDevice(@NonNull BluetoothDevice device) {
        A2dpService service = A2dpService.getA2dpService();
        if (service == null) {
            Log.d(TAG, "setA2dpActiveDevice: A2dp service not found");
@@ -459,6 +460,7 @@ public class AvrcpTargetService extends ProfileService {
        Log.i(TAG, "setActiveDevice: device=" + device);
        if (device == null) {
            Log.wtf(TAG, "setActiveDevice: could not find device " + device);
            return;
        }
        setA2dpActiveDevice(device);
    }
+56 −29
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.bluetooth.btservice;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothA2dp;
@@ -307,7 +309,7 @@ class ActiveDeviceManager {
            mHearingAidConnectedDevices.add(device);
            // New connected device: select it as active
            setHearingAidActiveDevice(device);
            setA2dpActiveDevice(null);
            setA2dpActiveDevice(null, true);
            setHfpActiveDevice(null);
            setLeAudioActiveDevice(null);
        }
@@ -327,12 +329,12 @@ class ActiveDeviceManager {
                    && mPendingLeHearingAidActiveDevice.isEmpty()) {
                // New connected device: select it as active
                setLeAudioActiveDevice(device);
                setA2dpActiveDevice(null);
                setA2dpActiveDevice(null, true);
                setHfpActiveDevice(null);
            } else if (mPendingLeHearingAidActiveDevice.contains(device)) {
                setLeHearingAidActiveDevice(device);
                setHearingAidActiveDevice(null);
                setA2dpActiveDevice(null);
                setA2dpActiveDevice(null, true);
                setHfpActiveDevice(null);
            }
        }
@@ -355,7 +357,7 @@ class ActiveDeviceManager {
                // New connected device: select it as active
                setLeHearingAidActiveDevice(device);
                setHearingAidActiveDevice(null);
                setA2dpActiveDevice(null);
                setA2dpActiveDevice(null, true);
                setHfpActiveDevice(null);
            }
        }
@@ -368,10 +370,9 @@ class ActiveDeviceManager {
            }
            mA2dpConnectedDevices.remove(device);
            if (Objects.equals(mA2dpActiveDevice, device)) {
                if (mA2dpConnectedDevices.isEmpty()) {
                    setA2dpActiveDevice(null);
                if (!setFallbackDeviceActiveLocked()) {
                    setA2dpActiveDevice(null, false);
                }
                setFallbackDeviceActiveLocked();
            }
        }
    }
@@ -487,7 +488,7 @@ class ActiveDeviceManager {
                }
            }
            if (device != null) {
                setA2dpActiveDevice(null);
                setA2dpActiveDevice(null, true);
                setHfpActiveDevice(null);
                setLeAudioActiveDevice(null);
            }
@@ -504,7 +505,7 @@ class ActiveDeviceManager {
            }
            // Just assign locally the new value
            if (device != null && !Objects.equals(mLeAudioActiveDevice, device)) {
                setA2dpActiveDevice(null);
                setA2dpActiveDevice(null, true);
                setHfpActiveDevice(null);
                setHearingAidActiveDevice(null);
            }
@@ -522,7 +523,7 @@ class ActiveDeviceManager {
            }
            // Just assign locally the new value
            if (device != null && !Objects.equals(mLeHearingAidActiveDevice, device)) {
                setA2dpActiveDevice(null);
                setA2dpActiveDevice(null, true);
                setHfpActiveDevice(null);
                setHearingAidActiveDevice(null);
            }
@@ -633,18 +634,33 @@ class ActiveDeviceManager {
        return mHandlerThread.getLooper();
    }

    private void setA2dpActiveDevice(BluetoothDevice device) {
        synchronized (mLock) {
    private void setA2dpActiveDevice(@NonNull BluetoothDevice device) {
        setA2dpActiveDevice(device, false);
    }

    private void setA2dpActiveDevice(@Nullable BluetoothDevice device, boolean hasFallbackDevice) {
        if (DBG) {
                Log.d(TAG, "setA2dpActiveDevice(" + device + ")");
            Log.d(TAG, "setA2dpActiveDevice(" + device + ")"
                    + (device == null ? " hasFallbackDevice=" + hasFallbackDevice : ""));
        }

        final A2dpService a2dpService = mFactory.getA2dpService();
        if (a2dpService == null) {
            return;
        }
            if (!a2dpService.setActiveDevice(device)) {

        boolean success = false;
        if (device == null) {
            success = a2dpService.removeActiveDevice(!hasFallbackDevice);
        } else {
            success = a2dpService.setActiveDevice(device);
        }

        if (!success) {
            return;
        }

        synchronized (mLock) {
            mA2dpActiveDevice = device;
        }
    }
@@ -733,13 +749,20 @@ class ActiveDeviceManager {
        }
    }

    private void setFallbackDeviceActiveLocked() {
    /**
     * TODO: This method can return true when a fallback device for an unrelated profile is found.
     *       Take disconnected profile as an argument, and find the exact fallback device.
     *       Also, split this method to smaller methods for better readability.
     *
     * @return true when the fallback device is activated, false otherwise
     */
    private boolean setFallbackDeviceActiveLocked() {
        if (DBG) {
            Log.d(TAG, "setFallbackDeviceActive");
        }
        DatabaseManager dbManager = mAdapterService.getDatabase();
        if (dbManager == null) {
            return;
            return false;
        }
        List<BluetoothDevice> connectedHearingAidDevices = new ArrayList<>();
        if (!mHearingAidConnectedDevices.isEmpty()) {
@@ -757,7 +780,7 @@ class ActiveDeviceManager {
                        Log.d(TAG, "set hearing aid device active: " + device);
                    }
                    setHearingAidActiveDevice(device);
                    setA2dpActiveDevice(null);
                    setA2dpActiveDevice(null, true);
                    setHfpActiveDevice(null);
                    setLeAudioActiveDevice(null);
                } else {
@@ -766,10 +789,10 @@ class ActiveDeviceManager {
                    }
                    setLeHearingAidActiveDevice(device);
                    setHearingAidActiveDevice(null);
                    setA2dpActiveDevice(null);
                    setA2dpActiveDevice(null, true);
                    setHfpActiveDevice(null);
                }
                return;
                return true;
            }
        }

@@ -820,7 +843,7 @@ class ActiveDeviceManager {
                        Log.d(TAG, "set LE audio device active: " + device);
                    }
                    setLeAudioActiveDevice(device);
                    setA2dpActiveDevice(null);
                    setA2dpActiveDevice(null, true);
                    setHfpActiveDevice(null);
                }
            } else {
@@ -838,11 +861,15 @@ class ActiveDeviceManager {
                        Log.d(TAG, "set LE audio device active: " + device);
                    }
                    setLeAudioActiveDevice(device);
                    setA2dpActiveDevice(null);
                    setA2dpActiveDevice(null, true);
                    setHfpActiveDevice(null);
                }
            }
            return true;
        }

        // No fallback device is found.
        return false;
    }

    private void resetState() {
@@ -908,7 +935,7 @@ class ActiveDeviceManager {
        if (DBG) {
            Log.d(TAG, "wiredAudioDeviceConnected");
        }
        setA2dpActiveDevice(null);
        setA2dpActiveDevice(null, true);
        setHfpActiveDevice(null);
        setHearingAidActiveDevice(null);
        setLeAudioActiveDevice(null);
+5 −1
Original line number Diff line number Diff line
@@ -5185,8 +5185,12 @@ public class AdapterService extends Service {
                || mA2dpService.getConnectionPolicy(device)
                == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
            Log.i(TAG, "setActiveDevice: Setting active A2dp device " + device);
            if (device == null) {
                mA2dpService.removeActiveDevice(false);
            } else {
                mA2dpService.setActiveDevice(device);
            }
        }

        if (mHearingAidService != null && (device == null
                || mHearingAidService.getConnectionPolicy(device)
+2 −3
Original line number Diff line number Diff line
@@ -25,8 +25,6 @@ import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_READ;
import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE;
import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE;

import static java.util.Map.entry;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.bluetooth.BluetoothAdapter;
@@ -937,9 +935,10 @@ public class MediaControlGattService implements MediaControlGattServiceInterface
                    + " request up");
        }

        // TODO: Activate/deactivate devices with ActiveDeviceManager
        if (req.getOpcode() == Request.Opcodes.PLAY) {
            if (mAdapterService.getActiveDevices(BluetoothProfile.A2DP).size() > 0) {
                A2dpService.getA2dpService().setActiveDevice(null);
                A2dpService.getA2dpService().removeActiveDevice(false);
            }
            if (mAdapterService.getActiveDevices(BluetoothProfile.HEARING_AID).size() > 0) {
                HearingAidService.getHearingAidService().setActiveDevice(null);
Loading