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

Commit e74af084 authored by Hall Liu's avatar Hall Liu
Browse files

Add way for external bluetooth audio changes to notify CARSM

update onBluetoothStateChange and BluetoothManager to be more aware of
bluetooth changes coming from external sources. Also add tests for
BluetoothManager.

Bug: 27133607
Change-Id: I10790a9cfad329574c8e9dd4144127020eea65e4
parent f0237f21
Loading
Loading
Loading
Loading
+37 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.server.telecom;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothProfile;
import android.content.Context;

/**
 * Proxy class used so that BluetoothAdapter can be mocked for testing.
 */
public class BluetoothAdapterProxy {
    private BluetoothAdapter mBluetoothAdapter;

    public BluetoothAdapterProxy() {
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    }

    public boolean getProfileProxy(Context context, BluetoothProfile.ServiceListener listener,
            int profile) {
        return mBluetoothAdapter.getProfileProxy(context, listener, profile);
    }
}
+23 −0
Original line number Diff line number Diff line
@@ -16,8 +16,11 @@

package com.android.server.telecom;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;

import java.util.List;

/**
 * A proxy class that facilitates testing of the BluetoothPhoneServiceImpl class.
 *
@@ -44,4 +47,24 @@ public class BluetoothHeadsetProxy {

        mBluetoothHeadset.phoneStateChanged(numActive, numHeld, callState, number, type);
    }

    public List<BluetoothDevice> getConnectedDevices() {
        return mBluetoothHeadset.getConnectedDevices();
    }

    public int getConnectionState(BluetoothDevice device) {
        return mBluetoothHeadset.getConnectionState(device);
    }

    public boolean isAudioConnected(BluetoothDevice device) {
        return mBluetoothHeadset.isAudioConnected(device);
    }

    public boolean connectAudio() {
        return mBluetoothHeadset.connectAudio();
    }

    public boolean disconnectAudio() {
        return mBluetoothHeadset.disconnectAudio();
    }
}
 No newline at end of file
+72 −24
Original line number Diff line number Diff line
@@ -32,14 +32,21 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;

import java.util.List;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * Listens to and caches bluetooth headset state.  Used By the CallAudioManager for maintaining
 * overall audio state. Also provides method for connecting the bluetooth headset to the phone call.
 */
public class BluetoothManager {
    public static final int BLUETOOTH_UNINITIALIZED = 0;
    public static final int BLUETOOTH_DISCONNECTED = 1;
    public static final int BLUETOOTH_DEVICE_CONNECTED = 2;
    public static final int BLUETOOTH_AUDIO_PENDING = 3;
    public static final int BLUETOOTH_AUDIO_CONNECTED = 4;

    public interface BluetoothStateListener {
        void onBluetoothStateChange(BluetoothManager bluetoothManager);
        void onBluetoothStateChange(int oldState, int newState);
    }

