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());    }
+}