Connect to IBluetoothFinder and use it

Bug: 325088217
Test: atest NearbyUnitTests
Change-Id: I3650da59c86ef83d9c08926f422a37773edc4af8
This commit is contained in:
Yahav Nussbaum 2024-02-13 20:56:31 +00:00
commit cd6c6822c8
3 changed files with 320 additions and 8 deletions

View file

@ -43,6 +43,7 @@ java_library {
],
static_libs: [
"androidx.core_core",
"android.hardware.bluetooth.finder-V1-java",
"guava",
"libprotobuf-java-lite",
"modules-utils-build",

View file

@ -16,26 +16,151 @@
package com.android.server.nearby.managers;
import static com.android.server.nearby.NearbyService.TAG;
import android.annotation.TargetApi;
import android.hardware.bluetooth.finder.Eid;
import android.hardware.bluetooth.finder.IBluetoothFinder;
import android.nearby.PoweredOffFindingEphemeralId;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import java.util.List;
/** Connects to {@link IBluetoothFinder} HAL and invokes its API. */
// A placeholder implementation until the HAL API can be used.
@TargetApi(Build.VERSION_CODES.TIRAMISU)
public class BluetoothFinderManager {
private boolean mPoweredOffFindingModeEnabled = false;
private static final String HAL_INSTANCE_NAME = IBluetoothFinder.DESCRIPTOR + "/default";
/** An empty implementation of the corresponding HAL API call. */
public void sendEids(List<PoweredOffFindingEphemeralId> eids) {}
private IBluetoothFinder mBluetoothFinder;
private IBinder.DeathRecipient mServiceDeathRecipient;
private final Object mLock = new Object();
/** A placeholder implementation of the corresponding HAL API call. */
public void setPoweredOffFinderMode(boolean enable) {
mPoweredOffFindingModeEnabled = enable;
private boolean initBluetoothFinderHal() {
final String methodStr = "initBluetoothFinderHal";
if (!SdkLevel.isAtLeastV()) return false;
synchronized (mLock) {
if (mBluetoothFinder != null) {
Log.i(TAG, "Bluetooth Finder HAL is already initialized");
return true;
}
try {
mBluetoothFinder = getServiceMockable();
if (mBluetoothFinder == null) {
Log.e(TAG, "Unable to obtain IBluetoothFinder");
return false;
}
Log.i(TAG, "Obtained IBluetoothFinder. Local ver: " + IBluetoothFinder.VERSION
+ ", Remote ver: " + mBluetoothFinder.getInterfaceVersion());
IBinder serviceBinder = getServiceBinderMockable();
if (serviceBinder == null) {
Log.e(TAG, "Unable to obtain the service binder for IBluetoothFinder");
return false;
}
mServiceDeathRecipient = new BluetoothFinderDeathRecipient();
serviceBinder.linkToDeath(mServiceDeathRecipient, /* flags= */ 0);
Log.i(TAG, "Bluetooth Finder HAL initialization was successful");
return true;
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
} catch (Exception e) {
Log.e(TAG, methodStr + " encountered an exception: " + e);
}
return false;
}
}
/** A placeholder implementation of the corresponding HAL API call. */
@VisibleForTesting
protected IBluetoothFinder getServiceMockable() {
return IBluetoothFinder.Stub.asInterface(
ServiceManager.waitForDeclaredService(HAL_INSTANCE_NAME));
}
@VisibleForTesting
protected IBinder getServiceBinderMockable() {
return mBluetoothFinder.asBinder();
}
private class BluetoothFinderDeathRecipient implements IBinder.DeathRecipient {
@Override
public void binderDied() {
Log.e(TAG, "BluetoothFinder service died.");
synchronized (mLock) {
mBluetoothFinder = null;
}
}
}
/** See comments for {@link IBluetoothFinder#sendEids(Eid[])} */
public void sendEids(List<PoweredOffFindingEphemeralId> eids) {
final String methodStr = "sendEids";
if (!checkHalAndLogFailure(methodStr)) return;
Eid[] eidArray = eids.stream().map(
ephmeralId -> {
Eid eid = new Eid();
eid.bytes = ephmeralId.bytes;
return eid;
}).toArray(Eid[]::new);
try {
mBluetoothFinder.sendEids(eidArray);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
} catch (ServiceSpecificException e) {
handleServiceSpecificException(e, methodStr);
}
}
/** See comments for {@link IBluetoothFinder#setPoweredOffFinderMode(boolean)} */
public void setPoweredOffFinderMode(boolean enable) {
final String methodStr = "setPoweredOffMode";
if (!checkHalAndLogFailure(methodStr)) return;
try {
mBluetoothFinder.setPoweredOffFinderMode(enable);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
} catch (ServiceSpecificException e) {
handleServiceSpecificException(e, methodStr);
}
}
/** See comments for {@link IBluetoothFinder#getPoweredOffFinderMode()} */
public boolean getPoweredOffFinderMode() {
return mPoweredOffFindingModeEnabled;
final String methodStr = "getPoweredOffMode";
if (!checkHalAndLogFailure(methodStr)) return false;
try {
return mBluetoothFinder.getPoweredOffFinderMode();
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
} catch (ServiceSpecificException e) {
handleServiceSpecificException(e, methodStr);
}
return false;
}
private boolean checkHalAndLogFailure(String methodStr) {
if ((mBluetoothFinder == null) && !initBluetoothFinderHal()) {
Log.e(TAG, "Unable to call " + methodStr + " because IBluetoothFinder is null.");
return false;
}
return true;
}
private void handleRemoteException(RemoteException e, String methodStr) {
mBluetoothFinder = null;
Log.e(TAG, methodStr + " failed with remote exception: " + e);
}
private void handleServiceSpecificException(ServiceSpecificException e, String methodStr) {
Log.e(TAG, methodStr + " failed with service-specific exception: " + e);
}
}

View file

@ -0,0 +1,186 @@
/*
* Copyright (C) 2024 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.nearby.managers;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.hardware.bluetooth.finder.Eid;
import android.hardware.bluetooth.finder.IBluetoothFinder;
import android.nearby.PoweredOffFindingEphemeralId;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.List;
public class BluetoothFinderManagerTest {
private BluetoothFinderManager mBluetoothFinderManager;
private boolean mGetServiceCalled = false;
@Mock private IBluetoothFinder mIBluetoothFinderMock;
@Mock private IBinder mServiceBinderMock;
private ArgumentCaptor<DeathRecipient> mDeathRecipientCaptor =
ArgumentCaptor.forClass(DeathRecipient.class);
private ArgumentCaptor<Eid[]> mEidArrayCaptor = ArgumentCaptor.forClass(Eid[].class);
private class BluetoothFinderManagerSpy extends BluetoothFinderManager {
@Override
protected IBluetoothFinder getServiceMockable() {
mGetServiceCalled = true;
return mIBluetoothFinderMock;
}
@Override
protected IBinder getServiceBinderMockable() {
return mServiceBinderMock;
}
}
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mBluetoothFinderManager = new BluetoothFinderManagerSpy();
}
@Test
public void testSendEids() throws Exception {
byte[] eidBytes1 = {
(byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
(byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
(byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
(byte) 0xe1, (byte) 0xde
};
byte[] eidBytes2 = {
(byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
(byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
(byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
(byte) 0xf2, (byte) 0xef
};
PoweredOffFindingEphemeralId ephemeralId1 = new PoweredOffFindingEphemeralId();
PoweredOffFindingEphemeralId ephemeralId2 = new PoweredOffFindingEphemeralId();
ephemeralId1.bytes = eidBytes1;
ephemeralId2.bytes = eidBytes2;
mBluetoothFinderManager.sendEids(List.of(ephemeralId1, ephemeralId2));
verify(mIBluetoothFinderMock).sendEids(mEidArrayCaptor.capture());
assertThat(mEidArrayCaptor.getValue()[0].bytes).isEqualTo(eidBytes1);
assertThat(mEidArrayCaptor.getValue()[1].bytes).isEqualTo(eidBytes2);
}
@Test
public void testSendEids_remoteException() throws Exception {
doThrow(new RemoteException())
.when(mIBluetoothFinderMock).sendEids(any());
mBluetoothFinderManager.sendEids(List.of());
// Verify that we get the service again following a RemoteException.
mGetServiceCalled = false;
mBluetoothFinderManager.sendEids(List.of());
assertThat(mGetServiceCalled).isTrue();
}
@Test
public void testSendEids_serviceSpecificException() throws Exception {
doThrow(new ServiceSpecificException(1))
.when(mIBluetoothFinderMock).sendEids(any());
mBluetoothFinderManager.sendEids(List.of());
}
@Test
public void testSetPoweredOffFinderMode() throws Exception {
mBluetoothFinderManager.setPoweredOffFinderMode(true);
verify(mIBluetoothFinderMock).setPoweredOffFinderMode(true);
mBluetoothFinderManager.setPoweredOffFinderMode(false);
verify(mIBluetoothFinderMock).setPoweredOffFinderMode(false);
}
@Test
public void testSetPoweredOffFinderMode_remoteException() throws Exception {
doThrow(new RemoteException())
.when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
mBluetoothFinderManager.setPoweredOffFinderMode(true);
// Verify that we get the service again following a RemoteException.
mGetServiceCalled = false;
mBluetoothFinderManager.setPoweredOffFinderMode(true);
assertThat(mGetServiceCalled).isTrue();
}
@Test
public void testSetPoweredOffFinderMode_serviceSpecificException() throws Exception {
doThrow(new ServiceSpecificException(1))
.when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
mBluetoothFinderManager.setPoweredOffFinderMode(true);
}
@Test
public void testGetPoweredOffFinderMode() throws Exception {
when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(true);
assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isTrue();
when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(false);
assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
}
@Test
public void testGetPoweredOffFinderMode_remoteException() throws Exception {
when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenThrow(new RemoteException());
assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
// Verify that we get the service again following a RemoteException.
mGetServiceCalled = false;
assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
assertThat(mGetServiceCalled).isTrue();
}
@Test
public void testGetPoweredOffFinderMode_serviceSpecificException() throws Exception {
when(mIBluetoothFinderMock.getPoweredOffFinderMode())
.thenThrow(new ServiceSpecificException(1));
assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
}
@Test
public void testDeathRecipient() throws Exception {
mBluetoothFinderManager.setPoweredOffFinderMode(true);
verify(mServiceBinderMock).linkToDeath(mDeathRecipientCaptor.capture(), anyInt());
mDeathRecipientCaptor.getValue().binderDied();
// Verify that we get the service again following a binder death.
mGetServiceCalled = false;
mBluetoothFinderManager.setPoweredOffFinderMode(true);
assertThat(mGetServiceCalled).isTrue();
}
}