/* * Copyright 2019 Fitbit, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ package com.fitbit.bluetooth.fbgatt.util; import com.fitbit.bluetooth.fbgatt.BuildConfig; import com.fitbit.bluetooth.fbgatt.btcopies.BluetoothGattCharacteristicCopy; import com.fitbit.bluetooth.fbgatt.btcopies.BluetoothGattDescriptorCopy; import com.fitbit.bluetooth.fbgatt.btcopies.BluetoothGattServiceCopy; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.content.Context; import java.util.Arrays; import java.util.List; import androidx.annotation.Nullable; import timber.log.Timber; /** * A simple utility class for gatt helper methods * <p> * Created by iowens on 6/5/18. */ public class GattUtils { /** * To prevent the characteristic from changing out from under us we need to copy it * <p> * This may happen under high throughput/concurrency * * @param characteristic The characteristic to be copied * @return The shallow-ish copy of the characteristic */ public @Nullable BluetoothGattCharacteristicCopy copyCharacteristic(@Nullable BluetoothGattCharacteristic characteristic) { if (null == characteristic || null == characteristic.getUuid()) { return null; } BluetoothGattCharacteristicCopy newCharacteristic = new BluetoothGattCharacteristicCopy(characteristic.getUuid(), characteristic.getProperties(), characteristic.getPermissions()); if (characteristic.getValue() != null) { newCharacteristic.setValue(Arrays.copyOf(characteristic.getValue(), characteristic.getValue().length)); } if (!characteristic.getDescriptors().isEmpty()) { for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) { BluetoothGattDescriptorCopy newDescriptor = new BluetoothGattDescriptorCopy(descriptor.getUuid(), descriptor.getPermissions()); if (descriptor.getValue() != null) { newDescriptor.setValue(Arrays.copyOf(descriptor.getValue(), descriptor.getValue().length)); } newCharacteristic.addDescriptor(newDescriptor); } } if (characteristic.getService() != null) { BluetoothGattServiceCopy newService = new BluetoothGattServiceCopy(characteristic.getService().getUuid(), characteristic.getService().getType()); newService.addCharacteristic(newCharacteristic); } return newCharacteristic; } /** * Will fetch the bluetooth adapter or return null if it's not available * * @param context The android context * @return The bluetooth adapter or null */ public @Nullable BluetoothAdapter getBluetoothAdapter(Context context) { BluetoothManager manager = getBluetoothManager(context); if (manager == null) { return null; } BluetoothAdapter adapter = manager.getAdapter(); if (adapter == null) { return null; } return adapter; } public @Nullable BluetoothManager getBluetoothManager(Context context) { return (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); } /** * To prevent the descriptor from changing out from under us we need to copy it * <p> * This may happen under high throughput/concurrency * * @param descriptor The descriptor to be copied * @return The shallow-ish copy of the descriptor */ public @Nullable BluetoothGattDescriptorCopy copyDescriptor(@Nullable BluetoothGattDescriptor descriptor) { if (null == descriptor || null == descriptor.getUuid()) { return null; } BluetoothGattDescriptorCopy newDescriptor = new BluetoothGattDescriptorCopy(descriptor.getUuid(), descriptor.getPermissions()); if (newDescriptor.getValue() != null) { newDescriptor.setValue(Arrays.copyOf(descriptor.getValue(), descriptor.getValue().length)); } if (newDescriptor.getCharacteristic() != null) { BluetoothGattCharacteristicCopy oldCharacteristic = newDescriptor.getCharacteristic(); BluetoothGattCharacteristicCopy copyOfCharacteristic = new BluetoothGattCharacteristicCopy(oldCharacteristic.getUuid(), oldCharacteristic.getProperties(), oldCharacteristic.getPermissions()); if (oldCharacteristic.getValue() != null) { copyOfCharacteristic.setValue(Arrays.copyOf(oldCharacteristic.getValue(), oldCharacteristic.getValue().length)); } copyOfCharacteristic.addDescriptor(newDescriptor); } return newDescriptor; } /** * Will return the copy of the service * * @param service The gatt service * @return a shallow-ish copy of the service */ public @Nullable BluetoothGattServiceCopy copyService(@Nullable BluetoothGattService service) { if (null == service || null == service.getUuid()) { return null; } BluetoothGattServiceCopy newService = new BluetoothGattServiceCopy(service.getUuid(), service.getType()); if (!service.getIncludedServices().isEmpty()) { for (BluetoothGattService includedService : service.getIncludedServices()) { BluetoothGattServiceCopy newGattService = new BluetoothGattServiceCopy(includedService.getUuid(), includedService.getType()); newService.addService(newGattService); } } if (!service.getCharacteristics().isEmpty()) { // why not use the copy characteristic method, it will implicitly link itself to the null service for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { BluetoothGattCharacteristicCopy newCharacteristic = new BluetoothGattCharacteristicCopy(characteristic.getUuid(), characteristic.getProperties(), characteristic.getPermissions()); if (characteristic.getValue() != null) { newCharacteristic.setValue(Arrays.copyOf(characteristic.getValue(), characteristic.getValue().length)); } // why not use the copy descriptor method? It will implicitly link itself to the null characteristic for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) { BluetoothGattDescriptorCopy newDescriptor = new BluetoothGattDescriptorCopy(descriptor.getUuid(), descriptor.getPermissions()); if (descriptor.getValue() != null) { newDescriptor.setValue(Arrays.copyOf(descriptor.getValue(), descriptor.getValue().length)); } newCharacteristic.addDescriptor(newDescriptor); } newService.addCharacteristic(newCharacteristic); } } return newService; } /** * In some operations, it is important for us to be able to determine if a particular perhiperhal * is actually connected to the phone at the ACL level as our access to the GATT is somewhat * limited. We understand that some phones lie, saying that all of it's bonded devices are connected, * like the HTC One M9 on 5.0.2 * * @param context The android context * @param device The bluetooth device which we want to check it's status * @return True if the device is connected to the phone, false if not */ public boolean isPerhipheralCurrentlyConnectedToPhone(@Nullable Context context, @Nullable BluetoothDevice device) { if (context == null || device == null) { return false; } BluetoothManager mgr = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); if (mgr != null) { List<BluetoothDevice> devices = mgr.getConnectedDevices(BluetoothProfile.GATT); return devices.contains(device); } else { return false; } } /** * Protect against NPEs while getting the bluetooth device name if we suspect that it might * not be set on the remote peripheral * * In release this method will return "Unknown Name". * * @param localGatt The {@link BluetoothGatt} instance * @return The device name or "unknown" if null */ public String debugSafeGetBtDeviceName(@Nullable BluetoothGatt localGatt) { //The problem with accessing this is that it may not be cached on some phones // resulting a long blocking operation if(BuildConfig.DEBUG) { try { if (localGatt == null || localGatt.getDevice() == null) { throw new NullPointerException("The device was null inside of the gatt"); } String btName = localGatt.getDevice().getName(); if (btName != null) { return btName; } } catch (NullPointerException ex) { // https://fabric.io/fitbit7/android/apps/com.fitbit.fitbitmobile/issues/5a9637c68cb3c2fa63fa1333?time=last-seven-days Timber.e(ex, "get name internal to the gatt failed with an Parcel Read Exception NPE"); } } return "Unknown Name"; } /** * Protect against NPEs while getting the bluetooth device name if we suspect that it might * not be set on the remote peripheral * * @param device The {@link BluetoothDevice} instance * @return The device name or "unknown" if null */ public String debugSafeGetBtDeviceName(@Nullable BluetoothDevice device) { try { if (device == null) { throw new NullPointerException("The device was null inside of the gatt"); } String btName = device.getName(); if (btName != null) { return btName; } } catch (NullPointerException ex) { // https://fabric.io/fitbit7/android/apps/com.fitbit.fitbitmobile/issues/5a9637c68cb3c2fa63fa1333?time=last-seven-days Timber.e(ex, "get name internal to the adapter failed with an Parcel Read Exception NPE"); } return "Unknown Name"; } /** * Will take an int from the bond state intent extra and translate it into a string * * @param bondState The bond state integer * @return The string value */ public String getBondStateDescription(int bondState) { switch (bondState) { case BluetoothDevice.BOND_NONE: return "NONE"; case BluetoothDevice.BOND_BONDED: return "BONDED"; case BluetoothDevice.BOND_BONDING: return "BONDING"; default: return "UNKNOWN"; } } /** * Will take an int from the Major Device Class and translate it into a string * * @param devType The device type numerical value * @return The string value */ public String getDevTypeDescription(int devType) { switch (devType) { case BluetoothDevice.DEVICE_TYPE_CLASSIC: return "CLASSIC"; case BluetoothDevice.DEVICE_TYPE_DUAL: return "DUAL"; case BluetoothDevice.DEVICE_TYPE_LE: return "LE"; case BluetoothDevice.DEVICE_TYPE_UNKNOWN: return "UNKNOWN"; default: return "UNKNOWN" + Integer.toString(devType); } } /** * Will take an int from the Major Device Class and translate it into a string * * @param majDevClass The major device class numerical value * @return The string value */ public String getMajDevClassDescription(int majDevClass) { switch (majDevClass) { case BluetoothClass.Device.Major.AUDIO_VIDEO: return "AUDIO_VIDEO"; case BluetoothClass.Device.Major.COMPUTER: return "COMPUTER"; case BluetoothClass.Device.Major.HEALTH: return "HEALTH"; case BluetoothClass.Device.Major.IMAGING: return "IMAGING"; case BluetoothClass.Device.Major.MISC: return "MISC"; case BluetoothClass.Device.Major.NETWORKING: return "NETWORKING"; case BluetoothClass.Device.Major.PERIPHERAL: return "PERIPHERAL"; case BluetoothClass.Device.Major.PHONE: return "PHONE"; case BluetoothClass.Device.Major.TOY: return "TOY"; case BluetoothClass.Device.Major.UNCATEGORIZED: return "UNCATEGORIZED"; case BluetoothClass.Device.Major.WEARABLE: return "WEARABLE"; default: return "UNKNOWN" + Integer.toString(majDevClass); } } /** * Will take an int from the Device Class and translate it into a string * * @param devClass The device class numerical value * @return The string value */ public String getDevClassDescription(int devClass) { switch (devClass) { case BluetoothClass.Device.AUDIO_VIDEO_CAMCORDER: return "AUDIO_VIDEO_CAMCORDER"; case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO: return "AUDIO_VIDEO_CAR_AUDIO"; case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE: return "AUDIO_VIDEO_HANDSFREE"; case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES: return "AUDIO_VIDEO_HEADPHONES"; case BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO: return "AUDIO_VIDEO_HIFI_AUDIO"; case BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER: return "AUDIO_VIDEO_LOUDSPEAKER"; case BluetoothClass.Device.AUDIO_VIDEO_MICROPHONE: return "AUDIO_VIDEO_MICROPHONE"; case BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO: return "AUDIO_VIDEO_PORTABLE_AUDIO"; case BluetoothClass.Device.AUDIO_VIDEO_SET_TOP_BOX: return "AUDIO_VIDEO_SET_TOP_BOX"; case BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED: return "AUDIO_VIDEO_UNCATEGORIZED"; case BluetoothClass.Device.AUDIO_VIDEO_VCR: return "AUDIO_VIDEO_VCR"; case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CAMERA: return "AUDIO_VIDEO_VIDEO_CAMERA"; case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CONFERENCING: return "AUDIO_VIDEO_VIDEO_CONFERENCING"; case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER: return "AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER"; case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_GAMING_TOY: return "AUDIO_VIDEO_VIDEO_GAMING_TOY"; case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_MONITOR: return "AUDIO_VIDEO_VIDEO_MONITOR"; case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET: return "AUDIO_VIDEO_WEARABLE_HEADSET"; case BluetoothClass.Device.COMPUTER_DESKTOP: return "COMPUTER_DESKTOP"; case BluetoothClass.Device.COMPUTER_HANDHELD_PC_PDA: return "COMPUTER_HANDHELD_PC_PDA"; case BluetoothClass.Device.COMPUTER_LAPTOP: return "COMPUTER_LAPTOP"; case BluetoothClass.Device.COMPUTER_PALM_SIZE_PC_PDA: return "COMPUTER_PALM_SIZE_PC_PDA"; case BluetoothClass.Device.COMPUTER_SERVER: return "COMPUTER_SERVER"; case BluetoothClass.Device.COMPUTER_UNCATEGORIZED: return "COMPUTER_UNCATEGORIZED"; case BluetoothClass.Device.COMPUTER_WEARABLE: return "COMPUTER_WEARABLE"; case BluetoothClass.Device.HEALTH_BLOOD_PRESSURE: return "HEALTH_BLOOD_PRESSURE"; case BluetoothClass.Device.HEALTH_DATA_DISPLAY: return "HEALTH_DATA_DISPLAY"; case BluetoothClass.Device.HEALTH_GLUCOSE: return "HEALTH_GLUCOSE"; case BluetoothClass.Device.HEALTH_PULSE_OXIMETER: return "HEALTH_PULSE_OXIMETER"; case BluetoothClass.Device.HEALTH_PULSE_RATE: return "HEALTH_PULSE_RATE"; case BluetoothClass.Device.HEALTH_THERMOMETER: return "HEALTH_THERMOMETER"; case BluetoothClass.Device.HEALTH_UNCATEGORIZED: return "HEALTH_UNCATEGORIZED"; case BluetoothClass.Device.HEALTH_WEIGHING: return "HEALTH_WEIGHING"; case BluetoothClass.Device.PHONE_CELLULAR: return "PHONE_CELLULAR"; case BluetoothClass.Device.PHONE_CORDLESS: return "PHONE_CORDLESS"; case BluetoothClass.Device.PHONE_ISDN: return "PHONE_ISDN"; case BluetoothClass.Device.PHONE_MODEM_OR_GATEWAY: return "PHONE_MODEM_OR_GATEWAY"; case BluetoothClass.Device.PHONE_SMART: return "PHONE_SMART"; case BluetoothClass.Device.PHONE_UNCATEGORIZED: return "PHONE_UNCATEGORIZED"; case BluetoothClass.Device.TOY_CONTROLLER: return "TOY_CONTROLLER"; case BluetoothClass.Device.TOY_DOLL_ACTION_FIGURE: return "TOY_DOLL_ACTION_FIGURE"; case BluetoothClass.Device.TOY_GAME: return "TOY_GAME"; case BluetoothClass.Device.TOY_ROBOT: return "TOY_ROBOT"; case BluetoothClass.Device.TOY_UNCATEGORIZED: return "TOY_UNCATEGORIZED"; case BluetoothClass.Device.TOY_VEHICLE: return "TOY_VEHICLE"; case BluetoothClass.Device.WEARABLE_GLASSES: return "WEARABLE_GLASSES"; case BluetoothClass.Device.WEARABLE_HELMET: return "WEARABLE_HELMET"; case BluetoothClass.Device.WEARABLE_JACKET: return "WEARABLE_JACKET"; case BluetoothClass.Device.WEARABLE_PAGER: return "WEARABLE_PAGER"; case BluetoothClass.Device.WEARABLE_UNCATEGORIZED: return "WEARABLE_UNCATEGORIZED"; case BluetoothClass.Device.WEARABLE_WRIST_WATCH: return "WEARABLE_WRIST_WATCH"; case BluetoothClass.Device.Major.UNCATEGORIZED: return "UNCATEGORIZED"; default: return "UNKNOWN" + Integer.toString(devClass); } } }