Loading service/src/com/android/server/bluetooth/AdapterBinder.kt 0 → 100644 +112 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.server.bluetooth import android.bluetooth.BluetoothProfile import android.bluetooth.IBluetooth import android.bluetooth.IBluetoothCallback import android.content.AttributionSource import android.os.IBinder import android.os.RemoteException import com.android.modules.utils.SynchronousResultReceiver import com.android.server.bluetooth.BluetoothManagerService.timeToLog import java.time.Duration import java.util.concurrent.TimeoutException val SYNC_TIMEOUT = Duration.ofSeconds(3) class AdapterBinder(rawBinder: IBinder) { val adapterBinder: IBluetooth = IBluetooth.Stub.asInterface(rawBinder) val createdAt = System.currentTimeMillis() override fun toString(): String = "[Binder=" + adapterBinder.hashCode() + ", createdAt=" + timeToLog(createdAt) + "]" @Throws(RemoteException::class, TimeoutException::class) fun disable(source: AttributionSource): Boolean { val recv: SynchronousResultReceiver<Boolean> = SynchronousResultReceiver.get() adapterBinder.disable(source, recv) return recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(false) } @Throws(RemoteException::class, TimeoutException::class) fun enable(quietMode: Boolean, source: AttributionSource): Boolean { val recv: SynchronousResultReceiver<Boolean> = SynchronousResultReceiver.get() adapterBinder.enable(quietMode, source, recv) return recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(false) } @Throws(RemoteException::class, TimeoutException::class) fun getAddress(source: AttributionSource): String? { val recv: SynchronousResultReceiver<String> = SynchronousResultReceiver.get() adapterBinder.getAddress(source, recv) return recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun getName(source: AttributionSource): String? { val recv: SynchronousResultReceiver<String> = SynchronousResultReceiver.get() adapterBinder.getName(source, recv) return recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun onBrEdrDown(source: AttributionSource) { val recv: SynchronousResultReceiver<Any> = SynchronousResultReceiver.get() adapterBinder.onBrEdrDown(source, recv) recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun onLeServiceUp(source: AttributionSource) { val recv: SynchronousResultReceiver<Any> = SynchronousResultReceiver.get() adapterBinder.onLeServiceUp(source, recv) recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun registerCallback(callback: IBluetoothCallback, source: AttributionSource) { val recv: SynchronousResultReceiver<Any> = SynchronousResultReceiver.get() adapterBinder.registerCallback(callback, source, recv) recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun unregisterCallback(callback: IBluetoothCallback, source: AttributionSource) { val recv: SynchronousResultReceiver<Any> = SynchronousResultReceiver.get() adapterBinder.unregisterCallback(callback, source, recv) recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun getSupportedProfiles(source: AttributionSource): MutableList<Int> { val supportedProfiles = ArrayList<Int>() val recv: SynchronousResultReceiver<Long> = SynchronousResultReceiver.get() adapterBinder.getSupportedProfiles(source, recv) val supportedProfilesBitMask = recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(0L) for (i in 0..BluetoothProfile.MAX_PROFILE_ID) { if (supportedProfilesBitMask and (1 shl i).toLong() != 0L) { supportedProfiles.add(i) } } return supportedProfiles } @Throws(RemoteException::class) fun setForegroundUserId(userId: Int, source: AttributionSource) { adapterBinder.setForegroundUserId(userId, source) } } service/src/com/android/server/bluetooth/BluetoothManagerService.java +136 −234 File changed.Preview size limit exceeded, changes collapsed. Show changes service/src/com/android/server/bluetooth/BluetoothServerProxy.java +5 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.bluetooth; import android.annotation.NonNull; import android.content.ContentResolver; import android.os.IBinder; import android.provider.Settings; import android.util.Log; Loading Loading @@ -55,6 +56,10 @@ class BluetoothServerProxy { } } AdapterBinder createAdapterBinder(IBinder binder) { return new AdapterBinder(binder); } String settingsSecureGetString(ContentResolver contentResolver, String name) { return Settings.Secure.getString(contentResolver, name); } Loading service/tests/src/com/android/server/bluetooth/BluetoothManagerServiceTest.java +201 −6 Original line number Diff line number Diff line Loading @@ -16,19 +16,42 @@ package com.android.server.bluetooth; import static android.bluetooth.BluetoothAdapter.STATE_BLE_ON; import static android.bluetooth.BluetoothAdapter.STATE_OFF; import static android.bluetooth.BluetoothAdapter.STATE_ON; import static android.bluetooth.BluetoothAdapter.STATE_TURNING_ON; import static com.android.server.bluetooth.BluetoothManagerService.MESSAGE_BLUETOOTH_SERVICE_CONNECTED; import static com.android.server.bluetooth.BluetoothManagerService.MESSAGE_BLUETOOTH_STATE_CHANGE; import static com.android.server.bluetooth.BluetoothManagerService.MESSAGE_DISABLE; import static com.android.server.bluetooth.BluetoothManagerService.MESSAGE_ENABLE; import static com.android.server.bluetooth.BluetoothManagerService.MESSAGE_REGISTER_STATE_CHANGE_CALLBACK; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.validateMockitoUsage; import static org.mockito.Mockito.verify; import android.bluetooth.IBluetooth; import android.bluetooth.IBluetoothCallback; import android.bluetooth.IBluetoothManagerCallback; import android.bluetooth.IBluetoothStateChangeCallback; import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.Message; import android.os.UserHandle; import android.os.UserManager; import android.os.test.TestLooper; Loading @@ -41,19 +64,36 @@ import org.junit.After; import org.junit.Before; 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.util.stream.IntStream; @RunWith(AndroidJUnit4.class) public class BluetoothManagerServiceTest { static int sTimeout = 3000; private static final String TAG = BluetoothManagerServiceTest.class.getSimpleName(); static final int STATE_BLE_TURNING_ON = 14; // can't find the symbol because hidden api static final int TIMEOUT_MS = 1000; // TO use to wait for handler execution BluetoothManagerService mManagerService; Context mContext; @Mock BluetoothServerProxy mBluetoothServerProxy; /* @Spy */ Context mContext; @Spy BluetoothServerProxy mBluetoothServerProxy; @Mock UserManager mUserManager; @Mock IBinder mBinder; @Mock IBluetoothManagerCallback mManagerCallback; @Mock IBluetoothStateChangeCallback mStateChangeCallback; @Mock IBluetooth mAdapterService; @Mock AdapterBinder mAdapterBinder; TestLooper mLooper; boolean mHasException = false; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); Loading @@ -71,26 +111,68 @@ public class BluetoothManagerServiceTest { new ContextWrapper( InstrumentationRegistry.getInstrumentation().getTargetContext())); // Test is not allowed to send broadcast as Bluetooth. doNothing Prevent SecurityException doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any(), any()); doReturn(mUserManager).when(mContext).getSystemService(UserManager.class); doReturn(mBinder).when(mManagerCallback).asBinder(); doReturn(mBinder).when(mStateChangeCallback).asBinder(); doReturn(mAdapterBinder).when(mBluetoothServerProxy).createAdapterBinder(any()); doReturn(mAdapterService).when(mAdapterBinder).getAdapterBinder(); doReturn(mock(Intent.class)) .when(mContext) .registerReceiverForAllUsers(any(), any(), eq(null), eq(null)); doReturn(true) .when(mContext) .bindServiceAsUser( any(Intent.class), any(ServiceConnection.class), anyInt(), any(UserHandle.class)); BluetoothServerProxy.setInstanceForTesting(mBluetoothServerProxy); mLooper = new TestLooper(); mManagerService = new BluetoothManagerService(mContext, mLooper.getLooper()); mManagerService.registerAdapter(mManagerCallback); } @After public void tearDown() { mManagerService.unregisterAdapter(mManagerCallback); mLooper.moveTimeForward(120_000); // 120 seconds // Do not try to assert if `syncHandler()` already raised an exception for it if (!mHasException) { assertThat(mLooper.nextMessage()).isNull(); } mManagerService = null; validateMockitoUsage(); } /** * 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) { IntStream.of(what) .forEach( w -> { Message msg = mLooper.nextMessage(); assertThat(msg).isNotNull(); assertThat(msg.what).isEqualTo(w); msg.getTarget().dispatchMessage(msg); }); } @Test public void onUserRestrictionsChanged_disallowBluetooth_onlySendDisableMessageOnSystemUser() throws InterruptedException { // Mimic the case when restriction settings changed doReturn(true) .when(mUserManager) .hasUserRestrictionForUser(eq(UserManager.DISALLOW_BLUETOOTH), any()); Loading @@ -103,13 +185,14 @@ public class BluetoothManagerServiceTest { // test run on user -1, should not turning Bluetooth off mManagerService.onUserRestrictionsChanged(UserHandle.CURRENT); verify(mBluetoothServerProxy, timeout(sTimeout).times(0)) verify(mBluetoothServerProxy, times(0)) .handlerSendWhatMessage(any(), eq(BluetoothManagerService.MESSAGE_DISABLE)); // called from SYSTEM user, should try to toggle Bluetooth off mManagerService.onUserRestrictionsChanged(UserHandle.SYSTEM); verify(mBluetoothServerProxy, timeout(sTimeout)) verify(mBluetoothServerProxy) .handlerSendWhatMessage(any(), eq(BluetoothManagerService.MESSAGE_DISABLE)); syncHandler(MESSAGE_DISABLE); } @Test Loading @@ -130,4 +213,116 @@ public class BluetoothManagerServiceTest { mContext.getContentResolver(), "apm_enhancement_enabled", 0)) .isEqualTo(1); } private void acceptBluetoothBinding(IBinder binder, String name, int n) { ComponentName compName = new ComponentName("", "com.android.bluetooth." + name); ArgumentCaptor<BluetoothManagerService.BluetoothServiceConnection> captor = ArgumentCaptor.forClass(BluetoothManagerService.BluetoothServiceConnection.class); verify(mContext, times(n)) .bindServiceAsUser( any(Intent.class), captor.capture(), anyInt(), any(UserHandle.class)); assertThat(captor.getAllValues().size()).isEqualTo(n); captor.getAllValues().get(n - 1).onServiceConnected(compName, binder); syncHandler(MESSAGE_BLUETOOTH_SERVICE_CONNECTED); } private static IBluetoothCallback captureBluetoothCallback(AdapterBinder adapterBinder) throws Exception { ArgumentCaptor<IBluetoothCallback> captor = ArgumentCaptor.forClass(IBluetoothCallback.class); verify(adapterBinder).registerCallback(captor.capture(), any()); assertThat(captor.getAllValues().size()).isEqualTo(1); return captor.getValue(); } IBluetoothCallback transition_offToBleOn() throws Exception { // Binding of IBluetooth acceptBluetoothBinding(mBinder, "btservice.AdapterService", 1); // TODO(b/280518177): This callback is too early, bt is not ON nor BLE_ON verify(mManagerCallback).onBluetoothServiceUp(any()); IBluetoothCallback btCallback = captureBluetoothCallback(mAdapterBinder); verify(mAdapterBinder).enable(anyBoolean(), any()); // AdapterService is sending AdapterState.BLE_TURN_ON that will trigger this callback // and in parallel it call its `bringUpBle()` btCallback.onBluetoothStateChange(STATE_OFF, STATE_BLE_TURNING_ON); syncHandler(MESSAGE_BLUETOOTH_STATE_CHANGE); assertThat(mManagerService.getState()).isEqualTo(STATE_BLE_TURNING_ON); // assertThat(mManagerService.waitForManagerState(STATE_BLE_TURNING_ON)).isTrue(); // GattService has been started by AdapterService and it will enable native side then // trigger the stateChangeCallback from native btCallback.onBluetoothStateChange(STATE_BLE_TURNING_ON, STATE_BLE_ON); syncHandler(MESSAGE_BLUETOOTH_STATE_CHANGE); assertThat(mManagerService.getState()).isEqualTo(STATE_BLE_ON); // Binding of IBluetoothGatt // TODO(b/280518177): Remove necessity to bind to gatt from the ManagerService acceptBluetoothBinding(mBinder, "gatt.GattService", 2); // Check that we sent 2 intent, one for BLE_TURNING_ON, one for BLE_ON // TODO(b/280518177): assert the intent are the correct one verify(mContext, times(2)).sendBroadcastAsUser(any(), any(), any(), any()); return btCallback; } private IBluetoothCallback transition_offToOn() throws Exception { IBluetoothCallback btCallback = transition_offToBleOn(); verify(mAdapterBinder, times(1)).onLeServiceUp(any()); // AdapterService go to turning_on and start all profile on its own btCallback.onBluetoothStateChange(STATE_BLE_ON, STATE_TURNING_ON); syncHandler(MESSAGE_BLUETOOTH_STATE_CHANGE); // When all the profile are started, adapterService consider it is ON btCallback.onBluetoothStateChange(STATE_TURNING_ON, STATE_ON); syncHandler(MESSAGE_BLUETOOTH_STATE_CHANGE); // Check that we sent 6 intent, 4 for BLE: BLE_TURNING_ON + BLE_ON + TURNING_ON + ON // and 2 for classic: TURNING_ON + ON // TODO(b/280518177): assert the intent are the correct one verify(mContext, times(6)).sendBroadcastAsUser(any(), any(), any(), any()); return btCallback; } @Test public void offToBleOn() throws Exception { // In order to go to BLE only, the persisted state should be BLUETOOTH_OFF doReturn(BluetoothManagerService.BLUETOOTH_OFF) .when(mBluetoothServerProxy) .getBluetoothPersistedState(any(), anyInt()); mManagerService.enableBle("test_offToBleOn", mBinder); syncHandler(MESSAGE_ENABLE); transition_offToBleOn(); // Check that there was no transition to STATE_ON verify(mAdapterBinder, times(0)).onLeServiceUp(any()); assertThat(mManagerService.getState()).isEqualTo(STATE_BLE_ON); } @Test public void offToOn() throws Exception { // In order to not go to BLE only, the persisted state should not be BLUETOOTH_OFF doReturn(BluetoothManagerService.BLUETOOTH_ON_BLUETOOTH) .when(mBluetoothServerProxy) .getBluetoothPersistedState(any(), anyInt()); mManagerService.registerStateChangeCallback(mStateChangeCallback); syncHandler(MESSAGE_REGISTER_STATE_CHANGE_CALLBACK); mManagerService.enable("test_offToOn"); syncHandler(MESSAGE_ENABLE); transition_offToOn(); verify(mStateChangeCallback).onBluetoothStateChange(eq(true)); assertThat(mManagerService.getState()).isEqualTo(STATE_ON); } } Loading
service/src/com/android/server/bluetooth/AdapterBinder.kt 0 → 100644 +112 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.server.bluetooth import android.bluetooth.BluetoothProfile import android.bluetooth.IBluetooth import android.bluetooth.IBluetoothCallback import android.content.AttributionSource import android.os.IBinder import android.os.RemoteException import com.android.modules.utils.SynchronousResultReceiver import com.android.server.bluetooth.BluetoothManagerService.timeToLog import java.time.Duration import java.util.concurrent.TimeoutException val SYNC_TIMEOUT = Duration.ofSeconds(3) class AdapterBinder(rawBinder: IBinder) { val adapterBinder: IBluetooth = IBluetooth.Stub.asInterface(rawBinder) val createdAt = System.currentTimeMillis() override fun toString(): String = "[Binder=" + adapterBinder.hashCode() + ", createdAt=" + timeToLog(createdAt) + "]" @Throws(RemoteException::class, TimeoutException::class) fun disable(source: AttributionSource): Boolean { val recv: SynchronousResultReceiver<Boolean> = SynchronousResultReceiver.get() adapterBinder.disable(source, recv) return recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(false) } @Throws(RemoteException::class, TimeoutException::class) fun enable(quietMode: Boolean, source: AttributionSource): Boolean { val recv: SynchronousResultReceiver<Boolean> = SynchronousResultReceiver.get() adapterBinder.enable(quietMode, source, recv) return recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(false) } @Throws(RemoteException::class, TimeoutException::class) fun getAddress(source: AttributionSource): String? { val recv: SynchronousResultReceiver<String> = SynchronousResultReceiver.get() adapterBinder.getAddress(source, recv) return recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun getName(source: AttributionSource): String? { val recv: SynchronousResultReceiver<String> = SynchronousResultReceiver.get() adapterBinder.getName(source, recv) return recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun onBrEdrDown(source: AttributionSource) { val recv: SynchronousResultReceiver<Any> = SynchronousResultReceiver.get() adapterBinder.onBrEdrDown(source, recv) recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun onLeServiceUp(source: AttributionSource) { val recv: SynchronousResultReceiver<Any> = SynchronousResultReceiver.get() adapterBinder.onLeServiceUp(source, recv) recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun registerCallback(callback: IBluetoothCallback, source: AttributionSource) { val recv: SynchronousResultReceiver<Any> = SynchronousResultReceiver.get() adapterBinder.registerCallback(callback, source, recv) recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun unregisterCallback(callback: IBluetoothCallback, source: AttributionSource) { val recv: SynchronousResultReceiver<Any> = SynchronousResultReceiver.get() adapterBinder.unregisterCallback(callback, source, recv) recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(null) } @Throws(RemoteException::class, TimeoutException::class) fun getSupportedProfiles(source: AttributionSource): MutableList<Int> { val supportedProfiles = ArrayList<Int>() val recv: SynchronousResultReceiver<Long> = SynchronousResultReceiver.get() adapterBinder.getSupportedProfiles(source, recv) val supportedProfilesBitMask = recv.awaitResultNoInterrupt(SYNC_TIMEOUT).getValue(0L) for (i in 0..BluetoothProfile.MAX_PROFILE_ID) { if (supportedProfilesBitMask and (1 shl i).toLong() != 0L) { supportedProfiles.add(i) } } return supportedProfiles } @Throws(RemoteException::class) fun setForegroundUserId(userId: Int, source: AttributionSource) { adapterBinder.setForegroundUserId(userId, source) } }
service/src/com/android/server/bluetooth/BluetoothManagerService.java +136 −234 File changed.Preview size limit exceeded, changes collapsed. Show changes
service/src/com/android/server/bluetooth/BluetoothServerProxy.java +5 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.bluetooth; import android.annotation.NonNull; import android.content.ContentResolver; import android.os.IBinder; import android.provider.Settings; import android.util.Log; Loading Loading @@ -55,6 +56,10 @@ class BluetoothServerProxy { } } AdapterBinder createAdapterBinder(IBinder binder) { return new AdapterBinder(binder); } String settingsSecureGetString(ContentResolver contentResolver, String name) { return Settings.Secure.getString(contentResolver, name); } Loading
service/tests/src/com/android/server/bluetooth/BluetoothManagerServiceTest.java +201 −6 Original line number Diff line number Diff line Loading @@ -16,19 +16,42 @@ package com.android.server.bluetooth; import static android.bluetooth.BluetoothAdapter.STATE_BLE_ON; import static android.bluetooth.BluetoothAdapter.STATE_OFF; import static android.bluetooth.BluetoothAdapter.STATE_ON; import static android.bluetooth.BluetoothAdapter.STATE_TURNING_ON; import static com.android.server.bluetooth.BluetoothManagerService.MESSAGE_BLUETOOTH_SERVICE_CONNECTED; import static com.android.server.bluetooth.BluetoothManagerService.MESSAGE_BLUETOOTH_STATE_CHANGE; import static com.android.server.bluetooth.BluetoothManagerService.MESSAGE_DISABLE; import static com.android.server.bluetooth.BluetoothManagerService.MESSAGE_ENABLE; import static com.android.server.bluetooth.BluetoothManagerService.MESSAGE_REGISTER_STATE_CHANGE_CALLBACK; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.validateMockitoUsage; import static org.mockito.Mockito.verify; import android.bluetooth.IBluetooth; import android.bluetooth.IBluetoothCallback; import android.bluetooth.IBluetoothManagerCallback; import android.bluetooth.IBluetoothStateChangeCallback; import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.Message; import android.os.UserHandle; import android.os.UserManager; import android.os.test.TestLooper; Loading @@ -41,19 +64,36 @@ import org.junit.After; import org.junit.Before; 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.util.stream.IntStream; @RunWith(AndroidJUnit4.class) public class BluetoothManagerServiceTest { static int sTimeout = 3000; private static final String TAG = BluetoothManagerServiceTest.class.getSimpleName(); static final int STATE_BLE_TURNING_ON = 14; // can't find the symbol because hidden api static final int TIMEOUT_MS = 1000; // TO use to wait for handler execution BluetoothManagerService mManagerService; Context mContext; @Mock BluetoothServerProxy mBluetoothServerProxy; /* @Spy */ Context mContext; @Spy BluetoothServerProxy mBluetoothServerProxy; @Mock UserManager mUserManager; @Mock IBinder mBinder; @Mock IBluetoothManagerCallback mManagerCallback; @Mock IBluetoothStateChangeCallback mStateChangeCallback; @Mock IBluetooth mAdapterService; @Mock AdapterBinder mAdapterBinder; TestLooper mLooper; boolean mHasException = false; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); Loading @@ -71,26 +111,68 @@ public class BluetoothManagerServiceTest { new ContextWrapper( InstrumentationRegistry.getInstrumentation().getTargetContext())); // Test is not allowed to send broadcast as Bluetooth. doNothing Prevent SecurityException doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any(), any()); doReturn(mUserManager).when(mContext).getSystemService(UserManager.class); doReturn(mBinder).when(mManagerCallback).asBinder(); doReturn(mBinder).when(mStateChangeCallback).asBinder(); doReturn(mAdapterBinder).when(mBluetoothServerProxy).createAdapterBinder(any()); doReturn(mAdapterService).when(mAdapterBinder).getAdapterBinder(); doReturn(mock(Intent.class)) .when(mContext) .registerReceiverForAllUsers(any(), any(), eq(null), eq(null)); doReturn(true) .when(mContext) .bindServiceAsUser( any(Intent.class), any(ServiceConnection.class), anyInt(), any(UserHandle.class)); BluetoothServerProxy.setInstanceForTesting(mBluetoothServerProxy); mLooper = new TestLooper(); mManagerService = new BluetoothManagerService(mContext, mLooper.getLooper()); mManagerService.registerAdapter(mManagerCallback); } @After public void tearDown() { mManagerService.unregisterAdapter(mManagerCallback); mLooper.moveTimeForward(120_000); // 120 seconds // Do not try to assert if `syncHandler()` already raised an exception for it if (!mHasException) { assertThat(mLooper.nextMessage()).isNull(); } mManagerService = null; validateMockitoUsage(); } /** * 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) { IntStream.of(what) .forEach( w -> { Message msg = mLooper.nextMessage(); assertThat(msg).isNotNull(); assertThat(msg.what).isEqualTo(w); msg.getTarget().dispatchMessage(msg); }); } @Test public void onUserRestrictionsChanged_disallowBluetooth_onlySendDisableMessageOnSystemUser() throws InterruptedException { // Mimic the case when restriction settings changed doReturn(true) .when(mUserManager) .hasUserRestrictionForUser(eq(UserManager.DISALLOW_BLUETOOTH), any()); Loading @@ -103,13 +185,14 @@ public class BluetoothManagerServiceTest { // test run on user -1, should not turning Bluetooth off mManagerService.onUserRestrictionsChanged(UserHandle.CURRENT); verify(mBluetoothServerProxy, timeout(sTimeout).times(0)) verify(mBluetoothServerProxy, times(0)) .handlerSendWhatMessage(any(), eq(BluetoothManagerService.MESSAGE_DISABLE)); // called from SYSTEM user, should try to toggle Bluetooth off mManagerService.onUserRestrictionsChanged(UserHandle.SYSTEM); verify(mBluetoothServerProxy, timeout(sTimeout)) verify(mBluetoothServerProxy) .handlerSendWhatMessage(any(), eq(BluetoothManagerService.MESSAGE_DISABLE)); syncHandler(MESSAGE_DISABLE); } @Test Loading @@ -130,4 +213,116 @@ public class BluetoothManagerServiceTest { mContext.getContentResolver(), "apm_enhancement_enabled", 0)) .isEqualTo(1); } private void acceptBluetoothBinding(IBinder binder, String name, int n) { ComponentName compName = new ComponentName("", "com.android.bluetooth." + name); ArgumentCaptor<BluetoothManagerService.BluetoothServiceConnection> captor = ArgumentCaptor.forClass(BluetoothManagerService.BluetoothServiceConnection.class); verify(mContext, times(n)) .bindServiceAsUser( any(Intent.class), captor.capture(), anyInt(), any(UserHandle.class)); assertThat(captor.getAllValues().size()).isEqualTo(n); captor.getAllValues().get(n - 1).onServiceConnected(compName, binder); syncHandler(MESSAGE_BLUETOOTH_SERVICE_CONNECTED); } private static IBluetoothCallback captureBluetoothCallback(AdapterBinder adapterBinder) throws Exception { ArgumentCaptor<IBluetoothCallback> captor = ArgumentCaptor.forClass(IBluetoothCallback.class); verify(adapterBinder).registerCallback(captor.capture(), any()); assertThat(captor.getAllValues().size()).isEqualTo(1); return captor.getValue(); } IBluetoothCallback transition_offToBleOn() throws Exception { // Binding of IBluetooth acceptBluetoothBinding(mBinder, "btservice.AdapterService", 1); // TODO(b/280518177): This callback is too early, bt is not ON nor BLE_ON verify(mManagerCallback).onBluetoothServiceUp(any()); IBluetoothCallback btCallback = captureBluetoothCallback(mAdapterBinder); verify(mAdapterBinder).enable(anyBoolean(), any()); // AdapterService is sending AdapterState.BLE_TURN_ON that will trigger this callback // and in parallel it call its `bringUpBle()` btCallback.onBluetoothStateChange(STATE_OFF, STATE_BLE_TURNING_ON); syncHandler(MESSAGE_BLUETOOTH_STATE_CHANGE); assertThat(mManagerService.getState()).isEqualTo(STATE_BLE_TURNING_ON); // assertThat(mManagerService.waitForManagerState(STATE_BLE_TURNING_ON)).isTrue(); // GattService has been started by AdapterService and it will enable native side then // trigger the stateChangeCallback from native btCallback.onBluetoothStateChange(STATE_BLE_TURNING_ON, STATE_BLE_ON); syncHandler(MESSAGE_BLUETOOTH_STATE_CHANGE); assertThat(mManagerService.getState()).isEqualTo(STATE_BLE_ON); // Binding of IBluetoothGatt // TODO(b/280518177): Remove necessity to bind to gatt from the ManagerService acceptBluetoothBinding(mBinder, "gatt.GattService", 2); // Check that we sent 2 intent, one for BLE_TURNING_ON, one for BLE_ON // TODO(b/280518177): assert the intent are the correct one verify(mContext, times(2)).sendBroadcastAsUser(any(), any(), any(), any()); return btCallback; } private IBluetoothCallback transition_offToOn() throws Exception { IBluetoothCallback btCallback = transition_offToBleOn(); verify(mAdapterBinder, times(1)).onLeServiceUp(any()); // AdapterService go to turning_on and start all profile on its own btCallback.onBluetoothStateChange(STATE_BLE_ON, STATE_TURNING_ON); syncHandler(MESSAGE_BLUETOOTH_STATE_CHANGE); // When all the profile are started, adapterService consider it is ON btCallback.onBluetoothStateChange(STATE_TURNING_ON, STATE_ON); syncHandler(MESSAGE_BLUETOOTH_STATE_CHANGE); // Check that we sent 6 intent, 4 for BLE: BLE_TURNING_ON + BLE_ON + TURNING_ON + ON // and 2 for classic: TURNING_ON + ON // TODO(b/280518177): assert the intent are the correct one verify(mContext, times(6)).sendBroadcastAsUser(any(), any(), any(), any()); return btCallback; } @Test public void offToBleOn() throws Exception { // In order to go to BLE only, the persisted state should be BLUETOOTH_OFF doReturn(BluetoothManagerService.BLUETOOTH_OFF) .when(mBluetoothServerProxy) .getBluetoothPersistedState(any(), anyInt()); mManagerService.enableBle("test_offToBleOn", mBinder); syncHandler(MESSAGE_ENABLE); transition_offToBleOn(); // Check that there was no transition to STATE_ON verify(mAdapterBinder, times(0)).onLeServiceUp(any()); assertThat(mManagerService.getState()).isEqualTo(STATE_BLE_ON); } @Test public void offToOn() throws Exception { // In order to not go to BLE only, the persisted state should not be BLUETOOTH_OFF doReturn(BluetoothManagerService.BLUETOOTH_ON_BLUETOOTH) .when(mBluetoothServerProxy) .getBluetoothPersistedState(any(), anyInt()); mManagerService.registerStateChangeCallback(mStateChangeCallback); syncHandler(MESSAGE_REGISTER_STATE_CHANGE_CALLBACK); mManagerService.enable("test_offToOn"); syncHandler(MESSAGE_ENABLE); transition_offToOn(); verify(mStateChangeCallback).onBluetoothStateChange(eq(true)); assertThat(mManagerService.getState()).isEqualTo(STATE_ON); } }