blob: 2dd5356418fa9b95f1b58bbcac273019c2cc009f [file] [log] [blame]
/*
* 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.hfp;
import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import android.app.Activity;
import android.app.Instrumentation;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.IBluetoothHeadset;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.net.Uri;
import android.os.ParcelUuid;
import android.os.PowerManager;
import android.os.RemoteException;
import android.telecom.PhoneAccount;
import androidx.test.InstrumentationRegistry;
import androidx.test.espresso.intent.Intents;
import androidx.test.espresso.intent.matcher.IntentMatchers;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ServiceTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.storage.DatabaseManager;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* A set of integration test that involves both {@link HeadsetService} and
* {@link HeadsetStateMachine}
*/
@MediumTest
@RunWith(AndroidJUnit4.class)
public class HeadsetServiceAndStateMachineTest {
private static final int ASYNC_CALL_TIMEOUT_MILLIS = 250;
private static final int START_VR_TIMEOUT_MILLIS = 1000;
private static final int START_VR_TIMEOUT_WAIT_MILLIS = START_VR_TIMEOUT_MILLIS * 3 / 2;
private static final int MAX_HEADSET_CONNECTIONS = 5;
private static final ParcelUuid[] FAKE_HEADSET_UUID = {BluetoothUuid.HFP};
private static final String TEST_PHONE_NUMBER = "1234567890";
private static final String TEST_CALLER_ID = "Test Name";
@Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
private Context mTargetContext;
private HeadsetService mHeadsetService;
private IBluetoothHeadset.Stub mHeadsetServiceBinder;
private BluetoothAdapter mAdapter;
private HeadsetNativeInterface mNativeInterface;
private ArgumentCaptor<HeadsetStateMachine> mStateMachineArgument =
ArgumentCaptor.forClass(HeadsetStateMachine.class);
private HashSet<BluetoothDevice> mBondedDevices = new HashSet<>();
private final BlockingQueue<Intent> mConnectionStateChangedQueue = new LinkedBlockingQueue<>();
private final BlockingQueue<Intent> mActiveDeviceChangedQueue = new LinkedBlockingQueue<>();
private final BlockingQueue<Intent> mAudioStateChangedQueue = new LinkedBlockingQueue<>();
private HeadsetIntentReceiver mHeadsetIntentReceiver;
private int mOriginalVrTimeoutMs = 5000;
private PowerManager.WakeLock mVoiceRecognitionWakeLock;
private class HeadsetIntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action == null) {
Assert.fail("Action is null for intent " + intent);
return;
}
switch (action) {
case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
try {
mConnectionStateChangedQueue.put(intent);
} catch (InterruptedException e) {
Assert.fail("Cannot add Intent to the Connection State Changed queue: "
+ e.getMessage());
}
break;
case BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED:
try {
mActiveDeviceChangedQueue.put(intent);
} catch (InterruptedException e) {
Assert.fail("Cannot add Intent to the Active Device Changed queue: "
+ e.getMessage());
}
break;
case BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED:
try {
mAudioStateChangedQueue.put(intent);
} catch (InterruptedException e) {
Assert.fail("Cannot add Intent to the Audio State Changed queue: "
+ e.getMessage());
}
break;
default:
Assert.fail("Unknown action " + action);
}
}
}
@Spy private HeadsetObjectsFactory mObjectsFactory = HeadsetObjectsFactory.getInstance();
@Mock private AdapterService mAdapterService;
@Mock private DatabaseManager mDatabaseManager;
@Mock private HeadsetSystemInterface mSystemInterface;
@Mock private AudioManager mAudioManager;
@Mock private HeadsetPhoneState mPhoneState;
@Before
public void setUp() throws Exception {
mTargetContext = InstrumentationRegistry.getTargetContext();
Assume.assumeTrue("Ignore test when HeadsetService is not enabled",
mTargetContext.getResources().getBoolean(R.bool.profile_supported_hs_hfp));
MockitoAnnotations.initMocks(this);
PowerManager powerManager =
(PowerManager) mTargetContext.getSystemService(Context.POWER_SERVICE);
mVoiceRecognitionWakeLock =
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "VoiceRecognitionTest");
TestUtils.setAdapterService(mAdapterService);
doReturn(MAX_HEADSET_CONNECTIONS).when(mAdapterService).getMaxConnectedAudioDevices();
doReturn(new ParcelUuid[]{BluetoothUuid.HFP}).when(mAdapterService)
.getRemoteUuids(any(BluetoothDevice.class));
doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
// We cannot mock HeadsetObjectsFactory.getInstance() with Mockito.
// Hence we need to use reflection to call a private method to
// initialize properly the HeadsetObjectsFactory.sInstance field.
Method method = HeadsetObjectsFactory.class.getDeclaredMethod("setInstanceForTesting",
HeadsetObjectsFactory.class);
method.setAccessible(true);
method.invoke(null, mObjectsFactory);
// This line must be called to make sure relevant objects are initialized properly
mAdapter = BluetoothAdapter.getDefaultAdapter();
// Mock methods in AdapterService
doReturn(FAKE_HEADSET_UUID).when(mAdapterService)
.getRemoteUuids(any(BluetoothDevice.class));
doReturn(BluetoothDevice.BOND_BONDED).when(mAdapterService)
.getBondState(any(BluetoothDevice.class));
doAnswer(invocation -> mBondedDevices.toArray(new BluetoothDevice[]{})).when(
mAdapterService).getBondedDevices();
// Mock system interface
doNothing().when(mSystemInterface).init();
doNothing().when(mSystemInterface).stop();
when(mSystemInterface.getHeadsetPhoneState()).thenReturn(mPhoneState);
when(mSystemInterface.getAudioManager()).thenReturn(mAudioManager);
when(mSystemInterface.activateVoiceRecognition()).thenReturn(true);
when(mSystemInterface.deactivateVoiceRecognition()).thenReturn(true);
when(mSystemInterface.getVoiceRecognitionWakeLock()).thenReturn(mVoiceRecognitionWakeLock);
when(mSystemInterface.isCallIdle()).thenReturn(true);
// Mock methods in HeadsetNativeInterface
mNativeInterface = spy(HeadsetNativeInterface.getInstance());
doNothing().when(mNativeInterface).init(anyInt(), anyBoolean());
doNothing().when(mNativeInterface).cleanup();
doReturn(true).when(mNativeInterface).connectHfp(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectHfp(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).connectAudio(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectAudio(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).setActiveDevice(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).sendBsir(any(BluetoothDevice.class), anyBoolean());
doReturn(true).when(mNativeInterface).startVoiceRecognition(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).stopVoiceRecognition(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface)
.atResponseCode(any(BluetoothDevice.class), anyInt(), anyInt());
// Use real state machines here
doCallRealMethod().when(mObjectsFactory)
.makeStateMachine(any(), any(), any(), any(), any(), any());
// Mock methods in HeadsetObjectsFactory
doReturn(mSystemInterface).when(mObjectsFactory).makeSystemInterface(any());
doReturn(mNativeInterface).when(mObjectsFactory).getNativeInterface();
Intents.init();
// Modify start VR timeout to a smaller value for testing
mOriginalVrTimeoutMs = HeadsetService.sStartVrTimeoutMs;
HeadsetService.sStartVrTimeoutMs = START_VR_TIMEOUT_MILLIS;
TestUtils.startService(mServiceRule, HeadsetService.class);
mHeadsetService = HeadsetService.getHeadsetService();
Assert.assertNotNull(mHeadsetService);
verify(mObjectsFactory).makeSystemInterface(mHeadsetService);
verify(mObjectsFactory).getNativeInterface();
verify(mNativeInterface).init(MAX_HEADSET_CONNECTIONS + 1, true /* inband ringtone */);
mHeadsetServiceBinder = (IBluetoothHeadset.Stub) mHeadsetService.initBinder();
Assert.assertNotNull(mHeadsetServiceBinder);
// Set up the Connection State Changed receiver
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
mHeadsetIntentReceiver = new HeadsetIntentReceiver();
mTargetContext.registerReceiver(mHeadsetIntentReceiver, filter);
}
@After
public void tearDown() throws Exception {
if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_hs_hfp)) {
return;
}
mTargetContext.unregisterReceiver(mHeadsetIntentReceiver);
TestUtils.stopService(mServiceRule, HeadsetService.class);
HeadsetService.sStartVrTimeoutMs = mOriginalVrTimeoutMs;
Intents.release();
mHeadsetService = HeadsetService.getHeadsetService();
Assert.assertNull(mHeadsetService);
Method method = HeadsetObjectsFactory.class.getDeclaredMethod("setInstanceForTesting",
HeadsetObjectsFactory.class);
method.setAccessible(true);
method.invoke(null, (HeadsetObjectsFactory) null);
TestUtils.clearAdapterService(mAdapterService);
mBondedDevices.clear();
mConnectionStateChangedQueue.clear();
mActiveDeviceChangedQueue.clear();
// Clear classes that is spied on and has static life time
clearInvocations(mNativeInterface);
}
/**
* Test to verify that HeadsetService can be successfully started
*/
@Test
public void testGetHeadsetService() {
Assert.assertEquals(mHeadsetService, HeadsetService.getHeadsetService());
// Verify default connection and audio states
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
mHeadsetService.getConnectionState(device));
Assert.assertEquals(BluetoothHeadset.STATE_AUDIO_DISCONNECTED,
mHeadsetService.getAudioState(device));
}
/**
* Test to verify that {@link HeadsetService#connect(BluetoothDevice)} actually result in a
* call to native interface to create HFP
*/
@Test
public void testConnectFromApi() {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
when(mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET))
.thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
mBondedDevices.add(device);
Assert.assertTrue(mHeadsetService.connect(device));
verify(mObjectsFactory).makeStateMachine(device,
mHeadsetService.getStateMachinesThreadLooper(), mHeadsetService, mAdapterService,
mNativeInterface, mSystemInterface);
// Wait ASYNC_CALL_TIMEOUT_MILLIS for state to settle, timing is also tested here and
// 250ms for processing two messages should be way more than enough. Anything that breaks
// this indicate some breakage in other part of Android OS
waitAndVerifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED);
verify(mNativeInterface).connectHfp(device);
Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
mHeadsetService.getConnectionState(device));
Assert.assertEquals(Collections.singletonList(device),
mHeadsetService.getDevicesMatchingConnectionStates(
new int[]{BluetoothProfile.STATE_CONNECTING}));
// Get feedback from native to put device into connected state
HeadsetStackEvent connectedEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED,
HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED, device);
mHeadsetService.messageFromNative(connectedEvent);
// Wait ASYNC_CALL_TIMEOUT_MILLIS for state to settle, timing is also tested here and
// 250ms for processing two messages should be way more than enough. Anything that breaks
// this indicate some breakage in other part of Android OS
waitAndVerifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING);
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
mHeadsetService.getConnectionState(device));
Assert.assertEquals(Collections.singletonList(device),
mHeadsetService.getDevicesMatchingConnectionStates(
new int[]{BluetoothProfile.STATE_CONNECTED}));
}
/**
* Test to verify that {@link BluetoothDevice#ACTION_BOND_STATE_CHANGED} intent with
* {@link BluetoothDevice#EXTRA_BOND_STATE} as {@link BluetoothDevice#BOND_NONE} will cause a
* disconnected device to be removed from state machine map
*/
@Test
public void testUnbondDevice_disconnectBeforeUnbond() {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
when(mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET))
.thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
mBondedDevices.add(device);
Assert.assertTrue(mHeadsetService.connect(device));
verify(mObjectsFactory).makeStateMachine(device,
mHeadsetService.getStateMachinesThreadLooper(), mHeadsetService, mAdapterService,
mNativeInterface, mSystemInterface);
// Wait ASYNC_CALL_TIMEOUT_MILLIS for state to settle, timing is also tested here and
// 250ms for processing two messages should be way more than enough. Anything that breaks
// this indicate some breakage in other part of Android OS
waitAndVerifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED);
verify(mNativeInterface).connectHfp(device);
// Get feedback from native layer to go back to disconnected state
HeadsetStackEvent connectedEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED,
HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED, device);
mHeadsetService.messageFromNative(connectedEvent);
// Wait ASYNC_CALL_TIMEOUT_MILLIS for state to settle, timing is also tested here and
// 250ms for processing two messages should be way more than enough. Anything that breaks
// this indicate some breakage in other part of Android OS
waitAndVerifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING);
// Send unbond intent
doReturn(BluetoothDevice.BOND_NONE).when(mAdapterService).getBondState(eq(device));
Intent unbondIntent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
unbondIntent.putExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
unbondIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
InstrumentationRegistry.getTargetContext().sendBroadcast(unbondIntent);
// Check that the state machine is actually destroyed
verify(mObjectsFactory, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).destroyStateMachine(
mStateMachineArgument.capture());
Assert.assertEquals(device, mStateMachineArgument.getValue().getDevice());
}
/**
* Test to verify that if a device can be property disconnected after
* {@link BluetoothDevice#ACTION_BOND_STATE_CHANGED} intent with
* {@link BluetoothDevice#EXTRA_BOND_STATE} as {@link BluetoothDevice#BOND_NONE} is received.
*/
@Test
public void testUnbondDevice_disconnectAfterUnbond() {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
when(mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET))
.thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
mBondedDevices.add(device);
Assert.assertTrue(mHeadsetService.connect(device));
verify(mObjectsFactory).makeStateMachine(device,
mHeadsetService.getStateMachinesThreadLooper(), mHeadsetService, mAdapterService,
mNativeInterface, mSystemInterface);
// Wait ASYNC_CALL_TIMEOUT_MILLIS for state to settle, timing is also tested here and
// 250ms for processing two messages should be way more than enough. Anything that breaks
// this indicate some breakage in other part of Android OS
verify(mNativeInterface, after(ASYNC_CALL_TIMEOUT_MILLIS)).connectHfp(device);
waitAndVerifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED);
// Get feedback from native layer to go to connected state
HeadsetStackEvent connectedEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED,
HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED, device);
mHeadsetService.messageFromNative(connectedEvent);
// Wait ASYNC_CALL_TIMEOUT_MILLIS for state to settle, timing is also tested here and
// 250ms for processing two messages should be way more than enough. Anything that breaks
// this indicate some breakage in other part of Android OS
waitAndVerifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING);
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
mHeadsetService.getConnectionState(device));
Assert.assertEquals(Collections.singletonList(device),
mHeadsetService.getConnectedDevices());
// Send unbond intent
doReturn(BluetoothDevice.BOND_NONE).when(mAdapterService).getBondState(eq(device));
Intent unbondIntent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
unbondIntent.putExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
unbondIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
InstrumentationRegistry.getTargetContext().sendBroadcast(unbondIntent);
// Check that the state machine is not destroyed
verify(mObjectsFactory, after(ASYNC_CALL_TIMEOUT_MILLIS).never()).destroyStateMachine(
any());
// Now disconnect the device
HeadsetStackEvent connectingEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED,
HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED, device);
mHeadsetService.messageFromNative(connectingEvent);
waitAndVerifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTED);
// Check that the state machine is destroyed after another async call
verify(mObjectsFactory, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).destroyStateMachine(
mStateMachineArgument.capture());
Assert.assertEquals(device, mStateMachineArgument.getValue().getDevice());
}
/**
* Test the functionality of
* {@link BluetoothHeadset#startScoUsingVirtualVoiceCall()} and
* {@link BluetoothHeadset#stopScoUsingVirtualVoiceCall()}
*
* Normal start and stop
*/
@Test
public void testVirtualCall_normalStartStop() throws RemoteException {
for (int i = 0; i < MAX_HEADSET_CONNECTIONS; ++i) {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, i);
connectTestDevice(device);
Assert.assertThat(mHeadsetServiceBinder.getConnectedDevices(),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
Assert.assertThat(mHeadsetServiceBinder.getDevicesMatchingConnectionStates(
new int[]{BluetoothProfile.STATE_CONNECTED}),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
}
List<BluetoothDevice> connectedDevices = mHeadsetServiceBinder.getConnectedDevices();
Assert.assertThat(connectedDevices, Matchers.containsInAnyOrder(mBondedDevices.toArray()));
Assert.assertFalse(mHeadsetService.isVirtualCallStarted());
BluetoothDevice activeDevice = connectedDevices.get(MAX_HEADSET_CONNECTIONS / 2);
Assert.assertTrue(mHeadsetServiceBinder.setActiveDevice(activeDevice));
verify(mNativeInterface).setActiveDevice(activeDevice);
waitAndVerifyActiveDeviceChangedIntent(ASYNC_CALL_TIMEOUT_MILLIS, activeDevice);
Assert.assertEquals(activeDevice, mHeadsetServiceBinder.getActiveDevice());
// Start virtual call
Assert.assertTrue(mHeadsetServiceBinder.startScoUsingVirtualVoiceCall());
Assert.assertTrue(mHeadsetService.isVirtualCallStarted());
verifyVirtualCallStartSequenceInvocations(connectedDevices);
// End virtual call
Assert.assertTrue(mHeadsetServiceBinder.stopScoUsingVirtualVoiceCall());
Assert.assertFalse(mHeadsetService.isVirtualCallStarted());
verifyVirtualCallStopSequenceInvocations(connectedDevices);
}
/**
* Test the functionality of
* {@link BluetoothHeadset#startScoUsingVirtualVoiceCall()} and
* {@link BluetoothHeadset#stopScoUsingVirtualVoiceCall()}
*
* Virtual call should be preempted by telecom call
*/
@Test
public void testVirtualCall_preemptedByTelecomCall() throws RemoteException {
for (int i = 0; i < MAX_HEADSET_CONNECTIONS; ++i) {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, i);
connectTestDevice(device);
Assert.assertThat(mHeadsetServiceBinder.getConnectedDevices(),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
Assert.assertThat(mHeadsetServiceBinder.getDevicesMatchingConnectionStates(
new int[]{BluetoothProfile.STATE_CONNECTED}),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
}
List<BluetoothDevice> connectedDevices = mHeadsetServiceBinder.getConnectedDevices();
Assert.assertThat(connectedDevices, Matchers.containsInAnyOrder(mBondedDevices.toArray()));
Assert.assertFalse(mHeadsetService.isVirtualCallStarted());
BluetoothDevice activeDevice = connectedDevices.get(MAX_HEADSET_CONNECTIONS / 2);
Assert.assertTrue(mHeadsetServiceBinder.setActiveDevice(activeDevice));
verify(mNativeInterface).setActiveDevice(activeDevice);
waitAndVerifyActiveDeviceChangedIntent(ASYNC_CALL_TIMEOUT_MILLIS, activeDevice);
Assert.assertEquals(activeDevice, mHeadsetServiceBinder.getActiveDevice());
// Start virtual call
Assert.assertTrue(mHeadsetServiceBinder.startScoUsingVirtualVoiceCall());
Assert.assertTrue(mHeadsetService.isVirtualCallStarted());
verifyVirtualCallStartSequenceInvocations(connectedDevices);
// Virtual call should be preempted by telecom call
mHeadsetServiceBinder.phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_INCOMING,
TEST_PHONE_NUMBER, 128, "");
Assert.assertFalse(mHeadsetService.isVirtualCallStarted());
verifyVirtualCallStopSequenceInvocations(connectedDevices);
verifyCallStateToNativeInvocation(
new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_INCOMING,
TEST_PHONE_NUMBER, 128, ""), connectedDevices);
}
/**
* Test the functionality of
* {@link BluetoothHeadset#startScoUsingVirtualVoiceCall()} and
* {@link BluetoothHeadset#stopScoUsingVirtualVoiceCall()}
*
* Virtual call should be rejected when there is a telecom call
*/
@Test
public void testVirtualCall_rejectedWhenThereIsTelecomCall() throws RemoteException {
for (int i = 0; i < MAX_HEADSET_CONNECTIONS; ++i) {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, i);
connectTestDevice(device);
Assert.assertThat(mHeadsetServiceBinder.getConnectedDevices(),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
Assert.assertThat(mHeadsetServiceBinder.getDevicesMatchingConnectionStates(
new int[]{BluetoothProfile.STATE_CONNECTED}),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
}
List<BluetoothDevice> connectedDevices = mHeadsetServiceBinder.getConnectedDevices();
Assert.assertThat(connectedDevices, Matchers.containsInAnyOrder(mBondedDevices.toArray()));
Assert.assertFalse(mHeadsetService.isVirtualCallStarted());
BluetoothDevice activeDevice = connectedDevices.get(MAX_HEADSET_CONNECTIONS / 2);
Assert.assertTrue(mHeadsetServiceBinder.setActiveDevice(activeDevice));
verify(mNativeInterface).setActiveDevice(activeDevice);
waitAndVerifyActiveDeviceChangedIntent(ASYNC_CALL_TIMEOUT_MILLIS, activeDevice);
Assert.assertEquals(activeDevice, mHeadsetServiceBinder.getActiveDevice());
// Reject virtual call setup if call state is not idle
when(mSystemInterface.isCallIdle()).thenReturn(false);
Assert.assertFalse(mHeadsetServiceBinder.startScoUsingVirtualVoiceCall());
Assert.assertFalse(mHeadsetService.isVirtualCallStarted());
}
/**
* Test the behavior when dialing outgoing call from the headset
*/
@Test
public void testDialingOutCall_NormalDialingOut() throws RemoteException {
for (int i = 0; i < MAX_HEADSET_CONNECTIONS; ++i) {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, i);
connectTestDevice(device);
Assert.assertThat(mHeadsetServiceBinder.getConnectedDevices(),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
Assert.assertThat(mHeadsetServiceBinder.getDevicesMatchingConnectionStates(
new int[]{BluetoothProfile.STATE_CONNECTED}),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
}
List<BluetoothDevice> connectedDevices = mHeadsetServiceBinder.getConnectedDevices();
Assert.assertThat(connectedDevices, Matchers.containsInAnyOrder(mBondedDevices.toArray()));
Assert.assertFalse(mHeadsetService.isVirtualCallStarted());
BluetoothDevice activeDevice = connectedDevices.get(0);
Assert.assertTrue(mHeadsetServiceBinder.setActiveDevice(activeDevice));
verify(mNativeInterface).setActiveDevice(activeDevice);
waitAndVerifyActiveDeviceChangedIntent(ASYNC_CALL_TIMEOUT_MILLIS, activeDevice);
Assert.assertEquals(activeDevice, mHeadsetServiceBinder.getActiveDevice());
// Try dialing out from the a non active Headset
BluetoothDevice dialingOutDevice = connectedDevices.get(1);
HeadsetStackEvent dialingOutEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_DIAL_CALL, TEST_PHONE_NUMBER,
dialingOutDevice);
Uri dialOutUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, TEST_PHONE_NUMBER, null);
Instrumentation.ActivityResult result =
new Instrumentation.ActivityResult(Activity.RESULT_OK, null);
Intents.intending(IntentMatchers.hasAction(Intent.ACTION_CALL_PRIVILEGED))
.respondWith(result);
mHeadsetService.messageFromNative(dialingOutEvent);
waitAndVerifyActiveDeviceChangedIntent(ASYNC_CALL_TIMEOUT_MILLIS, dialingOutDevice);
TestUtils.waitForLooperToFinishScheduledTask(
mHeadsetService.getStateMachinesThreadLooper());
Assert.assertTrue(mHeadsetService.hasDeviceInitiatedDialingOut());
// Make sure the correct intent is fired
Intents.intended(allOf(IntentMatchers.hasAction(Intent.ACTION_CALL_PRIVILEGED),
IntentMatchers.hasData(dialOutUri)), Intents.times(1));
// Further dial out attempt from same device will fail
mHeadsetService.messageFromNative(dialingOutEvent);
TestUtils.waitForLooperToFinishScheduledTask(
mHeadsetService.getStateMachinesThreadLooper());
verify(mNativeInterface).atResponseCode(dialingOutDevice,
HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
// Further dial out attempt from other device will fail
HeadsetStackEvent dialingOutEventOtherDevice =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_DIAL_CALL, TEST_PHONE_NUMBER,
activeDevice);
mHeadsetService.messageFromNative(dialingOutEventOtherDevice);
TestUtils.waitForLooperToFinishScheduledTask(
mHeadsetService.getStateMachinesThreadLooper());
verify(mNativeInterface).atResponseCode(activeDevice, HeadsetHalConstants.AT_RESPONSE_ERROR,
0);
TestUtils.waitForNoIntent(ASYNC_CALL_TIMEOUT_MILLIS, mActiveDeviceChangedQueue);
Assert.assertEquals(dialingOutDevice, mHeadsetServiceBinder.getActiveDevice());
// Make sure only one intent is fired
Intents.intended(allOf(IntentMatchers.hasAction(Intent.ACTION_CALL_PRIVILEGED),
IntentMatchers.hasData(dialOutUri)), Intents.times(1));
// Verify that phone state update confirms the dial out event
mHeadsetServiceBinder.phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_DIALING,
TEST_PHONE_NUMBER, 128, "");
HeadsetCallState dialingCallState =
new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_DIALING,
TEST_PHONE_NUMBER, 128, "");
verifyCallStateToNativeInvocation(dialingCallState, connectedDevices);
verify(mNativeInterface).atResponseCode(dialingOutDevice,
HeadsetHalConstants.AT_RESPONSE_OK, 0);
// Verify that IDLE phone state clears the dialing out flag
mHeadsetServiceBinder.phoneStateChanged(1, 0, HeadsetHalConstants.CALL_STATE_IDLE,
TEST_PHONE_NUMBER, 128, "");
HeadsetCallState activeCallState =
new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_DIALING,
TEST_PHONE_NUMBER, 128, "");
verifyCallStateToNativeInvocation(activeCallState, connectedDevices);
Assert.assertFalse(mHeadsetService.hasDeviceInitiatedDialingOut());
}
/**
* Test the behavior when dialing outgoing call from the headset
*/
@Test
public void testDialingOutCall_DialingOutPreemptVirtualCall() throws RemoteException {
for (int i = 0; i < MAX_HEADSET_CONNECTIONS; ++i) {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, i);
connectTestDevice(device);
Assert.assertThat(mHeadsetServiceBinder.getConnectedDevices(),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
Assert.assertThat(mHeadsetServiceBinder.getDevicesMatchingConnectionStates(
new int[]{BluetoothProfile.STATE_CONNECTED}),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
}
List<BluetoothDevice> connectedDevices = mHeadsetServiceBinder.getConnectedDevices();
Assert.assertThat(connectedDevices, Matchers.containsInAnyOrder(mBondedDevices.toArray()));
Assert.assertFalse(mHeadsetService.isVirtualCallStarted());
BluetoothDevice activeDevice = connectedDevices.get(0);
Assert.assertTrue(mHeadsetServiceBinder.setActiveDevice(activeDevice));
verify(mNativeInterface).setActiveDevice(activeDevice);
waitAndVerifyActiveDeviceChangedIntent(ASYNC_CALL_TIMEOUT_MILLIS, activeDevice);
Assert.assertEquals(activeDevice, mHeadsetServiceBinder.getActiveDevice());
// Start virtual call
Assert.assertTrue(mHeadsetServiceBinder.startScoUsingVirtualVoiceCall());
Assert.assertTrue(mHeadsetService.isVirtualCallStarted());
verifyVirtualCallStartSequenceInvocations(connectedDevices);
// Try dialing out from the a non active Headset
BluetoothDevice dialingOutDevice = connectedDevices.get(1);
HeadsetStackEvent dialingOutEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_DIAL_CALL, TEST_PHONE_NUMBER,
dialingOutDevice);
Uri dialOutUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, TEST_PHONE_NUMBER, null);
Instrumentation.ActivityResult result =
new Instrumentation.ActivityResult(Activity.RESULT_OK, null);
Intents.intending(IntentMatchers.hasAction(Intent.ACTION_CALL_PRIVILEGED))
.respondWith(result);
mHeadsetService.messageFromNative(dialingOutEvent);
waitAndVerifyActiveDeviceChangedIntent(ASYNC_CALL_TIMEOUT_MILLIS, dialingOutDevice);
TestUtils.waitForLooperToFinishScheduledTask(
mHeadsetService.getStateMachinesThreadLooper());
Assert.assertTrue(mHeadsetService.hasDeviceInitiatedDialingOut());
// Make sure the correct intent is fired
Intents.intended(allOf(IntentMatchers.hasAction(Intent.ACTION_CALL_PRIVILEGED),
IntentMatchers.hasData(dialOutUri)), Intents.times(1));
// Virtual call should be preempted by dialing out call
Assert.assertFalse(mHeadsetService.isVirtualCallStarted());
verifyVirtualCallStopSequenceInvocations(connectedDevices);
}
/**
* Test to verify the following behavior regarding active HF initiated voice recognition
* in the successful scenario
* 1. HF device sends AT+BVRA=1
* 2. HeadsetStateMachine sends out {@link Intent#ACTION_VOICE_COMMAND}
* 3. AG call {@link BluetoothHeadset#stopVoiceRecognition(BluetoothDevice)} to indicate
* that voice recognition has stopped
* 4. AG sends OK to HF
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_SingleHfInitiatedSuccess() {
// Connect HF
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(device);
// Make device active
Assert.assertTrue(mHeadsetService.setActiveDevice(device));
verify(mNativeInterface).setActiveDevice(device);
Assert.assertEquals(device, mHeadsetService.getActiveDevice());
// Start voice recognition
startVoiceRecognitionFromHf(device);
}
/**
* Test to verify the following behavior regarding active HF stop voice recognition
* in the successful scenario
* 1. HF device sends AT+BVRA=0
* 2. Let voice recognition app to stop
* 3. AG respond with OK
* 4. Disconnect audio
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_SingleHfStopSuccess() {
// Connect HF
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(device);
// Make device active
Assert.assertTrue(mHeadsetService.setActiveDevice(device));
verify(mNativeInterface).setActiveDevice(device);
Assert.assertEquals(device, mHeadsetService.getActiveDevice());
// Start voice recognition
startVoiceRecognitionFromHf(device);
// Stop voice recognition
HeadsetStackEvent stopVrEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED,
HeadsetHalConstants.VR_STATE_STOPPED, device);
mHeadsetService.messageFromNative(stopVrEvent);
verify(mSystemInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).deactivateVoiceRecognition();
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).atResponseCode(device,
HeadsetHalConstants.AT_RESPONSE_OK, 0);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).disconnectAudio(device);
verifyNoMoreInteractions(mNativeInterface);
}
/**
* Test to verify the following behavior regarding active HF initiated voice recognition
* in the failed to activate scenario
* 1. HF device sends AT+BVRA=1
* 2. HeadsetStateMachine sends out {@link Intent#ACTION_VOICE_COMMAND}
* 3. Failed to activate voice recognition through intent
* 4. AG sends ERROR to HF
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_SingleHfInitiatedFailedToActivate() {
when(mSystemInterface.activateVoiceRecognition()).thenReturn(false);
// Connect HF
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(device);
// Make device active
Assert.assertTrue(mHeadsetService.setActiveDevice(device));
verify(mNativeInterface).setActiveDevice(device);
Assert.assertEquals(device, mHeadsetService.getActiveDevice());
// Start voice recognition
HeadsetStackEvent startVrEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED,
HeadsetHalConstants.VR_STATE_STARTED, device);
mHeadsetService.messageFromNative(startVrEvent);
verify(mSystemInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).activateVoiceRecognition();
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(device,
HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
verifyNoMoreInteractions(mNativeInterface);
verifyZeroInteractions(mAudioManager);
}
/**
* Test to verify the following behavior regarding active HF initiated voice recognition
* in the timeout scenario
* 1. HF device sends AT+BVRA=1
* 2. HeadsetStateMachine sends out {@link Intent#ACTION_VOICE_COMMAND}
* 3. AG failed to get back to us on time
* 4. AG sends ERROR to HF
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_SingleHfInitiatedTimeout() {
// Connect HF
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(device);
// Make device active
Assert.assertTrue(mHeadsetService.setActiveDevice(device));
verify(mNativeInterface).setActiveDevice(device);
Assert.assertEquals(device, mHeadsetService.getActiveDevice());
// Start voice recognition
HeadsetStackEvent startVrEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED,
HeadsetHalConstants.VR_STATE_STARTED, device);
mHeadsetService.messageFromNative(startVrEvent);
verify(mSystemInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).activateVoiceRecognition();
verify(mNativeInterface, timeout(START_VR_TIMEOUT_WAIT_MILLIS)).atResponseCode(device,
HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
verifyNoMoreInteractions(mNativeInterface);
verifyZeroInteractions(mAudioManager);
}
/**
* Test to verify the following behavior regarding AG initiated voice recognition
* in the successful scenario
* 1. AG starts voice recognition and notify the Bluetooth stack via
* {@link BluetoothHeadset#startVoiceRecognition(BluetoothDevice)} to indicate that voice
* recognition has started
* 2. AG send +BVRA:1 to HF
* 3. AG start SCO connection if SCO has not been started
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_SingleAgInitiatedSuccess() {
// Connect HF
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(device);
// Make device active
Assert.assertTrue(mHeadsetService.setActiveDevice(device));
verify(mNativeInterface).setActiveDevice(device);
Assert.assertEquals(device, mHeadsetService.getActiveDevice());
// Start voice recognition
startVoiceRecognitionFromAg();
}
/**
* Test to verify the following behavior regarding AG initiated voice recognition
* in the successful scenario
* 1. AG starts voice recognition and notify the Bluetooth stack via
* {@link BluetoothHeadset#startVoiceRecognition(BluetoothDevice)} to indicate that voice
* recognition has started, BluetoothDevice is null in this case
* 2. AG send +BVRA:1 to current active HF
* 3. AG start SCO connection if SCO has not been started
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_SingleAgInitiatedSuccessNullInput() {
// Connect HF
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(device);
// Make device active
Assert.assertTrue(mHeadsetService.setActiveDevice(device));
verify(mNativeInterface).setActiveDevice(device);
Assert.assertEquals(device, mHeadsetService.getActiveDevice());
// Start voice recognition on null argument should go to active device
Assert.assertTrue(mHeadsetService.startVoiceRecognition(null));
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).startVoiceRecognition(device);
}
/**
* Test to verify the following behavior regarding AG initiated voice recognition
* in the successful scenario
* 1. AG starts voice recognition and notify the Bluetooth stack via
* {@link BluetoothHeadset#startVoiceRecognition(BluetoothDevice)} to indicate that voice
* recognition has started, BluetoothDevice is null and active device is null
* 2. The call should fail
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_SingleAgInitiatedFailNullActiveDevice() {
// Connect HF
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(device);
// Make device active
Assert.assertTrue(mHeadsetService.setActiveDevice(null));
// TODO(b/79760385): setActiveDevice(null) does not propagate to native layer
// verify(mNativeInterface).setActiveDevice(null);
Assert.assertNull(mHeadsetService.getActiveDevice());
// Start voice recognition on null argument should fail
Assert.assertFalse(mHeadsetService.startVoiceRecognition(null));
}
/**
* Test to verify the following behavior regarding AG stops voice recognition
* in the successful scenario
* 1. AG stops voice recognition and notify the Bluetooth stack via
* {@link BluetoothHeadset#stopVoiceRecognition(BluetoothDevice)} to indicate that voice
* recognition has stopped
* 2. AG send +BVRA:0 to HF
* 3. AG stop SCO connection
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_SingleAgStopSuccess() {
// Connect HF
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(device);
// Make device active
Assert.assertTrue(mHeadsetService.setActiveDevice(device));
verify(mNativeInterface).setActiveDevice(device);
Assert.assertEquals(device, mHeadsetService.getActiveDevice());
// Start voice recognition
startVoiceRecognitionFromAg();
// Stop voice recognition
Assert.assertTrue(mHeadsetService.stopVoiceRecognition(device));
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).stopVoiceRecognition(device);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).disconnectAudio(device);
verifyNoMoreInteractions(mNativeInterface);
}
/**
* Test to verify the following behavior regarding AG initiated voice recognition
* in the device not connected failure scenario
* 1. AG starts voice recognition and notify the Bluetooth stack via
* {@link BluetoothHeadset#startVoiceRecognition(BluetoothDevice)} to indicate that voice
* recognition has started
* 2. Device is not connected, return false
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_SingleAgInitiatedDeviceNotConnected() {
// Start voice recognition
BluetoothDevice disconnectedDevice = TestUtils.getTestDevice(mAdapter, 0);
Assert.assertFalse(mHeadsetService.startVoiceRecognition(disconnectedDevice));
verifyNoMoreInteractions(mNativeInterface);
verifyZeroInteractions(mAudioManager);
}
/**
* Test to verify the following behavior regarding non active HF initiated voice recognition
* in the successful scenario
* 1. HF device sends AT+BVRA=1
* 2. HeadsetStateMachine sends out {@link Intent#ACTION_VOICE_COMMAND}
* 3. AG call {@link BluetoothHeadset#startVoiceRecognition(BluetoothDevice)} to indicate
* that voice recognition has started
* 4. AG sends OK to HF
* 5. Suspend A2DP
* 6. Start SCO if SCO hasn't been started
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_MultiHfInitiatedSwitchActiveDeviceSuccess() {
// Connect two devices
BluetoothDevice deviceA = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(deviceA);
BluetoothDevice deviceB = TestUtils.getTestDevice(mAdapter, 1);
connectTestDevice(deviceB);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).sendBsir(deviceA, false);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).sendBsir(deviceB, false);
// Set active device to device B
Assert.assertTrue(mHeadsetService.setActiveDevice(deviceB));
verify(mNativeInterface).setActiveDevice(deviceB);
Assert.assertEquals(deviceB, mHeadsetService.getActiveDevice());
// Start voice recognition from non active device A
HeadsetStackEvent startVrEventA =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED,
HeadsetHalConstants.VR_STATE_STARTED, deviceA);
mHeadsetService.messageFromNative(startVrEventA);
verify(mSystemInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).activateVoiceRecognition();
// Active device should have been swapped to device A
verify(mNativeInterface).setActiveDevice(deviceA);
Assert.assertEquals(deviceA, mHeadsetService.getActiveDevice());
// Start voice recognition from other device should fail
HeadsetStackEvent startVrEventB =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED,
HeadsetHalConstants.VR_STATE_STARTED, deviceB);
mHeadsetService.messageFromNative(startVrEventB);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(deviceB,
HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
// Reply to continue voice recognition
mHeadsetService.startVoiceRecognition(deviceA);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(deviceA,
HeadsetHalConstants.AT_RESPONSE_OK, 0);
verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
.setParameters("A2dpSuspended=true");
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(deviceA);
verifyNoMoreInteractions(mNativeInterface);
}
/**
* Test to verify the following behavior regarding non active HF initiated voice recognition
* in the successful scenario
* 1. HF device sends AT+BVRA=1
* 2. HeadsetStateMachine sends out {@link Intent#ACTION_VOICE_COMMAND}
* 3. AG call {@link BluetoothHeadset#startVoiceRecognition(BluetoothDevice)} to indicate
* that voice recognition has started, but on a wrong HF
* 4. Headset service instead keep using the initiating HF
* 5. AG sends OK to HF
* 6. Suspend A2DP
* 7. Start SCO if SCO hasn't been started
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_MultiHfInitiatedSwitchActiveDeviceReplyWrongHfSuccess() {
// Connect two devices
BluetoothDevice deviceA = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(deviceA);
BluetoothDevice deviceB = TestUtils.getTestDevice(mAdapter, 1);
connectTestDevice(deviceB);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).sendBsir(deviceA, false);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).sendBsir(deviceB, false);
// Set active device to device B
Assert.assertTrue(mHeadsetService.setActiveDevice(deviceB));
verify(mNativeInterface).setActiveDevice(deviceB);
Assert.assertEquals(deviceB, mHeadsetService.getActiveDevice());
// Start voice recognition from non active device A
HeadsetStackEvent startVrEventA =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED,
HeadsetHalConstants.VR_STATE_STARTED, deviceA);
mHeadsetService.messageFromNative(startVrEventA);
verify(mSystemInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).activateVoiceRecognition();
// Active device should have been swapped to device A
verify(mNativeInterface).setActiveDevice(deviceA);
Assert.assertEquals(deviceA, mHeadsetService.getActiveDevice());
// Start voice recognition from other device should fail
HeadsetStackEvent startVrEventB =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED,
HeadsetHalConstants.VR_STATE_STARTED, deviceB);
mHeadsetService.messageFromNative(startVrEventB);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(deviceB,
HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
// Reply to continue voice recognition on a wrong device
mHeadsetService.startVoiceRecognition(deviceB);
// We still continue on the initiating HF
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(deviceA,
HeadsetHalConstants.AT_RESPONSE_OK, 0);
verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
.setParameters("A2dpSuspended=true");
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(deviceA);
verifyNoMoreInteractions(mNativeInterface);
}
/**
* Test to verify the following behavior regarding AG initiated voice recognition
* in the successful scenario
* 1. AG starts voice recognition and notify the Bluetooth stack via
* {@link BluetoothHeadset#startVoiceRecognition(BluetoothDevice)} to indicate that voice
* recognition has started
* 2. Suspend A2DP
* 3. AG send +BVRA:1 to HF
* 4. AG start SCO connection if SCO has not been started
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_MultiAgInitiatedSuccess() {
// Connect two devices
BluetoothDevice deviceA = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(deviceA);
BluetoothDevice deviceB = TestUtils.getTestDevice(mAdapter, 1);
connectTestDevice(deviceB);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).sendBsir(deviceA, false);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).sendBsir(deviceB, false);
// Set active device to device B
Assert.assertTrue(mHeadsetService.setActiveDevice(deviceB));
verify(mNativeInterface).setActiveDevice(deviceB);
Assert.assertEquals(deviceB, mHeadsetService.getActiveDevice());
// Start voice recognition
startVoiceRecognitionFromAg();
// Start voice recognition from other device should fail
HeadsetStackEvent startVrEventA =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED,
HeadsetHalConstants.VR_STATE_STARTED, deviceA);
mHeadsetService.messageFromNative(startVrEventA);
// TODO(b/79660380): Workaround in case voice recognition was not terminated properly
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).stopVoiceRecognition(deviceB);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).disconnectAudio(deviceB);
// This request should still fail
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(deviceA,
HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
verifyNoMoreInteractions(mNativeInterface);
}
/**
* Test to verify the following behavior regarding AG initiated voice recognition
* in the device not active failure scenario
* 1. AG starts voice recognition and notify the Bluetooth stack via
* {@link BluetoothHeadset#startVoiceRecognition(BluetoothDevice)} to indicate that voice
* recognition has started
* 2. Device is not active, should do voice recognition on active device only
*
* Reference: Section 4.25, Page 64/144 of HFP 1.7.1 specification
*/
@Test
public void testVoiceRecognition_MultiAgInitiatedDeviceNotActive() {
// Connect two devices
BluetoothDevice deviceA = TestUtils.getTestDevice(mAdapter, 0);
connectTestDevice(deviceA);
BluetoothDevice deviceB = TestUtils.getTestDevice(mAdapter, 1);
connectTestDevice(deviceB);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).sendBsir(deviceA, false);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).sendBsir(deviceB, false);
// Set active device to device B
Assert.assertTrue(mHeadsetService.setActiveDevice(deviceB));
verify(mNativeInterface).setActiveDevice(deviceB);
Assert.assertEquals(deviceB, mHeadsetService.getActiveDevice());
// Start voice recognition should succeed
Assert.assertTrue(mHeadsetService.startVoiceRecognition(deviceA));
verify(mNativeInterface).setActiveDevice(deviceA);
Assert.assertEquals(deviceA, mHeadsetService.getActiveDevice());
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).startVoiceRecognition(deviceA);
verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
.setParameters("A2dpSuspended=true");
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(deviceA);
waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, deviceA,
BluetoothHeadset.STATE_AUDIO_CONNECTING, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
mHeadsetService.messageFromNative(
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED,
HeadsetHalConstants.AUDIO_STATE_CONNECTED, deviceA));
waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, deviceA,
BluetoothHeadset.STATE_AUDIO_CONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTING);
verifyNoMoreInteractions(mNativeInterface);
}
/**
* Test to verify the call state and caller information are correctly delivered
* {@link BluetoothHeadset#phoneStateChanged(int, int, int, String, int, String, boolean)}
*/
@Test
public void testPhoneStateChangedWithIncomingCallState() throws RemoteException {
// Connect HF
for (int i = 0; i < MAX_HEADSET_CONNECTIONS; ++i) {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, i);
connectTestDevice(device);
Assert.assertThat(mHeadsetServiceBinder.getConnectedDevices(),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
Assert.assertThat(mHeadsetServiceBinder.getDevicesMatchingConnectionStates(
new int[]{BluetoothProfile.STATE_CONNECTED}),
Matchers.containsInAnyOrder(mBondedDevices.toArray()));
}
List<BluetoothDevice> connectedDevices = mHeadsetServiceBinder.getConnectedDevices();
Assert.assertThat(connectedDevices, Matchers.containsInAnyOrder(mBondedDevices.toArray()));
// Incoming call update by telecom
mHeadsetServiceBinder.phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_INCOMING,
TEST_PHONE_NUMBER, 128, TEST_CALLER_ID);
HeadsetCallState incomingCallState = new HeadsetCallState(0, 0,
HeadsetHalConstants.CALL_STATE_INCOMING, TEST_PHONE_NUMBER, 128, TEST_CALLER_ID);
verifyCallStateToNativeInvocation(incomingCallState, connectedDevices);
}
private void startVoiceRecognitionFromHf(BluetoothDevice device) {
// Start voice recognition
HeadsetStackEvent startVrEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED,
HeadsetHalConstants.VR_STATE_STARTED, device);
mHeadsetService.messageFromNative(startVrEvent);
verify(mSystemInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).activateVoiceRecognition();
Assert.assertTrue(mHeadsetService.startVoiceRecognition(device));
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(device,
HeadsetHalConstants.AT_RESPONSE_OK, 0);
verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
.setParameters("A2dpSuspended=true");
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(device);
waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothHeadset.STATE_AUDIO_CONNECTING, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
mHeadsetService.messageFromNative(
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED,
HeadsetHalConstants.AUDIO_STATE_CONNECTED, device));
waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothHeadset.STATE_AUDIO_CONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTING);
verifyNoMoreInteractions(mNativeInterface);
}
private void startVoiceRecognitionFromAg() {
BluetoothDevice device = mHeadsetService.getActiveDevice();
Assert.assertNotNull(device);
Assert.assertTrue(mHeadsetService.startVoiceRecognition(device));
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).startVoiceRecognition(device);
verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
.setParameters("A2dpSuspended=true");
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(device);
waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothHeadset.STATE_AUDIO_CONNECTING, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
mHeadsetService.messageFromNative(
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED,
HeadsetHalConstants.AUDIO_STATE_CONNECTED, device));
waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothHeadset.STATE_AUDIO_CONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTING);
verifyNoMoreInteractions(mNativeInterface);
}
private void connectTestDevice(BluetoothDevice device) {
when(mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET))
.thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
// Make device bonded
mBondedDevices.add(device);
// Use connecting event to indicate that device is connecting
HeadsetStackEvent rfcommConnectedEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED,
HeadsetHalConstants.CONNECTION_STATE_CONNECTED, device);
mHeadsetService.messageFromNative(rfcommConnectedEvent);
verify(mObjectsFactory).makeStateMachine(device,
mHeadsetService.getStateMachinesThreadLooper(), mHeadsetService, mAdapterService,
mNativeInterface, mSystemInterface);
// Wait ASYNC_CALL_TIMEOUT_MILLIS for state to settle, timing is also tested here and
// 250ms for processing two messages should be way more than enough. Anything that breaks
// this indicate some breakage in other part of Android OS
waitAndVerifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED);
Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
mHeadsetService.getConnectionState(device));
Assert.assertEquals(Collections.singletonList(device),
mHeadsetService.getDevicesMatchingConnectionStates(
new int[]{BluetoothProfile.STATE_CONNECTING}));
// Get feedback from native to put device into connected state
HeadsetStackEvent slcConnectedEvent =
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED,
HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED, device);
mHeadsetService.messageFromNative(slcConnectedEvent);
// Wait ASYNC_CALL_TIMEOUT_MILLIS for state to settle, timing is also tested here and
// 250ms for processing two messages should be way more than enough. Anything that breaks
// this indicate some breakage in other part of Android OS
waitAndVerifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING);
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
mHeadsetService.getConnectionState(device));
}
private void waitAndVerifyConnectionStateIntent(int timeoutMs, BluetoothDevice device,
int newState, int prevState) {
Intent intent = TestUtils.waitForIntent(timeoutMs, mConnectionStateChangedQueue);
Assert.assertNotNull(intent);
HeadsetTestUtils.verifyConnectionStateBroadcast(device, newState, prevState, intent, false);
}
private void waitAndVerifyActiveDeviceChangedIntent(int timeoutMs, BluetoothDevice device) {
Intent intent = TestUtils.waitForIntent(timeoutMs, mActiveDeviceChangedQueue);
Assert.assertNotNull(intent);
HeadsetTestUtils.verifyActiveDeviceChangedBroadcast(device, intent, false);
}
private void waitAndVerifyAudioStateIntent(int timeoutMs, BluetoothDevice device, int newState,
int prevState) {
Intent intent = TestUtils.waitForIntent(timeoutMs, mAudioStateChangedQueue);
Assert.assertNotNull(intent);
HeadsetTestUtils.verifyAudioStateBroadcast(device, newState, prevState, intent);
}
/**
* Verify the series of invocations after
* {@link BluetoothHeadset#startScoUsingVirtualVoiceCall()}
*
* @param connectedDevices must be in the same sequence as
* {@link BluetoothHeadset#getConnectedDevices()}
*/
private void verifyVirtualCallStartSequenceInvocations(List<BluetoothDevice> connectedDevices) {
// Do not verify HeadsetPhoneState changes as it is verified in HeadsetServiceTest
verifyCallStateToNativeInvocation(
new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_DIALING, "", 0, ""),
connectedDevices);
verifyCallStateToNativeInvocation(
new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_ALERTING, "", 0, ""),
connectedDevices);
verifyCallStateToNativeInvocation(
new HeadsetCallState(1, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, ""),
connectedDevices);
}
private void verifyVirtualCallStopSequenceInvocations(List<BluetoothDevice> connectedDevices) {
verifyCallStateToNativeInvocation(
new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, ""),
connectedDevices);
}
private void verifyCallStateToNativeInvocation(HeadsetCallState headsetCallState,
List<BluetoothDevice> connectedDevices) {
for (BluetoothDevice device : connectedDevices) {
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).phoneStateChange(device,
headsetCallState);
}
}
}