blob: 6d82e5d0abf3d2b0b4b753bf5baae2323b708cb2 [file] [log] [blame]
/*
* Copyright (C) 2017 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.mapclient;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static org.mockito.Mockito.*;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.SdpMasRecord;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.provider.Telephony.Sms;
import android.telephony.SubscriptionManager;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
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 com.android.vcard.VCardConstants;
import com.android.vcard.VCardEntry;
import com.android.vcard.VCardProperty;
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.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.HashMap;
import java.util.Map;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class MapClientStateMachineTest {
private static final String TAG = "MapStateMachineTest";
private static final String FOLDER_SENT = "sent";
private static final int ASYNC_CALL_TIMEOUT_MILLIS = 100;
private static final int DISCONNECT_TIMEOUT = 3000;
@Rule
public final ServiceTestRule mServiceRule = new ServiceTestRule();
private BluetoothAdapter mAdapter;
private MceStateMachine mMceStateMachine = null;
private BluetoothDevice mTestDevice;
private Context mTargetContext;
private Handler mHandler;
private ArgumentCaptor<Intent> mIntentArgument = ArgumentCaptor.forClass(Intent.class);
@Mock
private AdapterService mAdapterService;
@Mock
private DatabaseManager mDatabaseManager;
@Mock
private MapClientService mMockMapClientService;
private MockContentResolver mMockContentResolver;
private MockSmsContentProvider mMockContentProvider;
@Mock
private MasClient mMockMasClient;
@Mock
private RequestPushMessage mMockRequestPushMessage;
@Mock
private SubscriptionManager mMockSubscriptionManager;
@Before
public void setUp() throws Exception {
mTargetContext = InstrumentationRegistry.getTargetContext();
Assume.assumeTrue("Ignore test when MapClientService is not enabled",
mTargetContext.getResources().getBoolean(R.bool.profile_supported_mapmce));
MockitoAnnotations.initMocks(this);
mMockContentProvider = new MockSmsContentProvider();
mMockContentResolver = new MockContentResolver();
TestUtils.setAdapterService(mAdapterService);
when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
doReturn(true, false).when(mAdapterService).isStartedProfile(anyString());
TestUtils.startService(mServiceRule, MapClientService.class);
mMockContentResolver.addProvider("sms", mMockContentProvider);
mMockContentResolver.addProvider("mms", mMockContentProvider);
mMockContentResolver.addProvider("mms-sms", mMockContentProvider);
when(mMockMapClientService.getContentResolver()).thenReturn(mMockContentResolver);
when(mMockMapClientService.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE))
.thenReturn(mMockSubscriptionManager);
when(mMockMapClientService.getSystemServiceName(SubscriptionManager.class))
.thenReturn(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
doReturn(mTargetContext.getResources()).when(mMockMapClientService).getResources();
// This line must be called to make sure relevant objects are initialized properly
mAdapter = BluetoothAdapter.getDefaultAdapter();
// Get a device for testing
mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
when(mMockMasClient.makeRequest(any(Request.class))).thenReturn(true);
mMceStateMachine = new MceStateMachine(mMockMapClientService, mTestDevice, mMockMasClient);
Assert.assertNotNull(mMceStateMachine);
if (Looper.myLooper() == null) {
Looper.prepare();
}
mHandler = new Handler();
}
@After
public void tearDown() throws Exception {
if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_mapmce)) {
return;
}
if (mMceStateMachine != null) {
mMceStateMachine.doQuit();
}
TestUtils.stopService(mServiceRule, MapClientService.class);
TestUtils.clearAdapterService(mAdapterService);
}
/**
* Test that default state is STATE_CONNECTING
*/
@Test
public void testDefaultConnectingState() {
Log.i(TAG, "in testDefaultConnectingState");
Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, mMceStateMachine.getState());
}
/**
* Test transition from STATE_CONNECTING --> (receive MSG_MAS_DISCONNECTED) -->
* STATE_DISCONNECTED
*/
@Test
public void testStateTransitionFromConnectingToDisconnected() {
Log.i(TAG, "in testStateTransitionFromConnectingToDisconnected");
setupSdpRecordReceipt();
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_DISCONNECTED);
mMceStateMachine.sendMessage(msg);
// Wait until the message is processed and a broadcast request is sent to
// to MapClientService to change
// state from STATE_CONNECTING to STATE_DISCONNECTED
verify(mMockMapClientService,
timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, mMceStateMachine.getState());
}
/**
* Test transition from STATE_CONNECTING --> (receive MSG_MAS_CONNECTED) --> STATE_CONNECTED
*/
@Test
public void testStateTransitionFromConnectingToConnected() {
Log.i(TAG, "in testStateTransitionFromConnectingToConnected");
setupSdpRecordReceipt();
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_CONNECTED);
mMceStateMachine.sendMessage(msg);
// Wait until the message is processed and a broadcast request is sent to
// to MapClientService to change
// state from STATE_CONNECTING to STATE_CONNECTED
verify(mMockMapClientService,
timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
}
/**
* Test transition from STATE_CONNECTING --> (receive MSG_MAS_CONNECTED) --> STATE_CONNECTED -->
* (receive MSG_MAS_DISCONNECTED) --> STATE_DISCONNECTED
*/
@Test
public void testStateTransitionFromConnectedWithMasDisconnected() {
Log.i(TAG, "in testStateTransitionFromConnectedWithMasDisconnected");
setupSdpRecordReceipt();
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_CONNECTED);
mMceStateMachine.sendMessage(msg);
// Wait until the message is processed and a broadcast request is sent to
// to MapClientService to change
// state from STATE_CONNECTING to STATE_CONNECTED
verify(mMockMapClientService,
timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_DISCONNECTED);
mMceStateMachine.sendMessage(msg);
verify(mMockMapClientService,
timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(4)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, mMceStateMachine.getState());
}
/**
* Test receiving an empty event report
*/
@Test
public void testReceiveEmptyEvent() {
setupSdpRecordReceipt();
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_CONNECTED);
mMceStateMachine.sendMessage(msg);
// Wait until the message is processed and a broadcast request is sent to
// to MapClientService to change
// state from STATE_CONNECTING to STATE_CONNECTED
verify(mMockMapClientService,
timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
// Send an empty notification event, verify the mMceStateMachine is still connected
Message notification = Message.obtain(mHandler, MceStateMachine.MSG_NOTIFICATION);
mMceStateMachine.getCurrentState().processMessage(msg);
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
}
/**
* Test set message status
*/
@Test
public void testSetMessageStatus() {
setupSdpRecordReceipt();
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_CONNECTED);
mMceStateMachine.sendMessage(msg);
// Wait until the message is processed and a broadcast request is sent to
// to MapClientService to change
// state from STATE_CONNECTING to STATE_CONNECTED
verify(mMockMapClientService,
timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
Assert.assertTrue(
mMceStateMachine.setMessageStatus("123456789AB", BluetoothMapClient.READ));
}
/**
* Test disconnect
*/
@Test
public void testDisconnect() {
setupSdpRecordReceipt();
doAnswer(invocation -> {
mMceStateMachine.sendMessage(MceStateMachine.MSG_MAS_DISCONNECTED);
return null;
}).when(mMockMasClient).shutdown();
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_CONNECTED);
mMceStateMachine.sendMessage(msg);
// Wait until the message is processed and a broadcast request is sent to
// to MapClientService to change
// state from STATE_CONNECTING to STATE_CONNECTED
verify(mMockMapClientService,
timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
mMceStateMachine.disconnect();
verify(mMockMapClientService,
timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(4)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, mMceStateMachine.getState());
}
/**
* Test disconnect timeout
*/
@Test
public void testDisconnectTimeout() {
setupSdpRecordReceipt();
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_CONNECTED);
mMceStateMachine.sendMessage(msg);
// Wait until the message is processed and a broadcast request is sent to
// to MapClientService to change
// state from STATE_CONNECTING to STATE_CONNECTED
verify(mMockMapClientService,
timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
mMceStateMachine.disconnect();
verify(mMockMapClientService,
after(DISCONNECT_TIMEOUT / 2).times(3)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTING, mMceStateMachine.getState());
verify(mMockMapClientService,
timeout(DISCONNECT_TIMEOUT).times(4)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, mMceStateMachine.getState());
}
/**
* Test sending a message
*/
@Test
public void testSendSMSMessage() {
setupSdpRecordReceipt();
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_CONNECTED);
mMceStateMachine.sendMessage(msg);
TestUtils.waitForLooperToFinishScheduledTask(mMceStateMachine.getHandler().getLooper());
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
String testMessage = "Hello World!";
Uri[] contacts = new Uri[] {Uri.parse("tel://5551212")};
verify(mMockMasClient, times(0)).makeRequest(any(RequestPushMessage.class));
mMceStateMachine.sendMapMessage(contacts, testMessage, null, null);
verify(mMockMasClient, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1))
.makeRequest(any(RequestPushMessage.class));
}
/**
* Test message sent successfully
*/
@Test
public void testSMSMessageSent() {
setupSdpRecordReceipt();
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_CONNECTED);
mMceStateMachine.sendMessage(msg);
TestUtils.waitForLooperToFinishScheduledTask(mMceStateMachine.getHandler().getLooper());
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
String testMessage = "Hello World!";
VCardEntry recipient = new VCardEntry();
VCardProperty property = new VCardProperty();
property.setName(VCardConstants.PROPERTY_TEL);
property.addValues("555-1212");
recipient.addProperty(property);
Bmessage testBmessage = new Bmessage();
testBmessage.setType(Bmessage.Type.SMS_GSM);
testBmessage.setBodyContent(testMessage);
testBmessage.addRecipient(recipient);
RequestPushMessage testRequest =
new RequestPushMessage(FOLDER_SENT, testBmessage, null, false, false);
when(mMockRequestPushMessage.getMsgHandle()).thenReturn("12345");
when(mMockRequestPushMessage.getBMsg()).thenReturn(testBmessage);
Message msgSent = Message.obtain(mHandler, MceStateMachine.MSG_MAS_REQUEST_COMPLETED,
mMockRequestPushMessage);
mMceStateMachine.sendMessage(msgSent);
TestUtils.waitForLooperToFinishScheduledTask(mMceStateMachine.getHandler().getLooper());
Assert.assertEquals(1, mMockContentProvider.mInsertOperationCount);
}
private void setupSdpRecordReceipt() {
// Perform first part of MAP connection logic.
verify(mMockMapClientService,
timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendBroadcast(
mIntentArgument.capture(), eq(BLUETOOTH_CONNECT),
any(Bundle.class));
Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, mMceStateMachine.getState());
// Setup receipt of SDP record
SdpMasRecord record = new SdpMasRecord(1, 1, 1, 1, 1, 1, "MasRecord");
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_SDP_DONE, record);
mMceStateMachine.sendMessage(msg);
}
private class MockSmsContentProvider extends MockContentProvider {
Map<Uri, ContentValues> mContentValues = new HashMap<>();
int mInsertOperationCount = 0;
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
mInsertOperationCount++;
return Uri.withAppendedPath(Sms.CONTENT_URI, String.valueOf(mInsertOperationCount));
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Cursor cursor = Mockito.mock(Cursor.class);
when(cursor.moveToFirst()).thenReturn(true);
when(cursor.moveToNext()).thenReturn(true).thenReturn(false);
when(cursor.getLong(anyInt())).thenReturn((long) mContentValues.size());
when(cursor.getString(anyInt())).thenReturn(String.valueOf(mContentValues.size()));
return cursor;
}
}
}