| /* |
| * Copyright 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.btservice; |
| |
| import static android.Manifest.permission.BLUETOOTH_SCAN; |
| import static android.bluetooth.BluetoothAdapter.STATE_BLE_ON; |
| import static android.bluetooth.BluetoothAdapter.STATE_BLE_TURNING_OFF; |
| import static android.bluetooth.BluetoothAdapter.STATE_BLE_TURNING_ON; |
| import static android.bluetooth.BluetoothAdapter.STATE_OFF; |
| import static android.bluetooth.BluetoothAdapter.STATE_ON; |
| import static android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF; |
| import static android.bluetooth.BluetoothAdapter.STATE_TURNING_ON; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.Mockito.*; |
| |
| import android.app.AlarmManager; |
| import android.app.AppOpsManager; |
| import android.app.admin.DevicePolicyManager; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothManager; |
| import android.bluetooth.IBluetoothCallback; |
| import android.companion.CompanionDeviceManager; |
| import android.content.Context; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PermissionInfo; |
| import android.content.res.Resources; |
| import android.hardware.display.DisplayManager; |
| import android.media.AudioManager; |
| import android.os.BatteryStatsManager; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.PowerManager; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.os.test.TestLooper; |
| import android.permission.PermissionCheckerManager; |
| import android.permission.PermissionManager; |
| import android.provider.Settings; |
| import android.sysprop.BluetoothProperties; |
| 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.runner.AndroidJUnit4; |
| |
| import com.android.bluetooth.TestUtils; |
| import com.android.bluetooth.Utils; |
| import com.android.bluetooth.a2dp.A2dpService; |
| import com.android.bluetooth.a2dpsink.A2dpSinkService; |
| import com.android.bluetooth.avrcp.AvrcpTargetService; |
| import com.android.bluetooth.avrcpcontroller.AvrcpControllerService; |
| import com.android.bluetooth.bas.BatteryService; |
| import com.android.bluetooth.bass_client.BassClientService; |
| import com.android.bluetooth.csip.CsipSetCoordinatorService; |
| import com.android.bluetooth.gatt.GattService; |
| import com.android.bluetooth.hap.HapClientService; |
| import com.android.bluetooth.hearingaid.HearingAidService; |
| import com.android.bluetooth.hfp.HeadsetService; |
| import com.android.bluetooth.hfpclient.HeadsetClientService; |
| import com.android.bluetooth.hid.HidDeviceService; |
| import com.android.bluetooth.hid.HidHostService; |
| import com.android.bluetooth.le_audio.LeAudioService; |
| import com.android.bluetooth.map.BluetoothMapService; |
| import com.android.bluetooth.mapclient.MapClientService; |
| import com.android.bluetooth.mcp.McpService; |
| import com.android.bluetooth.opp.BluetoothOppService; |
| import com.android.bluetooth.pan.PanService; |
| import com.android.bluetooth.pbap.BluetoothPbapService; |
| import com.android.bluetooth.pbapclient.PbapClientService; |
| import com.android.bluetooth.sap.SapService; |
| import com.android.bluetooth.tbs.TbsService; |
| import com.android.bluetooth.vc.VolumeControlService; |
| import com.android.internal.app.IBatteryStats; |
| |
| import libcore.util.HexEncoding; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.security.InvalidKeyException; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.stream.IntStream; |
| |
| import javax.crypto.Mac; |
| import javax.crypto.spec.SecretKeySpec; |
| |
| @MediumTest |
| @RunWith(AndroidJUnit4.class) |
| public class AdapterServiceTest { |
| private static final String TAG = AdapterServiceTest.class.getSimpleName(); |
| private static final String TEST_BT_ADDR_1 = "00:11:22:33:44:55"; |
| private static final String TEST_BT_ADDR_2 = "00:11:22:33:44:66"; |
| |
| private static final int MESSAGE_PROFILE_SERVICE_STATE_CHANGED = 1; |
| private static final int MESSAGE_PROFILE_SERVICE_REGISTERED = 2; |
| private static final int MESSAGE_PROFILE_SERVICE_UNREGISTERED = 3; |
| |
| private AdapterService mAdapterService; |
| |
| private @Mock Context mMockContext; |
| private @Mock ApplicationInfo mMockApplicationInfo; |
| private @Mock Resources mMockResources; |
| private @Mock ProfileService mMockGattService; |
| private @Mock ProfileService mMockService; |
| private @Mock ProfileService mMockService2; |
| private @Mock IBluetoothCallback mIBluetoothCallback; |
| private @Mock Binder mBinder; |
| private @Mock android.app.Application mApplication; |
| private @Mock MetricsLogger mMockMetricsLogger; |
| |
| // SystemService that are not mocked |
| private BluetoothManager mBluetoothManager; |
| private CompanionDeviceManager mCompanionDeviceManager; |
| private DisplayManager mDisplayManager; |
| private PowerManager mPowerManager; |
| private PermissionCheckerManager mPermissionCheckerManager; |
| private PermissionManager mPermissionManager; |
| // BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the |
| // underlying binder calls. |
| final BatteryStatsManager mBatteryStatsManager = |
| new BatteryStatsManager(mock(IBatteryStats.class)); |
| |
| private static final int CONTEXT_SWITCH_MS = 100; |
| private static final int PROFILE_SERVICE_TOGGLE_TIME_MS = 200; |
| private static final int GATT_START_TIME_MS = 1000; |
| private static final int ONE_SECOND_MS = 1000; |
| private static final int NATIVE_INIT_MS = 8000; |
| private static final int NATIVE_DISABLE_MS = 8000; |
| |
| |
| private PackageManager mMockPackageManager; |
| private MockContentResolver mMockContentResolver; |
| private HashMap<String, HashMap<String, String>> mAdapterConfig; |
| private int mForegroundUserId; |
| private TestLooper mLooper; |
| |
| static void configureEnabledProfiles() { |
| Log.e(TAG, "configureEnabledProfiles"); |
| Config.setProfileEnabled(PanService.class, true); |
| Config.setProfileEnabled(BluetoothPbapService.class, true); |
| Config.setProfileEnabled(GattService.class, true); |
| |
| Config.setProfileEnabled(A2dpService.class, false); |
| Config.setProfileEnabled(A2dpSinkService.class, false); |
| Config.setProfileEnabled(AvrcpTargetService.class, false); |
| Config.setProfileEnabled(AvrcpControllerService.class, false); |
| Config.setProfileEnabled(BassClientService.class, false); |
| Config.setProfileEnabled(BatteryService.class, false); |
| Config.setProfileEnabled(CsipSetCoordinatorService.class, false); |
| Config.setProfileEnabled(HapClientService.class, false); |
| Config.setProfileEnabled(HeadsetService.class, false); |
| Config.setProfileEnabled(HeadsetClientService.class, false); |
| Config.setProfileEnabled(HearingAidService.class, false); |
| Config.setProfileEnabled(HidDeviceService.class, false); |
| Config.setProfileEnabled(HidHostService.class, false); |
| Config.setProfileEnabled(LeAudioService.class, false); |
| Config.setProfileEnabled(TbsService.class, false); |
| Config.setProfileEnabled(BluetoothMapService.class, false); |
| Config.setProfileEnabled(MapClientService.class, false); |
| Config.setProfileEnabled(McpService.class, false); |
| Config.setProfileEnabled(BluetoothOppService.class, false); |
| Config.setProfileEnabled(PbapClientService.class, false); |
| Config.setProfileEnabled(SapService.class, false); |
| Config.setProfileEnabled(VolumeControlService.class, false); |
| } |
| |
| @BeforeClass |
| public static void setupClass() { |
| Log.e(TAG, "setupClass"); |
| // Bring native layer up and down to make sure config files are properly loaded |
| if (Looper.myLooper() == null) { |
| Looper.prepare(); |
| } |
| assertThat(Looper.myLooper()).isNotNull(); |
| AdapterService adapterService = new AdapterService(Looper.myLooper()); |
| adapterService.initNative(false /* is_restricted */, false /* is_common_criteria_mode */, |
| 0 /* config_compare_result */, new String[0], false, ""); |
| adapterService.cleanupNative(); |
| HashMap<String, HashMap<String, String>> adapterConfig = TestUtils.readAdapterConfig(); |
| assertThat(adapterConfig).isNotNull(); |
| assertThat(getMetricsSalt(adapterConfig)).isNotNull(); |
| } |
| |
| <T> void mockGetSystemService(String serviceName, Class<T> serviceClass, T mockService) { |
| TestUtils.mockGetSystemService(mMockContext, serviceName, serviceClass, mockService); |
| } |
| |
| <T> T mockGetSystemService(String serviceName, Class<T> serviceClass) { |
| return TestUtils.mockGetSystemService(mMockContext, serviceName, serviceClass); |
| } |
| |
| @Before |
| public void setUp() throws PackageManager.NameNotFoundException { |
| Log.e(TAG, "setUp()"); |
| MockitoAnnotations.initMocks(this); |
| |
| mLooper = new TestLooper(); |
| Handler handler = new Handler(mLooper.getLooper()); |
| |
| // Post the creation of AdapterService since it rely on Looper.myLooper() |
| handler.post(() -> mAdapterService = new AdapterService(mLooper.getLooper())); |
| assertThat(mLooper.dispatchAll()).isEqualTo(1); |
| assertThat(mAdapterService).isNotNull(); |
| |
| mMockPackageManager = mock(PackageManager.class); |
| when(mMockPackageManager.getPermissionInfo(any(), anyInt())) |
| .thenReturn(new PermissionInfo()); |
| |
| Context targetContext = InstrumentationRegistry.getTargetContext(); |
| |
| mMockContentResolver = new MockContentResolver(targetContext); |
| mMockContentResolver.addProvider(Settings.AUTHORITY, new MockContentProvider() { |
| @Override |
| public Bundle call(String method, String request, Bundle args) { |
| return Bundle.EMPTY; |
| } |
| }); |
| |
| mBluetoothManager = targetContext.getSystemService(BluetoothManager.class); |
| mCompanionDeviceManager = targetContext.getSystemService(CompanionDeviceManager.class); |
| mDisplayManager = targetContext.getSystemService(DisplayManager.class); |
| mPermissionCheckerManager = targetContext.getSystemService(PermissionCheckerManager.class); |
| mPermissionManager = targetContext.getSystemService(PermissionManager.class); |
| mPowerManager = targetContext.getSystemService(PowerManager.class); |
| |
| when(mMockContext.getCacheDir()).thenReturn(targetContext.getCacheDir()); |
| when(mMockContext.getUser()).thenReturn(targetContext.getUser()); |
| when(mMockContext.getPackageName()).thenReturn(targetContext.getPackageName()); |
| when(mMockContext.getApplicationInfo()).thenReturn(mMockApplicationInfo); |
| when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver); |
| when(mMockContext.getApplicationContext()).thenReturn(mMockContext); |
| when(mMockContext.createContextAsUser(UserHandle.SYSTEM, /* flags= */ 0)).thenReturn( |
| mMockContext); |
| when(mMockContext.getResources()).thenReturn(mMockResources); |
| when(mMockContext.getUserId()).thenReturn(Process.BLUETOOTH_UID); |
| when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); |
| |
| mockGetSystemService(Context.ALARM_SERVICE, AlarmManager.class); |
| mockGetSystemService(Context.APP_OPS_SERVICE, AppOpsManager.class); |
| mockGetSystemService(Context.AUDIO_SERVICE, AudioManager.class); |
| DevicePolicyManager dpm = |
| mockGetSystemService(Context.DEVICE_POLICY_SERVICE, DevicePolicyManager.class); |
| doReturn(false).when(dpm).isCommonCriteriaModeEnabled(any()); |
| mockGetSystemService(Context.USER_SERVICE, UserManager.class); |
| |
| mockGetSystemService( |
| Context.BATTERY_STATS_SERVICE, BatteryStatsManager.class, mBatteryStatsManager); |
| mockGetSystemService(Context.BLUETOOTH_SERVICE, BluetoothManager.class, mBluetoothManager); |
| mockGetSystemService( |
| Context.COMPANION_DEVICE_SERVICE, |
| CompanionDeviceManager.class, |
| mCompanionDeviceManager); |
| mockGetSystemService(Context.DISPLAY_SERVICE, DisplayManager.class, mDisplayManager); |
| mockGetSystemService( |
| Context.PERMISSION_CHECKER_SERVICE, |
| PermissionCheckerManager.class, |
| mPermissionCheckerManager); |
| mockGetSystemService( |
| Context.PERMISSION_SERVICE, PermissionManager.class, mPermissionManager); |
| mockGetSystemService(Context.POWER_SERVICE, PowerManager.class, mPowerManager); |
| |
| when(mMockContext.getSharedPreferences(anyString(), anyInt())) |
| .thenReturn( |
| targetContext.getSharedPreferences( |
| "AdapterServiceTestPrefs", Context.MODE_PRIVATE)); |
| |
| doAnswer( |
| invocation -> { |
| Object[] args = invocation.getArguments(); |
| return targetContext.getDatabasePath((String) args[0]); |
| }) |
| .when(mMockContext) |
| .getDatabasePath(anyString()); |
| |
| // Sets the foreground user id to match that of the tests (restored in tearDown) |
| mForegroundUserId = Utils.getForegroundUserId(); |
| int callingUid = Binder.getCallingUid(); |
| UserHandle callingUser = UserHandle.getUserHandleForUid(callingUid); |
| Utils.setForegroundUserId(callingUser.getIdentifier()); |
| |
| when(mIBluetoothCallback.asBinder()).thenReturn(mBinder); |
| |
| doReturn(Process.BLUETOOTH_UID).when(mMockPackageManager) |
| .getPackageUidAsUser(any(), anyInt(), anyInt()); |
| |
| when(mMockGattService.getName()).thenReturn("GattService"); |
| when(mMockService.getName()).thenReturn("Service1"); |
| when(mMockService2.getName()).thenReturn("Service2"); |
| |
| when(mMockMetricsLogger.init(any())).thenReturn(true); |
| when(mMockMetricsLogger.close()).thenReturn(true); |
| |
| configureEnabledProfiles(); |
| Config.init(mMockContext); |
| |
| mAdapterService.setMetricsLogger(mMockMetricsLogger); |
| |
| // Attach a context to the service for permission checks. |
| mAdapterService.attach(mMockContext, null, null, null, mApplication, null); |
| mAdapterService.onCreate(); |
| |
| mLooper.dispatchAll(); |
| |
| mAdapterService.registerCallback(mIBluetoothCallback); |
| |
| mAdapterConfig = TestUtils.readAdapterConfig(); |
| assertThat(mAdapterConfig).isNotNull(); |
| } |
| |
| @After |
| public void tearDown() { |
| Log.e(TAG, "tearDown()"); |
| |
| // Restores the foregroundUserId to the ID prior to the test setup |
| Utils.setForegroundUserId(mForegroundUserId); |
| |
| mAdapterService.cleanup(); |
| mAdapterService.unregisterCallback(mIBluetoothCallback); |
| } |
| |
| /** |
| * Dispatch all the message on the Loopper and check that the `what` is expected |
| * |
| * @param what list of message that are expected to be run by the handler |
| */ |
| private void syncHandler(int... what) { |
| syncHandler(mLooper, what); |
| } |
| |
| private void dropNextMessage(int what) { |
| Message msg = mLooper.nextMessage(); |
| assertThat(msg).isNotNull(); |
| assertWithMessage("Not the expected Message:\n" + msg).that(msg.what).isEqualTo(what); |
| Log.d(TAG, "Message dropped on purpose: " + msg); |
| } |
| |
| private static void syncHandler(TestLooper looper, int... what) { |
| IntStream.of(what) |
| .forEach( |
| w -> { |
| Message msg = looper.nextMessage(); |
| assertThat(msg).isNotNull(); |
| assertWithMessage("Not the expected Message:\n" + msg) |
| .that(msg.what) |
| .isEqualTo(w); |
| Log.d(TAG, "Processing message: " + msg); |
| msg.getTarget().dispatchMessage(msg); |
| }); |
| } |
| |
| private void verifyStateChange(int prevState, int currState) { |
| try { |
| verify(mIBluetoothCallback).onBluetoothStateChange(prevState, currState); |
| } catch (RemoteException e) { |
| // the mocked onBluetoothStateChange doesn't throw RemoteException |
| } |
| } |
| |
| private void verifyStateChange(int prevState, int currState, int timeoutMs) { |
| try { |
| verify(mIBluetoothCallback, timeout(timeoutMs)) |
| .onBluetoothStateChange(prevState, currState); |
| } catch (RemoteException e) { |
| // the mocked onBluetoothStateChange doesn't throw RemoteException |
| } |
| } |
| |
| private static void verifyStateChange( |
| IBluetoothCallback cb, int prevState, int currState, int timeoutMs) { |
| try { |
| verify(cb, timeout(timeoutMs)).onBluetoothStateChange(prevState, currState); |
| } catch (RemoteException e) { |
| // the mocked onBluetoothStateChange doesn't throw RemoteException |
| } |
| } |
| |
| private static void verifyStateChange(IBluetoothCallback cb, int prevState, int currState) { |
| try { |
| verify(cb).onBluetoothStateChange(prevState, currState); |
| } catch (RemoteException e) { |
| // the mocked onBluetoothStateChange doesn't throw RemoteException |
| } |
| } |
| |
| static void offToBleOn( |
| TestLooper looper, |
| ProfileService gattService, |
| AdapterService adapter, |
| Context ctx, |
| IBluetoothCallback callback) { |
| adapter.enable(false); |
| syncHandler(looper, AdapterState.BLE_TURN_ON); |
| verifyStateChange(callback, STATE_OFF, STATE_BLE_TURNING_ON); |
| |
| syncHandler(looper, MESSAGE_PROFILE_SERVICE_REGISTERED); |
| |
| syncHandler(looper, MESSAGE_PROFILE_SERVICE_STATE_CHANGED); |
| |
| // Native loop is not in TestLooper and it will send a event later |
| looper.startAutoDispatch(); |
| verifyStateChange(callback, STATE_BLE_TURNING_ON, STATE_BLE_ON, NATIVE_INIT_MS); |
| looper.stopAutoDispatch(); // stop autoDispatch ASAP |
| |
| assertThat(adapter.getState()).isEqualTo(STATE_BLE_ON); |
| } |
| |
| static void onToBleOn( |
| TestLooper looper, |
| AdapterService adapter, |
| Context ctx, |
| IBluetoothCallback callback, |
| boolean onlyGatt, |
| List<ProfileService> services) { |
| adapter.disable(); |
| syncHandler(looper, AdapterState.USER_TURN_OFF); |
| verifyStateChange(callback, STATE_ON, STATE_TURNING_OFF); |
| |
| if (!onlyGatt) { |
| // Stop PBAP and PAN services |
| verify(ctx, times(4)).startService(any()); |
| |
| for (ProfileService service : services) { |
| adapter.onProfileServiceStateChanged(service, STATE_OFF); |
| syncHandler(looper, MESSAGE_PROFILE_SERVICE_STATE_CHANGED); |
| } |
| } |
| |
| syncHandler(looper, AdapterState.BREDR_STOPPED); |
| verifyStateChange(callback, STATE_TURNING_OFF, STATE_BLE_ON); |
| |
| assertThat(adapter.getState()).isEqualTo(STATE_BLE_ON); |
| } |
| |
| void doEnable(boolean onlyGatt) { |
| doEnable( |
| mLooper, |
| mMockGattService, |
| mAdapterService, |
| mMockContext, |
| onlyGatt, |
| List.of(mMockService, mMockService2)); |
| } |
| // Method is re-used in other AdapterService*Test |
| static void doEnable( |
| TestLooper looper, |
| ProfileService gattService, |
| AdapterService adapter, |
| Context ctx, |
| boolean onlyGatt, |
| List<ProfileService> services) { |
| Log.e(TAG, "doEnable() start"); |
| |
| IBluetoothCallback callback = mock(IBluetoothCallback.class); |
| Binder binder = mock(Binder.class); |
| doReturn(binder).when(callback).asBinder(); |
| adapter.registerCallback(callback); |
| |
| assertThat(adapter.getState()).isEqualTo(STATE_OFF); |
| |
| offToBleOn(looper, gattService, adapter, ctx, callback); |
| |
| adapter.startBrEdr(); |
| syncHandler(looper, AdapterState.USER_TURN_ON); |
| verifyStateChange(callback, STATE_BLE_ON, STATE_TURNING_ON); |
| |
| if (!onlyGatt) { |
| // Start Mock PBAP and PAN services |
| verify(ctx, times(2)).startService(any()); |
| |
| for (ProfileService service : services) { |
| adapter.addProfile(service); |
| syncHandler(looper, MESSAGE_PROFILE_SERVICE_REGISTERED); |
| } |
| // Keep in 2 separate loop to first add the services and then eventually trigger the |
| // ON transition during the callback |
| for (ProfileService service : services) { |
| adapter.onProfileServiceStateChanged(service, STATE_ON); |
| syncHandler(looper, MESSAGE_PROFILE_SERVICE_STATE_CHANGED); |
| } |
| } |
| syncHandler(looper, AdapterState.BREDR_STARTED); |
| verifyStateChange(callback, STATE_TURNING_ON, STATE_ON); |
| |
| assertThat(adapter.getState()).isEqualTo(STATE_ON); |
| adapter.unregisterCallback(callback); |
| Log.e(TAG, "doEnable() complete success"); |
| } |
| |
| void doDisable(boolean onlyGatt) { |
| doDisable( |
| mLooper, |
| mMockGattService, |
| mAdapterService, |
| mMockContext, |
| onlyGatt, |
| List.of(mMockService, mMockService2)); |
| } |
| |
| private static void doDisable( |
| TestLooper looper, |
| ProfileService gattService, |
| AdapterService adapter, |
| Context ctx, |
| boolean onlyGatt, |
| List<ProfileService> services) { |
| Log.e(TAG, "doDisable() start"); |
| IBluetoothCallback callback = mock(IBluetoothCallback.class); |
| Binder binder = mock(Binder.class); |
| doReturn(binder).when(callback).asBinder(); |
| adapter.registerCallback(callback); |
| |
| assertThat(adapter.getState()).isEqualTo(STATE_ON); |
| |
| onToBleOn(looper, adapter, ctx, callback, onlyGatt, services); |
| |
| adapter.stopBle(); |
| syncHandler(looper, AdapterState.BLE_TURN_OFF); |
| verifyStateChange(callback, STATE_BLE_ON, STATE_BLE_TURNING_OFF); |
| |
| syncHandler(looper, MESSAGE_PROFILE_SERVICE_STATE_CHANGED); |
| |
| // Native loop is not in TestLooper and it will send a event later |
| looper.startAutoDispatch(); |
| verifyStateChange(callback, STATE_BLE_TURNING_OFF, STATE_OFF, NATIVE_DISABLE_MS); |
| looper.stopAutoDispatch(); // stop autoDispatch ASAP |
| |
| assertThat(adapter.getState()).isEqualTo(STATE_OFF); |
| adapter.unregisterCallback(callback); |
| Log.e(TAG, "doDisable() complete success"); |
| } |
| |
| /** |
| * Test: Turn Bluetooth on. |
| * Check whether the AdapterService gets started. |
| */ |
| @Test |
| public void testEnable() { |
| doEnable(false); |
| } |
| |
| @Test |
| public void enable_isCorrectScanMode() { |
| doEnable(false); |
| |
| verify(mMockContext, timeout(ONE_SECOND_MS).times(2)) |
| .sendBroadcast(any(), eq(BLUETOOTH_SCAN), any(Bundle.class)); |
| |
| assertThat(mAdapterService.getScanMode()).isEqualTo(BluetoothAdapter.SCAN_MODE_CONNECTABLE); |
| } |
| |
| /** |
| * Test: Turn Bluetooth on/off. |
| * Check whether the AdapterService gets started and stopped. |
| */ |
| @Test |
| public void testEnableDisable() { |
| doEnable(false); |
| doDisable(false); |
| } |
| |
| /** |
| * Test: Turn Bluetooth on/off with only GATT supported. |
| * Check whether the AdapterService gets started and stopped. |
| */ |
| @Test |
| public void testEnableDisableOnlyGatt() { |
| Context mockContext = mock(Context.class); |
| Resources mockResources = mock(Resources.class); |
| |
| when(mockContext.getApplicationInfo()).thenReturn(mMockApplicationInfo); |
| when(mockContext.getContentResolver()).thenReturn(mMockContentResolver); |
| when(mockContext.getApplicationContext()).thenReturn(mockContext); |
| when(mockContext.getResources()).thenReturn(mockResources); |
| when(mockContext.getUserId()).thenReturn(Process.BLUETOOTH_UID); |
| when(mockContext.getPackageManager()).thenReturn(mMockPackageManager); |
| |
| // Config is set to PBAP, PAN and GATT by default. Turn off PAN and PBAP. |
| Config.setProfileEnabled(PanService.class, false); |
| Config.setProfileEnabled(BluetoothPbapService.class, false); |
| |
| Config.init(mockContext); |
| doEnable(true); |
| doDisable(true); |
| } |
| |
| /** |
| * Test: Don't start GATT |
| * Check whether the AdapterService quits gracefully |
| */ |
| @Test |
| public void testGattStartTimeout() { |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| |
| mAdapterService.enable(false); |
| syncHandler(AdapterState.BLE_TURN_ON); |
| verifyStateChange(STATE_OFF, STATE_BLE_TURNING_ON); |
| assertThat(mAdapterService.getBluetoothGatt()).isNotNull(); |
| syncHandler(MESSAGE_PROFILE_SERVICE_REGISTERED); |
| |
| // Fetch next message and never process it to simulate a timeout. |
| dropNextMessage(MESSAGE_PROFILE_SERVICE_STATE_CHANGED); |
| |
| mLooper.moveTimeForward(120_000); // Skip time so the timeout fires |
| syncHandler(AdapterState.BLE_START_TIMEOUT); |
| assertThat(mAdapterService.getBluetoothGatt()).isNull(); |
| |
| // Native loop is not in TestLooper and it will send a event later |
| mLooper.startAutoDispatch(); |
| verifyStateChange(STATE_BLE_TURNING_OFF, STATE_OFF, NATIVE_DISABLE_MS); |
| mLooper.stopAutoDispatch(); // stop autoDispatch ASAP |
| |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| } |
| |
| /** |
| * Test: Don't stop GATT |
| * Check whether the AdapterService quits gracefully |
| */ |
| @Test |
| public void testGattStopTimeout() { |
| doEnable(false); |
| |
| onToBleOn( |
| mLooper, |
| mAdapterService, |
| mMockContext, |
| mIBluetoothCallback, |
| false, |
| List.of(mMockService, mMockService2)); |
| |
| mAdapterService.stopBle(); |
| syncHandler(AdapterState.BLE_TURN_OFF); |
| verifyStateChange(STATE_BLE_ON, STATE_BLE_TURNING_OFF, CONTEXT_SWITCH_MS); |
| assertThat(mAdapterService.getBluetoothGatt()).isNull(); |
| |
| // Fetch Gatt message and never process it to simulate a timeout. |
| dropNextMessage(MESSAGE_PROFILE_SERVICE_STATE_CHANGED); |
| dropNextMessage(MESSAGE_PROFILE_SERVICE_UNREGISTERED); |
| |
| mLooper.moveTimeForward(120_000); // Skip time so the timeout fires |
| syncHandler(AdapterState.BLE_STOP_TIMEOUT); |
| verifyStateChange(STATE_BLE_TURNING_OFF, STATE_OFF); |
| |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| } |
| |
| /** |
| * Test: Don't start a classic profile |
| * Check whether the AdapterService quits gracefully |
| */ |
| @Test |
| public void testProfileStartTimeout() { |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| |
| offToBleOn(mLooper, mMockGattService, mAdapterService, mMockContext, mIBluetoothCallback); |
| |
| mAdapterService.startBrEdr(); |
| syncHandler(AdapterState.USER_TURN_ON); |
| verifyStateChange(STATE_BLE_ON, STATE_TURNING_ON); |
| verify(mMockContext, times(2)).startService(any()); // Register Mock PBAP and PAN services |
| |
| mAdapterService.addProfile(mMockService); |
| syncHandler(MESSAGE_PROFILE_SERVICE_REGISTERED); |
| mAdapterService.addProfile(mMockService2); |
| syncHandler(MESSAGE_PROFILE_SERVICE_REGISTERED); |
| mAdapterService.onProfileServiceStateChanged(mMockService, STATE_ON); |
| syncHandler(MESSAGE_PROFILE_SERVICE_STATE_CHANGED); |
| |
| // Skip onProfileServiceStateChanged for mMockService2 to be in the test situation |
| |
| mLooper.moveTimeForward(120_000); // Skip time so the timeout fires |
| syncHandler(AdapterState.BREDR_START_TIMEOUT); |
| |
| verifyStateChange(STATE_TURNING_ON, STATE_TURNING_OFF); |
| verify(mMockContext, times(4)).startService(any()); // Stop PBAP and PAN services |
| |
| mAdapterService.onProfileServiceStateChanged(mMockService, STATE_OFF); |
| syncHandler(MESSAGE_PROFILE_SERVICE_STATE_CHANGED); |
| syncHandler(AdapterState.BREDR_STOPPED); |
| verifyStateChange(STATE_TURNING_OFF, STATE_BLE_ON); |
| |
| // Ensure GATT is still running |
| assertThat(mAdapterService.getBluetoothGatt()).isNotNull(); |
| } |
| |
| /** |
| * Test: Don't stop a classic profile |
| * Check whether the AdapterService quits gracefully |
| */ |
| @Test |
| public void testProfileStopTimeout() { |
| doEnable(false); |
| |
| mAdapterService.disable(); |
| syncHandler(AdapterState.USER_TURN_OFF); |
| verifyStateChange(STATE_ON, STATE_TURNING_OFF); |
| verify(mMockContext, times(4)).startService(any()); |
| |
| mAdapterService.onProfileServiceStateChanged(mMockService, STATE_OFF); |
| syncHandler(MESSAGE_PROFILE_SERVICE_STATE_CHANGED); |
| |
| // Skip onProfileServiceStateChanged for mMockService2 to be in the test situation |
| |
| mLooper.moveTimeForward(120_000); // Skip time so the timeout fires |
| syncHandler(AdapterState.BREDR_STOP_TIMEOUT); |
| verifyStateChange(STATE_TURNING_OFF, STATE_BLE_TURNING_OFF); |
| |
| syncHandler(MESSAGE_PROFILE_SERVICE_STATE_CHANGED); |
| syncHandler(MESSAGE_PROFILE_SERVICE_UNREGISTERED); |
| |
| // TODO(b/280518177): The only timeout to fire here should be the BREDR |
| mLooper.moveTimeForward(120_000); // Skip time so the timeout fires |
| syncHandler(AdapterState.BLE_STOP_TIMEOUT); |
| verifyStateChange(STATE_BLE_TURNING_OFF, STATE_OFF); |
| |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| } |
| |
| /** |
| * Test: Toggle snoop logging setting |
| * Check whether the AdapterService restarts fully |
| */ |
| @Test |
| public void testSnoopLoggingChange() { |
| BluetoothProperties.snoop_log_mode_values snoopSetting = |
| BluetoothProperties.snoop_log_mode() |
| .orElse(BluetoothProperties.snoop_log_mode_values.EMPTY); |
| BluetoothProperties.snoop_log_mode(BluetoothProperties.snoop_log_mode_values.DISABLED); |
| doEnable(false); |
| |
| assertThat( |
| BluetoothProperties.snoop_log_mode() |
| .orElse(BluetoothProperties.snoop_log_mode_values.EMPTY)) |
| .isNotEqualTo(BluetoothProperties.snoop_log_mode_values.FULL); |
| |
| BluetoothProperties.snoop_log_mode(BluetoothProperties.snoop_log_mode_values.FULL); |
| |
| onToBleOn( |
| mLooper, |
| mAdapterService, |
| mMockContext, |
| mIBluetoothCallback, |
| false, |
| List.of(mMockService, mMockService2)); |
| |
| // Do not call stopBle(). The Adapter should turn itself off. |
| syncHandler(AdapterState.BLE_TURN_OFF); |
| verifyStateChange(STATE_BLE_ON, STATE_BLE_TURNING_OFF, CONTEXT_SWITCH_MS); |
| syncHandler(MESSAGE_PROFILE_SERVICE_STATE_CHANGED); // stop GATT |
| |
| // Native loop is not in TestLooper and it will send a event later |
| mLooper.startAutoDispatch(); |
| verifyStateChange(STATE_BLE_TURNING_OFF, STATE_OFF, NATIVE_DISABLE_MS); |
| mLooper.stopAutoDispatch(); // stop autoDispatch ASAP |
| |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| |
| // Restore earlier setting |
| BluetoothProperties.snoop_log_mode(snoopSetting); |
| } |
| |
| |
| /** |
| * Test: Obfuscate a null Bluetooth |
| * Check if returned value from {@link AdapterService#obfuscateAddress(BluetoothDevice)} is |
| * an empty array when device address is null |
| */ |
| @Test |
| public void testObfuscateBluetoothAddress_NullAddress() { |
| assertThat(mAdapterService.obfuscateAddress(null)).isEmpty(); |
| } |
| |
| /** |
| * Test: Obfuscate Bluetooth address when Bluetooth is disabled |
| * Check whether the returned value meets expectation |
| */ |
| @Test |
| public void testObfuscateBluetoothAddress_BluetoothDisabled() { |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| byte[] metricsSalt = getMetricsSalt(mAdapterConfig); |
| assertThat(metricsSalt).isNotNull(); |
| BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0); |
| byte[] obfuscatedAddress = mAdapterService.obfuscateAddress(device); |
| assertThat(obfuscatedAddress).isNotEmpty(); |
| assertThat(isByteArrayAllZero(obfuscatedAddress)).isFalse(); |
| assertThat(obfuscateInJava(metricsSalt, device)).isEqualTo(obfuscatedAddress); |
| } |
| |
| /** |
| * Test: Obfuscate Bluetooth address when Bluetooth is enabled |
| * Check whether the returned value meets expectation |
| */ |
| @Test |
| public void testObfuscateBluetoothAddress_BluetoothEnabled() { |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| doEnable(false); |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_ON); |
| byte[] metricsSalt = getMetricsSalt(mAdapterConfig); |
| assertThat(metricsSalt).isNotNull(); |
| BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0); |
| byte[] obfuscatedAddress = mAdapterService.obfuscateAddress(device); |
| assertThat(obfuscatedAddress).isNotEmpty(); |
| assertThat(isByteArrayAllZero(obfuscatedAddress)).isFalse(); |
| assertThat(obfuscateInJava(metricsSalt, device)).isEqualTo(obfuscatedAddress); |
| } |
| |
| /** |
| * Test: Check if obfuscated Bluetooth address stays the same after toggling Bluetooth |
| */ |
| @Test |
| public void testObfuscateBluetoothAddress_PersistentBetweenToggle() { |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| byte[] metricsSalt = getMetricsSalt(mAdapterConfig); |
| assertThat(metricsSalt).isNotNull(); |
| BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0); |
| byte[] obfuscatedAddress1 = mAdapterService.obfuscateAddress(device); |
| assertThat(obfuscatedAddress1).isNotEmpty(); |
| assertThat(isByteArrayAllZero(obfuscatedAddress1)).isFalse(); |
| assertThat(obfuscateInJava(metricsSalt, device)).isEqualTo(obfuscatedAddress1); |
| // Enable |
| doEnable(false); |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_ON); |
| byte[] obfuscatedAddress3 = mAdapterService.obfuscateAddress(device); |
| assertThat(obfuscatedAddress3).isNotEmpty(); |
| assertThat(isByteArrayAllZero(obfuscatedAddress3)).isFalse(); |
| assertThat(obfuscatedAddress3).isEqualTo(obfuscatedAddress1); |
| // Disable |
| doDisable(false); |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| byte[] obfuscatedAddress4 = mAdapterService.obfuscateAddress(device); |
| assertThat(obfuscatedAddress4).isNotEmpty(); |
| assertThat(isByteArrayAllZero(obfuscatedAddress4)).isFalse(); |
| assertThat(obfuscatedAddress4).isEqualTo(obfuscatedAddress1); |
| } |
| |
| @Test |
| public void testAddressConsolidation() { |
| // Create device properties |
| RemoteDevices remoteDevices = mAdapterService.getRemoteDevices(); |
| remoteDevices.addDeviceProperties(Utils.getBytesFromAddress((TEST_BT_ADDR_1))); |
| String identityAddress = mAdapterService.getIdentityAddress(TEST_BT_ADDR_1); |
| assertThat(identityAddress).isEqualTo(TEST_BT_ADDR_1); |
| |
| // Trigger address consolidate callback |
| remoteDevices.addressConsolidateCallback(Utils.getBytesFromAddress(TEST_BT_ADDR_1), |
| Utils.getBytesFromAddress(TEST_BT_ADDR_2)); |
| |
| // Verify we can get correct identity address |
| identityAddress = mAdapterService.getIdentityAddress(TEST_BT_ADDR_1); |
| assertThat(identityAddress).isEqualTo(TEST_BT_ADDR_2); |
| } |
| |
| public static byte[] getMetricsSalt(HashMap<String, HashMap<String, String>> adapterConfig) { |
| HashMap<String, String> metricsSection = adapterConfig.get("Metrics"); |
| if (metricsSection == null) { |
| Log.e(TAG, "Metrics section is null: " + adapterConfig.toString()); |
| return null; |
| } |
| String saltString = metricsSection.get("Salt256Bit"); |
| if (saltString == null) { |
| Log.e(TAG, "Salt256Bit is null: " + metricsSection.toString()); |
| return null; |
| } |
| byte[] metricsSalt = HexEncoding.decode(saltString, false /* allowSingleChar */); |
| if (metricsSalt.length != 32) { |
| Log.e(TAG, "Salt length is not 32 bit, but is " + metricsSalt.length); |
| return null; |
| } |
| return metricsSalt; |
| } |
| |
| public static byte[] obfuscateInJava(byte[] key, BluetoothDevice device) { |
| String algorithm = "HmacSHA256"; |
| try { |
| Mac hmac256 = Mac.getInstance(algorithm); |
| hmac256.init(new SecretKeySpec(key, algorithm)); |
| return hmac256.doFinal(Utils.getByteAddress(device)); |
| } catch (NoSuchAlgorithmException | IllegalStateException | InvalidKeyException exp) { |
| exp.printStackTrace(); |
| return null; |
| } |
| } |
| |
| public static boolean isByteArrayAllZero(byte[] byteArray) { |
| for (byte i : byteArray) { |
| if (i != 0) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Test: Get id for null address |
| * Check if returned value from {@link AdapterService#getMetricId(BluetoothDevice)} is |
| * 0 when device address is null |
| */ |
| @Test |
| public void testGetMetricId_NullAddress() { |
| assertThat(mAdapterService.getMetricId(null)).isEqualTo(0); |
| } |
| |
| /** |
| * Test: Get id when Bluetooth is disabled |
| * Check whether the returned value meets expectation |
| */ |
| @Test |
| public void testGetMetricId_BluetoothDisabled() { |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0); |
| assertThat(mAdapterService.getMetricId(device)).isGreaterThan(0); |
| } |
| |
| /** |
| * Test: Get id when Bluetooth is enabled |
| * Check whether the returned value meets expectation |
| */ |
| @Test |
| public void testGetMetricId_BluetoothEnabled() { |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| doEnable(false); |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_ON); |
| BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0); |
| assertThat(mAdapterService.getMetricId(device)).isGreaterThan(0); |
| } |
| |
| /** |
| * Test: Check if id gotten stays the same after toggling Bluetooth |
| */ |
| @Test |
| public void testGetMetricId_PersistentBetweenToggle() { |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0); |
| final int initialMetricId = mAdapterService.getMetricId(device); |
| assertThat(initialMetricId).isGreaterThan(0); |
| |
| // Enable |
| doEnable(false); |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_ON); |
| assertThat(mAdapterService.getMetricId(device)).isEqualTo(initialMetricId); |
| |
| // Disable |
| doDisable(false); |
| assertThat(mAdapterService.getState()).isEqualTo(STATE_OFF); |
| assertThat(mAdapterService.getMetricId(device)).isEqualTo(initialMetricId); |
| } |
| |
| @Test |
| public void testDump_doesNotCrash() { |
| FileDescriptor fd = new FileDescriptor(); |
| PrintWriter writer = mock(PrintWriter.class); |
| |
| mAdapterService.dump(fd, writer, new String[]{}); |
| mAdapterService.dump(fd, writer, new String[]{"set-test-mode", "enabled"}); |
| mAdapterService.dump(fd, writer, new String[]{"--proto-bin"}); |
| mAdapterService.dump(fd, writer, new String[]{"random", "arguments"}); |
| } |
| } |