blob: fe416aadbe56a2369eee50e0ff6de4fea1cf0705 [file] [log] [blame]
/*
*
* Copyright (c) 013-2017 Nest Labs, Inc.
* All rights reserved.
*
* 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 nl.Weave.DeviceManager;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.os.Build;
import android.util.Log;
import java.util.ArrayList;
import java.util.UUID;
/*
* WeaveStack - a singleton object shared between all instances of
* WeaveDeviceManager.
*
* Holds list of active connections with mapping to the associated
* connection object.
*/
public final class WeaveStack
{
private static final String TAG = WeaveStack.class.getSimpleName();
public static final int INITIAL_CONNECTIONS = 4;
private static class BleMtuBlacklist
{
/**
* Will be set at initialization to indicate whether the device on which this code is being run
* is known to indicate unreliable MTU values for Bluetooth LE connections.
*/
static final boolean BLE_MTU_BLACKLISTED;
/**
* If {@link #BLE_MTU_BLACKLISTED} is true, then this is the fallback MTU to use for this device
*/
static final int BLE_MTU_FALLBACK = 23;
static
{
if ("OnePlus".equals(android.os.Build.MANUFACTURER))
{
if ("ONE A2005".equals(android.os.Build.MODEL))
{
BLE_MTU_BLACKLISTED = true;
}
else
{
BLE_MTU_BLACKLISTED = false;
}
}
else if ("motorola".equals(android.os.Build.MANUFACTURER))
{
if ("XT1575".equals(android.os.Build.MODEL) ||
"XT1585".equals(android.os.Build.MODEL))
{
BLE_MTU_BLACKLISTED = true;
}
else
{
BLE_MTU_BLACKLISTED = false;
}
}
else
{
BLE_MTU_BLACKLISTED = false;
}
}
}
/*
* Singleton instance of this class
*/
private static final WeaveStack sInstance = new WeaveStack();
/*
* Mapping of connections to connection objects
*/
private final ArrayList<WeaveDeviceManager> mConnections;
private BluetoothGattCallback mGattCallback;
private WeaveStack()
{
mConnections = new ArrayList<WeaveDeviceManager>(INITIAL_CONNECTIONS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
{
mGattCallback = new BluetoothGattCallback()
{
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
{
int connId = 0;
if (newState == BluetoothProfile.STATE_DISCONNECTED)
{
connId = getConnId(gatt);
if (connId > 0)
{
Log.d(TAG, "onConnectionStateChange Disconnected");
handleConnectionError(connId);
}
else
{
Log.e(TAG, "onConnectionStateChange disconnected: no active connection");
}
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status)
{
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status)
{
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status)
{
byte[] svcIdBytes = convertUUIDToBytes(characteristic.getService().getUuid());
byte[] charIdBytes = convertUUIDToBytes(characteristic.getUuid());
if (status != BluetoothGatt.GATT_SUCCESS)
{
Log.e(TAG, "onCharacteristicWrite for " + characteristic.getUuid().toString() + " failed with status: " + status);
return;
}
int connId = getConnId(gatt);
if (connId > 0)
{
handleWriteConfirmation(connId, svcIdBytes, charIdBytes, status == BluetoothGatt.GATT_SUCCESS);
}
else
{
Log.e(TAG, "onCharacteristicWrite no active connection");
return;
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic)
{
byte[] svcIdBytes = convertUUIDToBytes(characteristic.getService().getUuid());
byte[] charIdBytes = convertUUIDToBytes(characteristic.getUuid());
int connId = getConnId(gatt);
if (connId > 0)
{
handleIndicationReceived(connId, svcIdBytes, charIdBytes, characteristic.getValue());
}
else
{
Log.e(TAG, "onCharacteristicChanged no active connection");
return;
}
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt,
BluetoothGattDescriptor descriptor,
int status)
{
BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
byte[] svcIdBytes = convertUUIDToBytes(characteristic.getService().getUuid());
byte[] charIdBytes = convertUUIDToBytes(characteristic.getUuid());
if (status != BluetoothGatt.GATT_SUCCESS)
{
Log.e(TAG, "onDescriptorWrite for " + descriptor.getUuid().toString() + " failed with status: " + status);
}
int connId = getConnId(gatt);
if (connId == 0)
{
Log.e(TAG, "onDescriptorWrite no active connection");
return;
}
if (descriptor.getValue() == BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)
{
handleSubscribeComplete(connId, svcIdBytes, charIdBytes, status == BluetoothGatt.GATT_SUCCESS);
}
else if (descriptor.getValue() == BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)
{
handleUnsubscribeComplete(connId, svcIdBytes, charIdBytes, status == BluetoothGatt.GATT_SUCCESS);
}
else
{
Log.d(TAG, "Unexpected onDescriptorWrite().");
}
}
@Override
public void onDescriptorRead(BluetoothGatt gatt,
BluetoothGattDescriptor descriptor,
int status)
{
}
};
}
}
public static WeaveStack getInstance()
{
return sInstance;
}
public BluetoothGattCallback getCallback()
{
return mGattCallback;
}
public synchronized WeaveDeviceManager getConnection(int connId)
{
int connIndex = connId - 1;
if (connIndex >= 0 && connIndex < mConnections.size())
{
return mConnections.get(connIndex);
}
else
{
Log.e(TAG, "Unknown connId " + connId);
return null;
}
}
public synchronized int getConnId(BluetoothGatt gatt)
{
// Find callback given gatt
int connIndex = 0;
while (connIndex < mConnections.size())
{
WeaveDeviceManager devMgr = mConnections.get(connIndex);
if (devMgr != null)
{
if (gatt == devMgr.getBluetoothGatt())
{
return connIndex + 1;
}
}
connIndex++;
}
return 0;
}
// Returns connId, a 1's based version of the index.
public synchronized int addConnection(WeaveDeviceManager connObj)
{
int connIndex = 0;
while (connIndex < mConnections.size())
{
if (mConnections.get(connIndex) == null)
{
mConnections.set(connIndex, connObj);
return connIndex + 1;
}
connIndex++;
}
mConnections.add(connIndex, connObj);
return connIndex + 1;
}
public synchronized WeaveDeviceManager removeConnection(int connId)
{
int connIndex = connId - 1;
if (connIndex >= 0 && connIndex < mConnections.size())
{
// Set to null, rather than remove, so that other indexes are unchanged.
return mConnections.set(connIndex, null);
}
else
{
Log.e(TAG, "Trying to remove unknown connId " + connId);
return null;
}
}
public static void onNotifyWeaveConnectionClosed(int connId)
{
WeaveDeviceManager deviceManager = WeaveStack.getInstance().getConnection(connId);
deviceManager.onNotifyWeaveConnectionClosed(connId);
}
public static boolean onSendCharacteristic(int connId, byte[] svcId, byte[] charId, byte[] characteristicData)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
{
WeaveDeviceManager deviceManager = WeaveStack.getInstance().getConnection(connId);
BluetoothGatt bluetoothGatt = deviceManager.getBluetoothGatt();
if (bluetoothGatt == null)
return false;
UUID svcUUID = convertBytesToUUID(svcId);
BluetoothGattService sendSvc = bluetoothGatt.getService(svcUUID);
if (sendSvc == null)
{
Log.e(TAG, "Bad service");
return false;
}
UUID charUUID = convertBytesToUUID(charId);
BluetoothGattCharacteristic sendChar = sendSvc.getCharacteristic(charUUID);
if (!sendChar.setValue(characteristicData))
{
Log.e(TAG, "Failed to set characteristic");
return false;
}
if (!bluetoothGatt.writeCharacteristic(sendChar))
{
Log.e(TAG, "Failed writing char");
return false;
}
return true;
}
else
{
Log.e(TAG, "BLE not supported on device.");
}
return false;
}
public static boolean onSubscribeCharacteristic(int connId, byte[] svcId, byte[] charId)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
{
WeaveDeviceManager deviceManager = WeaveStack.getInstance().getConnection(connId);
BluetoothGatt bluetoothGatt = deviceManager.getBluetoothGatt();
if (bluetoothGatt == null)
return false;
UUID svcUUID = convertBytesToUUID(svcId);
BluetoothGattService subscribeSvc = bluetoothGatt.getService(svcUUID);
if (subscribeSvc == null)
{
Log.e(TAG, "Bad service");
return false;
}
UUID charUUID = convertBytesToUUID(charId);
BluetoothGattCharacteristic subscribeChar = subscribeSvc.getCharacteristic(charUUID);
if (subscribeChar == null)
{
Log.e(TAG, "Bad characteristic");
return false;
}
if (!bluetoothGatt.setCharacteristicNotification(subscribeChar, true))
{
Log.e(TAG, "Failed to subscribe to characteristic.");
return false;
}
BluetoothGattDescriptor descriptor =
subscribeChar.getDescriptor(UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
if (!bluetoothGatt.writeDescriptor(descriptor))
{
Log.e(TAG, "writeDescriptor failed");
return false;
}
return true;
}
else
{
Log.e(TAG, "BLE not supported on device.");
}
return false;
}
public static boolean onUnsubscribeCharacteristic(int connId, byte[] svcId, byte[] charId)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
{
WeaveDeviceManager deviceManager = WeaveStack.getInstance().getConnection(connId);
BluetoothGatt bluetoothGatt = deviceManager.getBluetoothGatt();
if (bluetoothGatt == null)
return false;
UUID svcUUID = convertBytesToUUID(svcId);
BluetoothGattService subscribeSvc = bluetoothGatt.getService(svcUUID);
if (subscribeSvc == null)
{
Log.e(TAG, "Bad service");
return false;
}
UUID charUUID = convertBytesToUUID(charId);
BluetoothGattCharacteristic subscribeChar = subscribeSvc.getCharacteristic(charUUID);
if (subscribeChar == null)
{
Log.e(TAG, "Bad characteristic");
return false;
}
if (!bluetoothGatt.setCharacteristicNotification(subscribeChar, false))
{
Log.e(TAG, "Failed to unsubscribe to characteristic.");
return false;
}
BluetoothGattDescriptor descriptor =
subscribeChar.getDescriptor(UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
if (!bluetoothGatt.writeDescriptor(descriptor))
{
Log.e(TAG, "writeDescriptor failed");
return false;
}
return true;
}
else
{
Log.e(TAG, "BLE not supported on device.");
}
return false;
}
public static boolean onCloseConnection(int connId)
{
WeaveDeviceManager deviceManager = WeaveStack.getInstance().getConnection(connId);
deviceManager.onCloseBleComplete(connId);
return true;
}
// onGetMTU returns the desired MTU for the BLE connection.
// In most cases, a value of 0 is used to indicate no preference.
// On some devices, we override to use the minimum MTU to work around device bugs.
public static int onGetMTU(int connId)
{
int mtu = 0;
Log.d(TAG, "Android Manufacturer: (" + android.os.Build.MANUFACTURER + ")");
Log.d(TAG, "Android Model: (" + android.os.Build.MODEL + ")");
if (BleMtuBlacklist.BLE_MTU_BLACKLISTED)
{
mtu = BleMtuBlacklist.BLE_MTU_FALLBACK;
Log.e(TAG, "Detected Android Manufacturer/Model with MTU compatibiility issues. Reporting mtu of " + mtu);
}
return mtu;
}
// ----- Private Members -----
static {
System.loadLibrary("WeaveDeviceManager");
}
private native void handleWriteConfirmation(int connId, byte[] svcId, byte[] charId, boolean success);
private native void handleIndicationReceived(int connId, byte[] svcId, byte[] charId, byte[] data);
private native void handleSubscribeComplete(int connId, byte[] svcId, byte[] charId, boolean success);
private native void handleUnsubscribeComplete(int connId, byte[] svcId, byte[] charId, boolean success);
private native void handleConnectionError(int connId);
// CLIENT_CHARACTERISTIC_CONFIG is the well-known UUID of the client characteristic descriptor that has the
// flags for enabling and disabling notifications and indications.
// c.f. https://www.bluetooth.org/en-us/specification/assigned-numbers/generic-attribute-profile
private static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";
private static byte[] convertUUIDToBytes(UUID uuid)
{
byte[] idBytes = new byte[16];
long idBits;
idBits = uuid.getLeastSignificantBits();
for (int i = 0; i < 8; i++)
{
idBytes[15 - i] = (byte) (idBits & 0xff);
idBits = idBits >> 8;
}
idBits = uuid.getMostSignificantBits();
for (int i = 0; i < 8; i++)
{
idBytes[7 - i] = (byte) (idBits & 0xff);
idBits = idBits >> 8;
}
return idBytes;
}
private static UUID convertBytesToUUID(byte[] id)
{
long mostSigBits = 0;
long leastSigBits = 0;
if (id.length == 16)
{
for (int i = 0; i < 8; i++)
{
mostSigBits = (mostSigBits << 8) | (0xff & id[i]);
}
for (int i = 0; i < 8; i++)
{
leastSigBits = (leastSigBits << 8) | (0xff & id[i + 8]);
}
}
return new UUID(mostSigBits, leastSigBits);
}
}