    private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
@@ -48,9 +55,14 @@ public class BluetoothManager {
                public void onServiceConnected(int profile, BluetoothProfile proxy) {
                    Log.startSession("BMSL.oSC");
                    try {
                        mBluetoothHeadset = (BluetoothHeadset) proxy;
                        if (profile == BluetoothProfile.HEADSET) {
                            mBluetoothHeadset = new BluetoothHeadsetProxy((BluetoothHeadset) proxy);
                            Log.v(this, "- Got BluetoothHeadset: " + mBluetoothHeadset);
                        updateBluetoothState();
                        } else {
                            Log.w(this, "Connected to non-headset bluetooth service. Not changing" +
                                    " bluetooth headset.");
                        }
                        updateListenerOfBluetoothState(true);
                    } finally {
                        Log.endSession();
                    }
@@ -62,7 +74,7 @@ public class BluetoothManager {
                    try {
                        mBluetoothHeadset = null;
                        Log.v(this, "Lost BluetoothHeadset: " + mBluetoothHeadset);
                        updateBluetoothState();
                        updateListenerOfBluetoothState(false);
                    } finally {
                        Log.endSession();
                    }
@@ -84,14 +96,16 @@ public class BluetoothManager {
                            BluetoothHeadset.STATE_DISCONNECTED);
                    Log.d(this, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
                    Log.d(this, "==> new state: %s ", bluetoothHeadsetState);
                    updateBluetoothState();
                    updateListenerOfBluetoothState(
                            bluetoothHeadsetState == BluetoothHeadset.STATE_CONNECTING);
                } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
                    int bluetoothHeadsetAudioState =
                            intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
                                    BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
                    Log.d(this, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
                    Log.d(this, "==> new state: %s", bluetoothHeadsetAudioState);
                    updateBluetoothState();
                    updateListenerOfBluetoothState(
                            bluetoothHeadsetAudioState == BluetoothHeadset.STATE_AUDIO_CONNECTING);
                }
            } finally {
                Log.endSession();
@@ -101,11 +115,10 @@ public class BluetoothManager {

    private final Handler mHandler = new Handler(Looper.getMainLooper());

    private final BluetoothAdapter mBluetoothAdapter;
    private final BluetoothAdapterProxy mBluetoothAdapter;
    private BluetoothStateListener mBluetoothStateListener;

    private BluetoothHeadset mBluetoothHeadset;
    private boolean mBluetoothConnectionPending = false;
    private BluetoothHeadsetProxy mBluetoothHeadset;
    private long mBluetoothConnectionRequestTime;
    private final Runnable mBluetoothConnectionTimeout = new Runnable("BM.cBA") {
        @Override
@@ -114,15 +127,14 @@ public class BluetoothManager {
                Log.v(this, "Bluetooth audio inexplicably disconnected within 5 seconds of " +
                        "connection. Updating UI.");
            }
            mBluetoothConnectionPending = false;
            updateBluetoothState();
            updateListenerOfBluetoothState(false);
        }
    };
    private final Context mContext;
    private int mBluetoothState = BLUETOOTH_UNINITIALIZED;


    public BluetoothManager(Context context) {
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    public BluetoothManager(Context context, BluetoothAdapterProxy bluetoothAdapterProxy) {
        mBluetoothAdapter = bluetoothAdapterProxy;
        mContext = context;

        if (mBluetoothAdapter != null) {
@@ -200,7 +212,8 @@ public class BluetoothManager {
    /**
     * @return true if a BT Headset is available, and its audio is currently connected.
     */
    boolean isBluetoothAudioConnected() {
    @VisibleForTesting
    public boolean isBluetoothAudioConnected() {
        if (mBluetoothHeadset == null) {
            Log.v(this, "isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
            return false;
@@ -241,7 +254,7 @@ public class BluetoothManager {
        // If we issued a connectAudio() call "recently enough", even
        // if BT isn't actually connected yet, let's still pretend BT is
        // on.  This makes the onscreen indication more responsive.
        if (mBluetoothConnectionPending) {
        if (isBluetoothAudioPending()) {
            long timeSinceRequest =
                    SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
            Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
@@ -253,11 +266,28 @@ public class BluetoothManager {
        return false;
    }

    private boolean isBluetoothAudioPending() {
        return mBluetoothState == BLUETOOTH_AUDIO_PENDING;
    }

    /**
     * Notified audio manager of a change to the bluetooth state.
     */
    void updateBluetoothState() {
        mBluetoothStateListener.onBluetoothStateChange(this);
    private void updateListenerOfBluetoothState(boolean canBePending) {
        int newState;
        if (isBluetoothAudioConnected()) {
            newState = BLUETOOTH_AUDIO_CONNECTED;
        } else if (canBePending && isBluetoothAudioPending()) {
            newState = BLUETOOTH_AUDIO_PENDING;
        } else if (isBluetoothAvailable()) {
            newState = BLUETOOTH_DEVICE_CONNECTED;
        } else {
            newState = BLUETOOTH_DISCONNECTED;
        }
        if (mBluetoothState != newState) {
            mBluetoothStateListener.onBluetoothStateChange(mBluetoothState, newState);
            mBluetoothState = newState;
        }
    }

    @VisibleForTesting
@@ -269,10 +299,8 @@ public class BluetoothManager {

        // Watch out: The bluetooth connection doesn't happen instantly;
        // the connectAudio() call returns instantly but does its real
        // work in another thread.  The mBluetoothConnectionPending flag
        // is just a little trickery to ensure that the onscreen UI updates
        // instantly. (See isBluetoothAudioConnectedOrPending() above.)
        mBluetoothConnectionPending = true;
        // work in another thread.
        mBluetoothState = BLUETOOTH_AUDIO_PENDING;
        mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
        mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel());
        mBluetoothConnectionTimeout.cancel();
@@ -286,11 +314,13 @@ public class BluetoothManager {
    public void disconnectBluetoothAudio() {
        Log.v(this, "disconnectBluetoothAudio()...");
        if (mBluetoothHeadset != null) {
            mBluetoothState = BLUETOOTH_DEVICE_CONNECTED;
            mBluetoothHeadset.disconnectAudio();
        } else {
            mBluetoothState = BLUETOOTH_DISCONNECTED;
        }
        mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel());
        mBluetoothConnectionTimeout.cancel();
        mBluetoothConnectionPending = false;
    }

    /**
@@ -322,4 +352,22 @@ public class BluetoothManager {
            pw.println("mBluetoothAdapter is null; device is not BT capable");
        }
    }

    /**
     * Set the bluetooth headset proxy for testing purposes.
     * @param bluetoothHeadsetProxy
     */
    @VisibleForTesting
    public void setBluetoothHeadsetForTesting(BluetoothHeadsetProxy bluetoothHeadsetProxy) {
        mBluetoothHeadset = bluetoothHeadsetProxy;
    }

    /**
     * Set mBluetoothState for testing.
     * @param state
     */
    @VisibleForTesting
    public void setInternalBluetoothState(int state) {
        mBluetoothState = state;
    }
}
+43 −13
Original line number Diff line number Diff line
@@ -20,8 +20,9 @@ package com.android.server.telecom;
 * A class that acts as a listener to things that could change call audio routing, namely
 * bluetooth status, wired headset status, and dock status.
 */
public class CallAudioRoutePeripheralAdapter implements BluetoothManager.BluetoothStateListener,
        WiredHeadsetManager.Listener, DockManager.Listener {
public class CallAudioRoutePeripheralAdapter implements WiredHeadsetManager.Listener,
        DockManager.Listener, BluetoothManager.BluetoothStateListener {

    private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
    private final BluetoothManager mBluetoothManager;

@@ -38,21 +39,50 @@ public class CallAudioRoutePeripheralAdapter implements BluetoothManager.Bluetoo
        dockManager.addListener(this);
    }

    public boolean isBluetoothAudioOn() {
        return mBluetoothManager.isBluetoothAudioConnected();
    }

    @Override
    public void onBluetoothStateChange(BluetoothManager bluetoothManager) {
        if (bluetoothManager.isBluetoothAvailable()) {
    public void onBluetoothStateChange(int oldState, int newState) {
        switch (oldState) {
            case BluetoothManager.BLUETOOTH_DISCONNECTED:
            case BluetoothManager.BLUETOOTH_UNINITIALIZED:
                switch (newState) {
                    case BluetoothManager.BLUETOOTH_DEVICE_CONNECTED:
                    case BluetoothManager.BLUETOOTH_AUDIO_CONNECTED:
                        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
                                CallAudioRouteStateMachine.CONNECT_BLUETOOTH);
        } else {
                        break;
                }
                break;
            case BluetoothManager.BLUETOOTH_DEVICE_CONNECTED:
                switch (newState) {
                    case BluetoothManager.BLUETOOTH_DISCONNECTED:
                        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
                                CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH);
                        break;
                    case BluetoothManager.BLUETOOTH_AUDIO_CONNECTED:
                        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
                                CallAudioRouteStateMachine.SWITCH_BLUETOOTH);
                        break;
                }
                break;
            case BluetoothManager.BLUETOOTH_AUDIO_CONNECTED:
            case BluetoothManager.BLUETOOTH_AUDIO_PENDING:
                switch (newState) {
                    case BluetoothManager.BLUETOOTH_DISCONNECTED:
                        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
                                CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH);
                        break;
                    case BluetoothManager.BLUETOOTH_DEVICE_CONNECTED:
                        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
                                CallAudioRouteStateMachine.BT_AUDIO_DISCONNECT);
                        break;
                }
                break;
        }

    public boolean isBluetoothAudioOn() {
        return mBluetoothManager.isBluetoothAudioConnected();
    }

    /**
      * Updates the audio route when the headset plugged in state changes. For example, if audio is
      * being routed over speakerphone and a headset is plugged in then switch to wired headset.
+28 −19
Original line number Diff line number Diff line
@@ -84,6 +84,7 @@ public class CallAudioRouteStateMachine extends StateMachine {
    public static final int SWITCH_SPEAKER = 1004;
    // Wired headset, earpiece, or speakerphone, in that order of precedence.
    public static final int SWITCH_BASELINE_ROUTE = 1005;
    public static final int BT_AUDIO_DISCONNECT = 1006;

    public static final int USER_SWITCH_EARPIECE = 1101;
    public static final int USER_SWITCH_BLUETOOTH = 1102;
@@ -119,6 +120,7 @@ public class CallAudioRouteStateMachine extends StateMachine {
        put(SWITCH_HEADSET, "SWITCH_HEADSET");
        put(SWITCH_SPEAKER, "SWITCH_SPEAKER");
        put(SWITCH_BASELINE_ROUTE, "SWITCH_BASELINE_ROUTE");
        put(BT_AUDIO_DISCONNECT, "BT_AUDIO_DISCONNECT");

        put(USER_SWITCH_EARPIECE, "USER_SWITCH_EARPIECE");
        put(USER_SWITCH_BLUETOOTH, "USER_SWITCH_BLUETOOTH");
@@ -187,17 +189,10 @@ public class CallAudioRouteStateMachine extends StateMachine {
                    mAvailableRoutes |= ROUTE_WIRED_HEADSET;
                    return NOT_HANDLED;
                case CONNECT_BLUETOOTH:
                    // This case is here because the bluetooth manager sends out a lot of spurious
                    // state changes, and no layers above this one can tell which are actual changes
                    // in connection/disconnection status. This filters it out.
                    if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
                        return HANDLED; // Do nothing if we already have bluetooth as enabled.
                    } else {
                    Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
                            "Bluetooth connected");
                    mAvailableRoutes |= ROUTE_BLUETOOTH;
                    return NOT_HANDLED;
                    }
                case DISCONNECT_WIRED_HEADSET:
                    Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
                            "Wired headset disconnected");
@@ -207,14 +202,10 @@ public class CallAudioRouteStateMachine extends StateMachine {
                    }
                    return NOT_HANDLED;
                case DISCONNECT_BLUETOOTH:
                    if ((mAvailableRoutes & ROUTE_BLUETOOTH) == 0) {
                        return HANDLED;
                    } else {
                    Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
                            "Bluetooth disconnected");
                    mAvailableRoutes &= ~ROUTE_BLUETOOTH;
                    return NOT_HANDLED;
                    }
                case SWITCH_BASELINE_ROUTE:
                    sendInternalMessage(calculateBaselineRouteMessage(false));
                    return HANDLED;
@@ -382,6 +373,8 @@ public class CallAudioRouteStateMachine extends StateMachine {
                    if (!mHasUserExplicitlyLeftBluetooth) {
                        sendInternalMessage(SWITCH_BLUETOOTH);
                    } else {
                        Log.i(this, "Not switching to BT route from earpiece because user has " +
                                "explicitly disconnected.");
                        updateSystemAudioState();
                    }
                    return HANDLED;
@@ -395,6 +388,9 @@ public class CallAudioRouteStateMachine extends StateMachine {
                            "earpiece");
                    updateSystemAudioState();
                    return HANDLED;
                case BT_AUDIO_DISCONNECT:
                    // This may be sent as a confirmation by the BT stack after switch off BT.
                    return HANDLED;
                case CONNECT_DOCK:
                    sendInternalMessage(SWITCH_SPEAKER);
                    return HANDLED;
@@ -553,6 +549,8 @@ public class CallAudioRouteStateMachine extends StateMachine {
                    if (!mHasUserExplicitlyLeftBluetooth) {
                        sendInternalMessage(SWITCH_BLUETOOTH);
                    } else {
                        Log.i(this, "Not switching to BT route from headset because user has " +
                                "explicitly disconnected.");
                        updateSystemAudioState();
                    }
                    return HANDLED;
@@ -567,6 +565,9 @@ public class CallAudioRouteStateMachine extends StateMachine {
                        sendInternalMessage(SWITCH_BASELINE_ROUTE);
                    }
                    return HANDLED;
                case BT_AUDIO_DISCONNECT:
                    // This may be sent as a confirmation by the BT stack after switch off BT.
                    return HANDLED;
                case CONNECT_DOCK:
                    // Nothing to do here
                    return HANDLED;
@@ -733,6 +734,9 @@ public class CallAudioRouteStateMachine extends StateMachine {
                    sendInternalMessage(SWITCH_BASELINE_ROUTE);
                    mWasOnSpeaker = false;
                    return HANDLED;
                case BT_AUDIO_DISCONNECT:
                    sendInternalMessage(SWITCH_BASELINE_ROUTE);
                    return HANDLED;
                case DISCONNECT_WIRED_HEADSET:
                    updateSystemAudioState();
                    // No change in audio route required
@@ -903,6 +907,8 @@ public class CallAudioRouteStateMachine extends StateMachine {
                    if (!mHasUserExplicitlyLeftBluetooth) {
                        sendInternalMessage(SWITCH_BLUETOOTH);
                    } else {
                        Log.i(this, "Not switching to BT route from speaker because user has " +
                                "explicitly disconnected.");
                        updateSystemAudioState();
                    }
                    return HANDLED;
@@ -914,6 +920,9 @@ public class CallAudioRouteStateMachine extends StateMachine {
                    updateSystemAudioState();
                    // No change in audio route required
                    return HANDLED;
                case BT_AUDIO_DISCONNECT:
                    // This may be sent as a confirmation by the BT stack after switch off BT.
                    return HANDLED;
                case CONNECT_DOCK:
                    // Nothing to do here
                    return HANDLED;
Loading