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

Commit ea80be8b authored by Leslie Watkins's avatar Leslie Watkins Committed by Gerrit Code Review
Browse files

Merge "Track SCO state in HfpClientDeviceBlock, and set SCO state of both...

Merge "Track SCO state in HfpClientDeviceBlock, and set SCO state of both Connections and Conferences whenever they're created, and when onAudioStateChanged is invoked."
parents a8440fe3 092c4ed0
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -65,7 +65,7 @@ public abstract class ProfileService extends Service {
        return getClass().getSimpleName();
    }

    protected boolean isAvailable() {
    public boolean isAvailable() {
        return mProfileStarted;
    }

+11 −2
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService;
import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.HashMap;
@@ -499,7 +500,9 @@ public class HeadsetClientService extends ProfileService {
        return sHeadsetClientService;
    }

    private static synchronized void setHeadsetClientService(HeadsetClientService instance) {
    /** Set a {@link HeadsetClientService} instance. */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public static synchronized void setHeadsetClientService(HeadsetClientService instance) {
        if (DBG) {
            Log.d(TAG, "setHeadsetClientService(): set to: " + instance);
        }
@@ -677,7 +680,13 @@ public class HeadsetClientService extends ProfileService {
        return true;
    }

    int getAudioState(BluetoothDevice device) {
    /**
     * Gets audio state of the connection with {@code device}.
     *
     * <p>Can be one of {@link STATE_AUDIO_CONNECTED}, {@link STATE_AUDIO_CONNECTING}, or
     * {@link STATE_AUDIO_DISCONNECTED}.
     */
    public int getAudioState(BluetoothDevice device) {
        HeadsetClientStateMachine sm = getStateMachine(device);
        if (sm == null) {
            Log.e(TAG, "Cannot allocate SM for device " + device);
+0 −15
Original line number Diff line number Diff line
@@ -34,7 +34,6 @@ public class HfpClientConnection extends Connection {
    private static final String TAG = "HfpClientConnection";
    private static final boolean DBG = false;

    private static final String KEY_SCO_STATE = "com.android.bluetooth.hfpclient.SCO_STATE";
    private static final String EVENT_SCO_CONNECT = "com.android.bluetooth.hfpclient.SCO_CONNECT";
    private static final String EVENT_SCO_DISCONNECT =
             "com.android.bluetooth.hfpclient.SCO_DISCONNECT";
@@ -65,7 +64,6 @@ public class HfpClientConnection extends Connection {
        }

        mCurrentCall = call;
        setScoState(BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED);
        handleCallChanged();
        finishInitializing();
    }
@@ -300,19 +298,6 @@ public class HfpClientConnection extends Connection {
        }
    }

    /**
     * Notify this connection of changes in the SCO state so we can update our call details
     */
    public void onScoStateChanged(int newState, int oldState) {
        setScoState(newState);
    }

    private void setScoState(int state) {
        Bundle bundle = new Bundle();
        bundle.putInt(KEY_SCO_STATE, state);
        setExtras(bundle);
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof HfpClientConnection)) {
+35 −4
Original line number Diff line number Diff line
@@ -27,19 +27,23 @@ import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;
import android.util.Log;

import com.android.bluetooth.hfpclient.HeadsetClientService;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

// Helper class that manages the call handling for one device. HfpClientConnectionService holdes a
// Helper class that manages the call handling for one device. HfpClientConnectionService holds a
// list of such blocks and routes traffic from the UI.
//
// Lifecycle of a Device Block is managed entirely by the Service which creates it. In essence it
// has only the active state otherwise the block should be GCed.
public class HfpClientDeviceBlock {
    private final String mTAG;
    private static final String KEY_SCO_STATE = "com.android.bluetooth.hfpclient.SCO_STATE";
    private static final boolean DBG = false;

    private final String mTAG;
    private final Context mContext;
    private final BluetoothDevice mDevice;
    private final PhoneAccount mPhoneAccount;
@@ -47,7 +51,7 @@ public class HfpClientDeviceBlock {
    private final TelecomManager mTelecomManager;
    private final HfpClientConnectionService mConnServ;
    private HfpClientConference mConference;

    private Bundle mScoState;
    private BluetoothHeadsetClientProxy mHeadsetProfile;

    HfpClientDeviceBlock(HfpClientConnectionService connServ, BluetoothDevice device,
@@ -64,6 +68,10 @@ public class HfpClientDeviceBlock {
        mTelecomManager.enablePhoneAccount(mPhoneAccount.getAccountHandle(), true);
        mTelecomManager.setUserSelectedOutgoingPhoneAccount(mPhoneAccount.getAccountHandle());
        mHeadsetProfile = headsetProfile;
        mScoState = getScoStateFromDevice(device);
        if (DBG) {
            Log.d(mTAG, "SCO state = " + mScoState);
        }

        // Read the current calls and add them to telecom if already present
        if (mHeadsetProfile != null) {
@@ -110,8 +118,13 @@ public class HfpClientDeviceBlock {
        if (DBG) {
            Log.d(mTAG, "Call audio state changed " + oldState + " -> " + newState);
        }
        mScoState.putInt(KEY_SCO_STATE, newState);

        for (HfpClientConnection connection : mConnections.values()) {
            connection.onScoStateChanged(newState, oldState);
            connection.setExtras(mScoState);
        }
        if (mConference != null) {
            mConference.setExtras(mScoState);
        }
    }

@@ -132,6 +145,7 @@ public class HfpClientDeviceBlock {
        if (mConference == null) {
            mConference = new HfpClientConference(mPhoneAccount.getAccountHandle(), mDevice,
                    mHeadsetProfile);
            mConference.setExtras(mScoState);
        }

        if (connection1.getConference() == null) {
@@ -263,6 +277,10 @@ public class HfpClientDeviceBlock {
        } else {
            connection = new HfpClientConnection(mConnServ, mDevice, mHeadsetProfile, number);
        }
        connection.setExtras(mScoState);
        if (DBG) {
            Log.d(mTAG, "Connection extras = " + connection.getExtras().toString());
        }

        if (connection.getState() != Connection.STATE_DISCONNECTED) {
            mConnections.put(connection.getUUID(), connection);
@@ -301,6 +319,7 @@ public class HfpClientDeviceBlock {
                if (mConference == null) {
                    mConference = new HfpClientConference(mPhoneAccount.getAccountHandle(), mDevice,
                            mHeadsetProfile);
                    mConference.setExtras(mScoState);
                }
                if (mConference.addConnection(otherConn)) {
                    if (DBG) {
@@ -330,4 +349,16 @@ public class HfpClientDeviceBlock {
        }
    }

    private Bundle getScoStateFromDevice(BluetoothDevice device) {
        Bundle bundle = new Bundle();

        HeadsetClientService headsetClientService = HeadsetClientService.getHeadsetClientService();
        if (headsetClientService == null) {
            return bundle;
        }

        bundle.putInt(KEY_SCO_STATE, headsetClientService.getAudioState(device));

        return bundle;
    }
}
+154 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.hfpclient.connserv;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHeadsetClientCall;
import android.bluetooth.hfpclient.connserv.BluetoothHeadsetClientProxy;
import android.content.Context;
import android.content.res.Resources;
import android.net.Uri;
import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;

import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.bluetooth.hfpclient.HeadsetClientService;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@MediumTest
@RunWith(AndroidJUnit4.class)
public class HfpClientDeviceBlockTest {
    private static final String TEST_DEVICE_ADDRESS = "00:11:22:33:44:55";
    private static final String TEST_NUMBER = "000-111-2222";
    private static final String KEY_SCO_STATE = "com.android.bluetooth.hfpclient.SCO_STATE";

    @Mock
    private HeadsetClientService mHeadsetClientService;
    @Mock
    private HfpClientConnectionService mConnServ;
    @Mock
    private BluetoothHeadsetClientProxy mHeadsetProfile;
    @Mock
    private Context mApplicationContext;
    @Mock
    private Resources mResources;
    @Mock
    private TelecomManager mTelecomManager;

    private HfpClientDeviceBlock mHfpClientDeviceBlock;
    private BluetoothDevice mBluetoothDevice;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        // HfpClientConnectionService.createAccount is static and can't be mocked, so the
        // application context and resources must be mocked to avoid NPE when creating an
        // HfpClientDeviceBlock for testing.
        when(mResources.getBoolean(com.android.bluetooth.R.bool
                        .hfp_client_connection_service_support_emergency_call)).thenReturn(true);
        when(mApplicationContext.getResources()).thenReturn(mResources);
        when(mConnServ.getApplicationContext()).thenReturn(mApplicationContext);

        when(mConnServ.getSystemService(Context.TELECOM_SERVICE)).thenReturn(mTelecomManager);

        mBluetoothDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(
                TEST_DEVICE_ADDRESS);

        when(mHeadsetClientService.isAvailable()).thenReturn(true);
        HeadsetClientService.setHeadsetClientService(mHeadsetClientService);
    }

    @Test
    public void testCreateOutgoingConnection_scoStateIsSet() {
        setUpCall(new BluetoothHeadsetClientCall(mBluetoothDevice, /* id= */0,
                        BluetoothHeadsetClientCall.CALL_STATE_ACTIVE, TEST_NUMBER,
                /* multiParty= */false, /* outgoing= */false, /* inBandRing= */true));
        HfpClientConnection connection =
                createOutgoingConnectionWithScoState(BluetoothHeadsetClient.STATE_AUDIO_CONNECTED);

        assertThat(connection.getExtras().getInt(KEY_SCO_STATE))
                .isEqualTo(BluetoothHeadsetClient.STATE_AUDIO_CONNECTED);
    }

    @Test
    public void testOnAudioStateChanged() {
        setUpCall(new BluetoothHeadsetClientCall(mBluetoothDevice, /* id= */0,
                BluetoothHeadsetClientCall.CALL_STATE_ACTIVE, TEST_NUMBER,
                /* multiParty= */false, /* outgoing= */false, /* inBandRing= */true));
        HfpClientConnection connection =
                createOutgoingConnectionWithScoState(BluetoothHeadsetClient.STATE_AUDIO_CONNECTED);
        assertThat(connection.getExtras().getInt(KEY_SCO_STATE))
                .isEqualTo(BluetoothHeadsetClient.STATE_AUDIO_CONNECTED);

        mHfpClientDeviceBlock.onAudioStateChange(BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED,
                BluetoothHeadsetClient.STATE_AUDIO_CONNECTED);

        assertThat(connection.getExtras().getInt(KEY_SCO_STATE))
                .isEqualTo(BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED);
    }

    @Test
    @Ignore("b/191783947")
    public void testHandleMultiPartyCall_scoStateIsSetOnConference() {
        BluetoothHeadsetClientCall call =
                new BluetoothHeadsetClientCall(mBluetoothDevice, /* id= */0,
                BluetoothHeadsetClientCall.CALL_STATE_ACTIVE, TEST_NUMBER, /* multiParty= */
                true, /* outgoing= */false, /* inBandRing= */true);
        setUpCall(call);
        createOutgoingConnectionWithScoState(BluetoothHeadsetClient.STATE_AUDIO_CONNECTING);

        mHfpClientDeviceBlock.handleCall(call);

        ArgumentCaptor<HfpClientConference> conferenceCaptor =
                ArgumentCaptor.forClass(HfpClientConference.class);
        // TODO(b/191783947): addConference is final and cannot be mocked
        verify(mConnServ).addConference(conferenceCaptor.capture());

        HfpClientConference conference = conferenceCaptor.getValue();
        assertThat(conference.getExtras().getInt(KEY_SCO_STATE))
                .isEqualTo(BluetoothHeadsetClient.STATE_AUDIO_CONNECTING);
    }

    private void setUpCall(BluetoothHeadsetClientCall call) {
        when(mHeadsetProfile.dial(mBluetoothDevice, TEST_NUMBER)).thenReturn(call);
    }

    private HfpClientConnection createOutgoingConnectionWithScoState(int scoState) {
        when(mHeadsetClientService.getAudioState(mBluetoothDevice)).thenReturn(scoState);
        mHfpClientDeviceBlock =
                new HfpClientDeviceBlock(mConnServ, mBluetoothDevice, mHeadsetProfile);
        return mHfpClientDeviceBlock.onCreateOutgoingConnection(
                Uri.fromParts(PhoneAccount.SCHEME_TEL, TEST_NUMBER, /* fragment= */ null));
    }
}