Merge "Add BluetoothGattUtils."
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java
new file mode 100644
index 0000000..1094060
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2021 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.common.bluetooth.util;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utils for Gatt profile.
+ */
+public class BluetoothGattUtils {
+
+ /**
+ * Returns a string message for a BluetoothGatt status codes.
+ */
+ public static String getMessageForStatusCode(int statusCode) {
+ switch (statusCode) {
+ case BluetoothGatt.GATT_SUCCESS:
+ return "GATT_SUCCESS";
+ case BluetoothGatt.GATT_FAILURE:
+ return "GATT_FAILURE";
+ case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
+ return "GATT_INSUFFICIENT_AUTHENTICATION";
+ case BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION:
+ return "GATT_INSUFFICIENT_AUTHORIZATION";
+ case BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION:
+ return "GATT_INSUFFICIENT_ENCRYPTION";
+ case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH:
+ return "GATT_INVALID_ATTRIBUTE_LENGTH";
+ case BluetoothGatt.GATT_INVALID_OFFSET:
+ return "GATT_INVALID_OFFSET";
+ case BluetoothGatt.GATT_READ_NOT_PERMITTED:
+ return "GATT_READ_NOT_PERMITTED";
+ case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED:
+ return "GATT_REQUEST_NOT_SUPPORTED";
+ case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
+ return "GATT_WRITE_NOT_PERMITTED";
+ case BluetoothGatt.GATT_CONNECTION_CONGESTED:
+ return "GATT_CONNECTION_CONGESTED";
+ default:
+ return "Unknown error code";
+ }
+ }
+
+ /** Clones a {@link BluetoothGattDescriptor} so the value can be changed thread-safely. */
+ public static BluetoothGattDescriptor clone(BluetoothGattDescriptor descriptor)
+ throws BluetoothException {
+ BluetoothGattDescriptor result =
+ new BluetoothGattDescriptor(descriptor.getUuid(), descriptor.getPermissions());
+ try {
+ try {
+ // TODO(b/201463121): remove usage of reflection.
+ Field instanceIdField = BluetoothGattDescriptor.class.getDeclaredField("mInstance");
+ instanceIdField.setAccessible(true);
+ instanceIdField.set(result, instanceIdField.get(descriptor));
+ } catch (NoSuchFieldException e) {
+ // This field doesn't seem to exist in early implementation so just ignore
+ // instanceId in this case.
+ }
+
+ // TODO(b/201463121): remove usage of reflection.
+ Field characteristicField =
+ BluetoothGattDescriptor.class.getDeclaredField("mCharacteristic");
+ characteristicField.setAccessible(true);
+ characteristicField.set(result, characteristicField.get(descriptor));
+ byte[] value = descriptor.getValue();
+ if (value != null) {
+ result.setValue(Arrays.copyOf(value, value.length));
+ }
+ } catch (NoSuchFieldException e) {
+ throw new BluetoothException("Cannot clone descriptor.", e);
+ } catch (IllegalAccessException e) {
+ throw new BluetoothException("Cannot clone descriptor.", e);
+ } catch (IllegalArgumentException e) {
+ throw new BluetoothException("Cannot clone descriptor.", e);
+ }
+ return result;
+ }
+
+ /** Clones a {@link BluetoothGattCharacteristic} so the value can be changed thread-safely. */
+ public static BluetoothGattCharacteristic clone(BluetoothGattCharacteristic characteristic)
+ throws BluetoothException {
+ BluetoothGattCharacteristic result =
+ new BluetoothGattCharacteristic(characteristic.getUuid(),
+ characteristic.getProperties(), characteristic.getPermissions());
+ try {
+ // TODO(b/201463121): remove usage of reflection.
+ Field instanceIdField = BluetoothGattCharacteristic.class.getDeclaredField("mInstance");
+ // TODO(b/201463121): remove usage of reflection.
+ Field serviceField = BluetoothGattCharacteristic.class.getDeclaredField("mService");
+ // TODO(b/201463121): remove usage of reflection.
+ Field descriptorField =
+ BluetoothGattCharacteristic.class.getDeclaredField("mDescriptors");
+ instanceIdField.setAccessible(true);
+ serviceField.setAccessible(true);
+ descriptorField.setAccessible(true);
+ instanceIdField.set(result, instanceIdField.get(characteristic));
+ serviceField.set(result, serviceField.get(characteristic));
+ descriptorField.set(result, descriptorField.get(characteristic));
+ byte[] value = characteristic.getValue();
+ if (value != null) {
+ result.setValue(Arrays.copyOf(value, value.length));
+ }
+ result.setWriteType(characteristic.getWriteType());
+ } catch (NoSuchFieldException e) {
+ throw new BluetoothException("Cannot clone characteristic.", e);
+ } catch (IllegalAccessException e) {
+ throw new BluetoothException("Cannot clone characteristic.", e);
+ } catch (IllegalArgumentException e) {
+ throw new BluetoothException("Cannot clone characteristic.", e);
+ }
+ return result;
+ }
+
+ /** Creates a user-readable string from a {@link BluetoothGattDescriptor}. */
+ public static String toString(@Nullable BluetoothGattDescriptor descriptor) {
+ if (descriptor == null) {
+ return "null descriptor";
+ }
+ return String.format("descriptor %s on %s",
+ descriptor.getUuid(),
+ toString(descriptor.getCharacteristic()));
+ }
+
+ /** Creates a user-readable string from a {@link BluetoothGattCharacteristic}. */
+ public static String toString(@Nullable BluetoothGattCharacteristic characteristic) {
+ if (characteristic == null) {
+ return "null characteristic";
+ }
+ return String.format("characteristic %s on %s",
+ characteristic.getUuid(),
+ toString(characteristic.getService()));
+ }
+
+ /** Creates a user-readable string from a {@link BluetoothGattService}. */
+ public static String toString(@Nullable BluetoothGattService service) {
+ if (service == null) {
+ return "null service";
+ }
+ return String.format("service %s", service.getUuid());
+ }
+}
diff --git a/nearby/tests/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java b/nearby/tests/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java
new file mode 100644
index 0000000..53aed6e
--- /dev/null
+++ b/nearby/tests/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2021 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.common.bluetooth.util;
+
+import static com.android.server.nearby.common.bluetooth.util.BluetoothGattUtils.getMessageForStatusCode;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.UUID;
+
+/** Unit tests for {@link BluetoothAddress}. */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothGattUtilsTest {
+ private static final UUID TEST_UUID = UUID.randomUUID();
+ private static final ImmutableSet<String> GATT_HIDDEN_CONSTANTS = ImmutableSet.of(
+ "GATT_WRITE_REQUEST_BUSY", "GATT_WRITE_REQUEST_FAIL", "GATT_WRITE_REQUEST_SUCCESS");
+
+ @Test
+ public void testGetMessageForStatusCode() throws Exception {
+ Field[] publicFields = BluetoothGatt.class.getFields();
+ for (Field field : publicFields) {
+ if ((field.getModifiers() & Modifier.STATIC) == 0
+ || field.getDeclaringClass() != BluetoothGatt.class) {
+ continue;
+ }
+ String fieldName = field.getName();
+ if (!fieldName.startsWith("GATT_") || GATT_HIDDEN_CONSTANTS.contains(fieldName)) {
+ continue;
+ }
+ int fieldValue = (Integer) field.get(null);
+ assertThat(getMessageForStatusCode(fieldValue)).isEqualTo(fieldName);
+ }
+ }
+
+ @Test
+ public void testCloneDescriptor() throws BluetoothException {
+ BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(TEST_UUID,
+ BluetoothGattCharacteristic.PROPERTY_INDICATE,
+ BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED
+ | BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED_MITM);
+ BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(TEST_UUID,
+ BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED
+ | BluetoothGattDescriptor.PERMISSION_WRITE_SIGNED_MITM);
+ characteristic.addDescriptor(descriptor);
+
+ BluetoothGattDescriptor result = BluetoothGattUtils.clone(descriptor);
+
+ assertThat(result.getUuid()).isEqualTo(descriptor.getUuid());
+ assertThat(result.getPermissions()).isEqualTo(descriptor.getPermissions());
+ assertThat(result.getCharacteristic()).isEqualTo(descriptor.getCharacteristic());
+ }
+
+ @Test
+ public void testCloneCharacteristic() throws BluetoothException {
+ BluetoothGattService service =
+ new BluetoothGattService(TEST_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY);
+ BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(TEST_UUID,
+ BluetoothGattCharacteristic.PROPERTY_INDICATE,
+ BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED
+ | BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED_MITM);
+ characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
+ service.addCharacteristic(characteristic);
+ BluetoothGattCharacteristic result = BluetoothGattUtils.clone(characteristic);
+
+ assertThat(result.getUuid()).isEqualTo(characteristic.getUuid());
+ assertThat(result.getPermissions()).isEqualTo(characteristic.getPermissions());
+ assertThat(result.getProperties()).isEqualTo(characteristic.getProperties());
+ assertThat(result.getService()).isEqualTo(characteristic.getService());
+ assertThat(result.getInstanceId()).isEqualTo(characteristic.getInstanceId());
+ assertThat(result.getWriteType()).isEqualTo(characteristic.getWriteType()); }
+}