/* * Copyright 2016 The WebRTC Project Authors. All rights reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ package org.appspot.apprtc; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.util.Log; import androidx.test.core.app.ApplicationProvider; import java.util.ArrayList; import java.util.List; import org.appspot.apprtc.AppRTCBluetoothManager.State; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLog; import org.robolectric.RobolectricTestRunner; /** * Verifies basic behavior of the AppRTCBluetoothManager class. * Note that the test object uses an AppRTCAudioManager (injected in ctor), * but a mocked version is used instead. Hence, the parts "driven" by the AppRTC * audio manager are not included in this test. */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) public class BluetoothManagerTest { private static final String TAG = "BluetoothManagerTest"; private static final String BLUETOOTH_TEST_DEVICE_NAME = "BluetoothTestDevice"; private BroadcastReceiver bluetoothHeadsetStateReceiver; private BluetoothProfile.ServiceListener bluetoothServiceListener; private BluetoothHeadset mockedBluetoothHeadset; private BluetoothDevice mockedBluetoothDevice; private List mockedBluetoothDeviceList; private AppRTCBluetoothManager bluetoothManager; private AppRTCAudioManager mockedAppRtcAudioManager; private AudioManager mockedAudioManager; private Context context; @Before public void setUp() { ShadowLog.stream = System.out; context = ApplicationProvider.getApplicationContext(); mockedAppRtcAudioManager = mock(AppRTCAudioManager.class); mockedAudioManager = mock(AudioManager.class); mockedBluetoothHeadset = mock(BluetoothHeadset.class); mockedBluetoothDevice = mock(BluetoothDevice.class); mockedBluetoothDeviceList = new ArrayList(); // Simulate that bluetooth SCO audio is available by default. when(mockedAudioManager.isBluetoothScoAvailableOffCall()).thenReturn(true); // Create the test object and override protected methods for this test. bluetoothManager = new AppRTCBluetoothManager(context, mockedAppRtcAudioManager) { @Override protected AudioManager getAudioManager(Context context) { Log.d(TAG, "getAudioManager"); return mockedAudioManager; } @Override protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { Log.d(TAG, "registerReceiver"); if (filter.hasAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) && filter.hasAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { // Gives access to the real broadcast receiver so the test can use it. bluetoothHeadsetStateReceiver = receiver; } } @Override protected void unregisterReceiver(BroadcastReceiver receiver) { Log.d(TAG, "unregisterReceiver"); if (receiver == bluetoothHeadsetStateReceiver) { bluetoothHeadsetStateReceiver = null; } } @Override protected boolean getBluetoothProfileProxy( Context context, BluetoothProfile.ServiceListener listener, int profile) { Log.d(TAG, "getBluetoothProfileProxy"); if (profile == BluetoothProfile.HEADSET) { // Allows the test to access the real Bluetooth service listener object. bluetoothServiceListener = listener; } return true; } @Override protected boolean hasPermission(Context context, String permission) { Log.d(TAG, "hasPermission(" + permission + ")"); // Ensure that the client asks for Bluetooth permission. return android.Manifest.permission.BLUETOOTH.equals(permission); } @Override protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { // Do nothing in tests. No need to mock BluetoothAdapter. } }; } // Verify that Bluetooth service listener for headset profile is properly initialized. @Test public void testBluetoothServiceListenerInitialized() { bluetoothManager.start(); assertNotNull(bluetoothServiceListener); verify(mockedAppRtcAudioManager, never()).updateAudioDeviceState(); } // Verify that broadcast receivers for Bluetooth SCO audio state and Bluetooth headset state // are properly registered and unregistered. @Test public void testBluetoothBroadcastReceiversAreRegistered() { bluetoothManager.start(); assertNotNull(bluetoothHeadsetStateReceiver); bluetoothManager.stop(); assertNull(bluetoothHeadsetStateReceiver); } // Verify that the Bluetooth manager starts and stops with correct states. @Test public void testBluetoothDefaultStartStopStates() { bluetoothManager.start(); assertEquals(State.HEADSET_UNAVAILABLE, bluetoothManager.getState()); bluetoothManager.stop(); assertEquals(State.UNINITIALIZED, bluetoothManager.getState()); } // Verify correct state after receiving BluetoothServiceListener.onServiceConnected() // when no BT device is enabled. @Test public void testBluetoothServiceListenerConnectedWithNoHeadset() { bluetoothManager.start(); assertEquals(State.HEADSET_UNAVAILABLE, bluetoothManager.getState()); simulateBluetoothServiceConnectedWithNoConnectedHeadset(); verify(mockedAppRtcAudioManager, times(1)).updateAudioDeviceState(); assertEquals(State.HEADSET_UNAVAILABLE, bluetoothManager.getState()); } // Verify correct state after receiving BluetoothServiceListener.onServiceConnected() // when one emulated (test) BT device is enabled. Android does not support more than // one connected BT headset. @Test public void testBluetoothServiceListenerConnectedWithHeadset() { bluetoothManager.start(); assertEquals(State.HEADSET_UNAVAILABLE, bluetoothManager.getState()); simulateBluetoothServiceConnectedWithConnectedHeadset(); verify(mockedAppRtcAudioManager, times(1)).updateAudioDeviceState(); assertEquals(State.HEADSET_AVAILABLE, bluetoothManager.getState()); } // Verify correct state after receiving BluetoothProfile.ServiceListener.onServiceDisconnected(). @Test public void testBluetoothServiceListenerDisconnected() { bluetoothManager.start(); assertEquals(State.HEADSET_UNAVAILABLE, bluetoothManager.getState()); simulateBluetoothServiceDisconnected(); verify(mockedAppRtcAudioManager, times(1)).updateAudioDeviceState(); assertEquals(State.HEADSET_UNAVAILABLE, bluetoothManager.getState()); } // Verify correct state after BluetoothServiceListener.onServiceConnected() and // the intent indicating that the headset is actually connected. Both these callbacks // results in calls to updateAudioDeviceState() on the AppRTC audio manager. // No BT SCO is enabled here to keep the test limited. @Test public void testBluetoothHeadsetConnected() { bluetoothManager.start(); assertEquals(State.HEADSET_UNAVAILABLE, bluetoothManager.getState()); simulateBluetoothServiceConnectedWithConnectedHeadset(); simulateBluetoothHeadsetConnected(); verify(mockedAppRtcAudioManager, times(2)).updateAudioDeviceState(); assertEquals(State.HEADSET_AVAILABLE, bluetoothManager.getState()); } // Verify correct state sequence for a case when a BT headset is available, // followed by BT SCO audio being enabled and then stopped. @Test public void testBluetoothScoAudioStartAndStop() { bluetoothManager.start(); assertEquals(State.HEADSET_UNAVAILABLE, bluetoothManager.getState()); simulateBluetoothServiceConnectedWithConnectedHeadset(); assertEquals(State.HEADSET_AVAILABLE, bluetoothManager.getState()); bluetoothManager.startScoAudio(); assertEquals(State.SCO_CONNECTING, bluetoothManager.getState()); simulateBluetoothScoConnectionConnected(); assertEquals(State.SCO_CONNECTED, bluetoothManager.getState()); bluetoothManager.stopScoAudio(); simulateBluetoothScoConnectionDisconnected(); assertEquals(State.SCO_DISCONNECTING,bluetoothManager.getState()); bluetoothManager.stop(); assertEquals(State.UNINITIALIZED, bluetoothManager.getState()); verify(mockedAppRtcAudioManager, times(3)).updateAudioDeviceState(); } /** * Private helper methods. */ private void simulateBluetoothServiceConnectedWithNoConnectedHeadset() { mockedBluetoothDeviceList.clear(); when(mockedBluetoothHeadset.getConnectedDevices()).thenReturn(mockedBluetoothDeviceList); bluetoothServiceListener.onServiceConnected(BluetoothProfile.HEADSET, mockedBluetoothHeadset); // In real life, the AppRTC audio manager makes this call. bluetoothManager.updateDevice(); } private void simulateBluetoothServiceConnectedWithConnectedHeadset() { mockedBluetoothDeviceList.clear(); mockedBluetoothDeviceList.add(mockedBluetoothDevice); when(mockedBluetoothHeadset.getConnectedDevices()).thenReturn(mockedBluetoothDeviceList); when(mockedBluetoothDevice.getName()).thenReturn(BLUETOOTH_TEST_DEVICE_NAME); bluetoothServiceListener.onServiceConnected(BluetoothProfile.HEADSET, mockedBluetoothHeadset); // In real life, the AppRTC audio manager makes this call. bluetoothManager.updateDevice(); } private void simulateBluetoothServiceDisconnected() { bluetoothServiceListener.onServiceDisconnected(BluetoothProfile.HEADSET); } private void simulateBluetoothHeadsetConnected() { Intent intent = new Intent(); intent.setAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); intent.putExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_CONNECTED); bluetoothHeadsetStateReceiver.onReceive(context, intent); } private void simulateBluetoothScoConnectionConnected() { Intent intent = new Intent(); intent.setAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); intent.putExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_CONNECTED); bluetoothHeadsetStateReceiver.onReceive(context, intent); } private void simulateBluetoothScoConnectionDisconnected() { Intent intent = new Intent(); intent.setAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); intent.putExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); bluetoothHeadsetStateReceiver.onReceive(context, intent); } }