diff options
Diffstat (limited to 'src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLEServer.java')
-rw-r--r-- | src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLEServer.java | 989 |
1 files changed, 989 insertions, 0 deletions
diff --git a/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLEServer.java b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLEServer.java new file mode 100644 index 00000000..94601403 --- /dev/null +++ b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLEServer.java @@ -0,0 +1,989 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +package org.qtproject.qt.android.bluetooth; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.content.Context; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattServer; +import android.bluetooth.BluetoothGattServerCallback; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.le.AdvertiseCallback; +import android.bluetooth.le.AdvertiseData; +import android.bluetooth.le.AdvertiseData.Builder; +import android.bluetooth.le.AdvertiseSettings; +import android.bluetooth.le.BluetoothLeAdvertiser; +import android.os.ParcelUuid; +import android.os.Build; +import android.util.Log; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.HashMap; +import java.util.UUID; + +public class QtBluetoothLEServer { + private static final String TAG = "QtBluetoothGattServer"; + + /* Pointer to the Qt object that "owns" the Java object */ + @SuppressWarnings({"CanBeFinal", "WeakerAccess"}) + long qtObject = 0; + @SuppressWarnings("WeakerAccess") + + private Context qtContext = null; + + // Bluetooth members + private BluetoothAdapter mBluetoothAdapter = null; + private BluetoothManager mBluetoothManager = null; + private BluetoothGattServer mGattServer = null; + private BluetoothLeAdvertiser mLeAdvertiser = null; + + private ArrayList<BluetoothGattService> mPendingServiceAdditions = + new ArrayList<BluetoothGattService>(); + + private String mRemoteName = ""; + // This function is called from Qt thread + public synchronized String remoteName() { + return mRemoteName; + } + + private String mRemoteAddress = ""; + // This function is called from Qt thread + public synchronized String remoteAddress() { + return mRemoteAddress; + } + + // BT Core v5.3, 5.2.1, Vol 3, Part G + private static final int DEFAULT_LE_ATT_MTU = 23; + // Holds the currently supported/used MTU + private int mSupportedMtu = DEFAULT_LE_ATT_MTU; + // Implementation defined limit + private static final int MAX_PENDING_WRITE_COUNT = 1024; + // BT Core v5.3, 3.4.6.1, Vol 3, Part F + private static final int GATT_ERROR_PREPARE_QUEUE_FULL = 0x9; + // BT Core v5.3, 3.2.9, Vol 3, Part F + private static final int BTLE_MAX_ATTRIBUTE_VALUE_SIZE = 512; + + // The class stores queued writes from the remote device. The writes are + // executed later when instructed to do so by onExecuteWrite() callback. + private class WriteEntry { + WriteEntry(BluetoothDevice remoteDevice, Object target) { + this.remoteDevice = remoteDevice; + this.target = target; + this.writes = new ArrayList<Pair<byte[], Integer>>(); + } + // Returns true if this is a proper entry for given device + target + public boolean match(BluetoothDevice device, Object target) { + return remoteDevice.equals(device) && target.equals(target); + } + public final BluetoothDevice remoteDevice; // Device that issued the writes + public final Object target; // Characteristic or Descriptor + public final List<Pair<byte[], Integer>> writes; // Value, offset + } + private final List<WriteEntry> mPendingPreparedWrites = new ArrayList<>(); + + // Helper function to clear the pending writes of a remote device. If the provided device + // is null, all writes are cleared + private void clearPendingPreparedWrites(Object device) { + if (device == null) + mPendingPreparedWrites.clear(); + ListIterator<WriteEntry> iterator = mPendingPreparedWrites.listIterator(); + while (iterator.hasNext()) { + if (iterator.next().remoteDevice.equals(device)) + iterator.remove(); + } + } + + // The function adds a 'prepared write' entry to target's queue. If the "target + device" + // didn't have a queue before (this being the first write), the queue is created. + // Targets must be either descriptors or characteristics. + private int addPendingPreparedWrite(BluetoothDevice device, Object target, + int offset, byte[] value) { + WriteEntry entry = null; + int currentWriteCount = 0; + + // Try to find an existing matching entry. Also while looping, count + // the total number of writes so far in order to know if we exceed the + // write queue size we have set for ourselves + for (WriteEntry e : mPendingPreparedWrites) { + if (e.match(device, target)) + entry = e; + currentWriteCount += e.writes.size(); + } + + // BT Core v5.3, 3.4.6.1, Vol 3, Part F + if (currentWriteCount > MAX_PENDING_WRITE_COUNT) { + Log.w(TAG, "Prepared write queue is full, returning an error."); + return GATT_ERROR_PREPARE_QUEUE_FULL; + } + + // If no matching entry, create a new one. This means this is the first prepared + // write request to this "device + target" combination + if (entry == null) + mPendingPreparedWrites.add(entry = new WriteEntry(device, target)); + + // Append the newly received chunk of data along with its offset + entry.writes.add(new Pair<byte[], Integer>(value, offset)); + return BluetoothGatt.GATT_SUCCESS; + } + + /* + As per Bluetooth specification each connected device can have individual and persistent + Client characteristic configurations (see Bluetooth Spec 5.0 Vol 3 Part G 3.3.3.3) + This class manages the existing configurrations. + */ + private class ClientCharacteristicManager { + private final HashMap<BluetoothGattCharacteristic, List<Entry>> notificationStore = new HashMap<BluetoothGattCharacteristic, List<Entry>>(); + + private class Entry { + BluetoothDevice device = null; + byte[] value = null; + boolean isConnected = false; + } + + public void insertOrUpdate(BluetoothGattCharacteristic characteristic, + BluetoothDevice device, byte[] newValue) + { + if (notificationStore.containsKey(characteristic)) { + + List<Entry> entries = notificationStore.get(characteristic); + for (int i = 0; i < entries.size(); i++) { + if (entries.get(i).device.equals(device)) { + Entry e = entries.get(i); + e.value = newValue; + entries.set(i, e); + return; + } + } + + // not match so far -> add device to list + Entry e = new Entry(); + e.device = device; + e.value = newValue; + e.isConnected = true; + entries.add(e); + return; + } + + // new characteristic + Entry e = new Entry(); + e.device = device; + e.value = newValue; + e.isConnected = true; + List<Entry> list = new LinkedList<Entry>(); + list.add(e); + notificationStore.put(characteristic, list); + } + + /* + Marks client characteristic configuration entries as (in)active based the associated + devices general connectivity state. + This function avoids that existing configurations are not acted + upon when the associated device is not connected. + */ + public void markDeviceConnectivity(BluetoothDevice device, boolean isConnected) + { + final Iterator<BluetoothGattCharacteristic> keys = notificationStore.keySet().iterator(); + while (keys.hasNext()) { + final BluetoothGattCharacteristic characteristic = keys.next(); + final List<Entry> entries = notificationStore.get(characteristic); + if (entries == null) + continue; + + ListIterator<Entry> charConfig = entries.listIterator(); + while (charConfig.hasNext()) { + Entry e = charConfig.next(); + if (e.device.equals(device)) + e.isConnected = isConnected; + } + } + } + + // Returns list of all BluetoothDevices which require notification or indication. + // No match returns an empty list. + List<BluetoothDevice> getToBeUpdatedDevices(BluetoothGattCharacteristic characteristic) + { + ArrayList<BluetoothDevice> result = new ArrayList<BluetoothDevice>(); + if (!notificationStore.containsKey(characteristic)) + return result; + + final ListIterator<Entry> iter = notificationStore.get(characteristic).listIterator(); + while (iter.hasNext()) + result.add(iter.next().device); + + return result; + } + + // Returns null if no match; otherwise the configured actual client characteristic + // configuration value + byte[] valueFor(BluetoothGattCharacteristic characteristic, BluetoothDevice device) + { + if (!notificationStore.containsKey(characteristic)) + return null; + + List<Entry> entries = notificationStore.get(characteristic); + for (int i = 0; i < entries.size(); i++) { + final Entry entry = entries.get(i); + if (entry.device.equals(device) && entry.isConnected == true) + return entries.get(i).value; + } + + return null; + } + } + + private static final UUID CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = UUID + .fromString("00002902-0000-1000-8000-00805f9b34fb"); + ClientCharacteristicManager clientCharacteristicManager = new ClientCharacteristicManager(); + + public QtBluetoothLEServer(Context context) + { + qtContext = context; + if (qtContext == null) { + Log.w(TAG, "Missing context object. Peripheral role disabled."); + return; + } + + mBluetoothManager = + (BluetoothManager) qtContext.getSystemService(Context.BLUETOOTH_SERVICE); + if (mBluetoothManager == null) { + Log.w(TAG, "Bluetooth service not available. Peripheral role disabled."); + return; + } + + mBluetoothAdapter = mBluetoothManager.getAdapter(); + if (mBluetoothAdapter == null) { + Log.w(TAG, "Missing Bluetooth adapter. Peripheral role disabled."); + return; + } + + Log.w(TAG, "Let's do BTLE Peripheral."); + } + + // The following functions are synchronized callback handlers. The callbacks + // from Android are forwarded to these methods to synchronize member variable + // access with other threads (the Qt thread's JNI calls in particular). + // + // We use a single lock object (this server) for simplicity because: + // - Some variables may change and would thus not be suitable as locking objects but + // would require their own additional objects => overhead + // - Many accesses to shared variables are infrequent and the code paths are fast and + // deterministic meaning that long "wait times" on a lock should not happen + // - Typically several shared variables are accessed in a single code block. + // If each variable would be protected individually, the amount of (nested) locking + // would become quite unreasonable + + public synchronized void handleOnConnectionStateChange(BluetoothDevice device, + int status, int newState) + { + if (mGattServer == null) { + Log.w(TAG, "Ignoring connection state event, server is disconnected"); + return; + } + // Multiple GATT devices may be connected. Check if we still have connected + // devices or not, and set the server state accordingly. Note: it seems we get + // notifications from all GATT clients, not just from the ones interested in + // the services provided by this BT LE Server. Furthermore the list of + // currently connected devices does not appear to be in any particular order. + List<BluetoothDevice> connectedDevices = + mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER); + Log.w(TAG, "Device " + device + " connection state: " + newState + ", status: " + + status + ", connected devices: " + connectedDevices); + // 0 == QLowEnergyController::UnconnectedState + // 2 == QLowEnergyController::ConnectedState + int qtControllerState = connectedDevices.size() > 0 ? 2 : 0; + + switch (newState) { + case BluetoothProfile.STATE_CONNECTED: + clientCharacteristicManager.markDeviceConnectivity(device, true); + mRemoteName = device.getName(); + mRemoteAddress = device.getAddress(); + break; + case BluetoothProfile.STATE_DISCONNECTED: + clientCharacteristicManager.markDeviceConnectivity(device, false); + clearPendingPreparedWrites(device); + // Update the remoteAddress and remoteName if needed + if (device.getAddress().equals(mRemoteAddress) + && !connectedDevices.isEmpty()) { + mRemoteName = connectedDevices.get(0).getName(); + mRemoteAddress = connectedDevices.get(0).getAddress(); + } + break; + default: + // According to the API doc of this callback this should not happen + Log.w(TAG, "Unhandled connection state change: " + newState); + return; + } + + // If last client disconnected, close down the server + if (qtControllerState == 0) { // QLowEnergyController::UnconnectedState + mPendingServiceAdditions.clear(); + mGattServer.close(); + mGattServer = null; + mRemoteName = ""; + mRemoteAddress = ""; + mSupportedMtu = DEFAULT_LE_ATT_MTU; + } + + int qtErrorCode; + switch (status) { + case BluetoothGatt.GATT_SUCCESS: + qtErrorCode = 0; + break; + default: + Log.w(TAG, "Unhandled error code on peripheral connectionStateChanged: " + + status + " " + newState); + qtErrorCode = status; + break; + } + + leConnectionStateChange(qtObject, qtErrorCode, qtControllerState); + } + + public synchronized void handleOnServiceAdded(int status, BluetoothGattService service) + { + if (mGattServer == null) { + Log.w(TAG, "Ignoring service addition event, server is disconnected"); + return; + } + + Log.d(TAG, "Service " + service.getUuid().toString() + " addition result: " + status); + + // Remove the indicated service from the pending queue + ListIterator<BluetoothGattService> iterator = mPendingServiceAdditions.listIterator(); + while (iterator.hasNext()) { + if (iterator.next().getUuid().equals(service.getUuid())) { + iterator.remove(); + break; + } + } + + // If there are more services in the queue, add the next whose add initiation succeeds + iterator = mPendingServiceAdditions.listIterator(); + while (iterator.hasNext()) { + BluetoothGattService nextService = iterator.next(); + if (mGattServer.addService(nextService)) { + break; + } else { + Log.w(TAG, "Adding service " + nextService.getUuid().toString() + " failed"); + iterator.remove(); + } + } + } + + public synchronized void handleOnCharacteristicReadRequest(BluetoothDevice device, + int requestId, int offset, + BluetoothGattCharacteristic characteristic) + { + if (mGattServer == null) { + Log.w(TAG, "Ignoring characteristic read, server is disconnected"); + return; + } + + byte[] characteristicData = + ((QtBluetoothGattCharacteristic)characteristic).getLocalValue(); + + try { + byte[] dataArray = Arrays.copyOfRange(characteristicData, + offset, characteristicData.length); + mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, + offset, dataArray); + } catch (Exception ex) { + Log.w(TAG, "onCharacteristicReadRequest: " + requestId + " " + + offset + " " + characteristicData.length); + ex.printStackTrace(); + mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, + offset, null); + } + } + + public synchronized void handleOnCharacteristicWriteRequest(BluetoothDevice device, + int requestId, + BluetoothGattCharacteristic characteristic, + boolean preparedWrite, boolean responseNeeded, + int offset, byte[] value) + { + if (mGattServer == null) { + Log.w(TAG, "Ignoring characteristic write, server is disconnected"); + return; + } + Log.w(TAG, "onCharacteristicWriteRequest " + preparedWrite + " " + offset + " " + + value.length); + final int minValueLen = ((QtBluetoothGattCharacteristic)characteristic).minValueLength; + final int maxValueLen = ((QtBluetoothGattCharacteristic)characteristic).maxValueLength; + + int resultStatus = BluetoothGatt.GATT_SUCCESS; + boolean sendNotificationOrIndication = false; + + if (!preparedWrite) { // regular write + // User may have defined minimum and maximum size for the value, which + // we enforce here. If the user has not defined these limits, the default + // values 0..INT_MAX do not limit anything. + if (value.length < minValueLen || value.length > maxValueLen) { + // BT Core v 5.3, 4.9.3, Vol 3, Part G + Log.w(TAG, "onCharacteristicWriteRequest invalid char value length: " + + value.length + ", min: " + minValueLen + ", max: " + maxValueLen); + resultStatus = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; + } else if (offset == 0) { + ((QtBluetoothGattCharacteristic)characteristic).setLocalValue(value); + leServerCharacteristicChanged(qtObject, characteristic, value); + sendNotificationOrIndication = true; + } else { + // This should not really happen as per Bluetooth spec + Log.w(TAG, "onCharacteristicWriteRequest: !preparedWrite, offset " + + offset + ", Not supported"); + resultStatus = BluetoothGatt.GATT_INVALID_OFFSET; + } + } else { + // BT Core v5.3, 3.4.6, Vol 3, Part F + // This is a prepared write which is used to write characteristics larger than + // MTU. We need to record all requests and execute them in one go once + // onExecuteWrite() is received. We use a queue to remember the pending + // requests. + resultStatus = addPendingPreparedWrite(device, characteristic, offset, value); + } + + if (responseNeeded) + mGattServer.sendResponse(device, requestId, resultStatus, offset, value); + if (sendNotificationOrIndication) + sendNotificationsOrIndications(characteristic); + } + + public synchronized void handleOnDescriptorReadRequest(BluetoothDevice device, int requestId, + int offset, BluetoothGattDescriptor descriptor) + { + if (mGattServer == null) { + Log.w(TAG, "Ignoring descriptor read, server is disconnected"); + return; + } + + byte[] dataArray = ((QtBluetoothGattDescriptor)descriptor).getLocalValue(); + + try { + if (descriptor.getUuid().equals(CLIENT_CHARACTERISTIC_CONFIGURATION_UUID)) { + dataArray = clientCharacteristicManager.valueFor( + descriptor.getCharacteristic(), device); + if (dataArray == null) + dataArray = ((QtBluetoothGattDescriptor)descriptor).getLocalValue(); + } + + dataArray = Arrays.copyOfRange(dataArray, offset, dataArray.length); + mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, + offset, dataArray); + } catch (Exception ex) { + Log.w(TAG, "onDescriptorReadRequest: " + requestId + " " + + offset + " " + dataArray.length); + ex.printStackTrace(); + mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, + offset, null); + } + } + + public synchronized void handleOnDescriptorWriteRequest(BluetoothDevice device, int requestId, + BluetoothGattDescriptor descriptor, boolean preparedWrite, + boolean responseNeeded, int offset, byte[] value) + { + if (mGattServer == null) { + Log.w(TAG, "Ignoring descriptor write, server is disconnected"); + return; + } + + Log.w(TAG, "onDescriptorWriteRequest " + preparedWrite + " " + offset + " " + value.length); + int resultStatus = BluetoothGatt.GATT_SUCCESS; + + if (!preparedWrite) { // regular write + if (offset == 0) { + if (descriptor.getUuid().equals(CLIENT_CHARACTERISTIC_CONFIGURATION_UUID)) { + // If both IND and NTF are requested, resort to NTF only. BT + // specification does not prohibit nor mention using both, but it is + // unlikely what the client intended. Stack behaviours vary; + // Apple client-side stack does not allow this, while Bluez client-side + // stack erroneously sends this even if the developer only asked for + // the other. The 0x03 value is a bitwise combination of 0x01 and 0x02 + // as per specification: BT Core v5.3, 3.3.3.3, Vol 3, Part G + if (value[0] == 0x03) { + Log.w(TAG, "Warning: In CCC of characteristic: " + + descriptor.getCharacteristic().getUuid() + + " enabling both NTF & IND requested, enabling NTF only."); + value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; + } + clientCharacteristicManager.insertOrUpdate( + descriptor.getCharacteristic(), + device, value); + } + ((QtBluetoothGattDescriptor)descriptor).setLocalValue(value); + leServerDescriptorWritten(qtObject, descriptor, value); + } else { + // This should not really happen as per Bluetooth spec + Log.w(TAG, "onDescriptorWriteRequest: !preparedWrite, offset " + + offset + ", Not supported"); + resultStatus = BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED; + } + } else { + // BT Core v5.3, 3.4.6, Vol 3, Part F + // This is a prepared write which is used to write descriptors larger than MTU. + // We need to record all requests and execute them in one go once + // onExecuteWrite() is received. We use a queue to remember the pending + // requests. + resultStatus = addPendingPreparedWrite(device, descriptor, offset, value); + } + + if (responseNeeded) + mGattServer.sendResponse(device, requestId, resultStatus, offset, value); + } + + public synchronized void handleOnExecuteWrite(BluetoothDevice device, + int requestId, boolean execute) + { + if (mGattServer == null) { + Log.w(TAG, "Ignoring execute write, server is disconnected"); + return; + } + + Log.w(TAG, "onExecuteWrite " + device + " " + requestId + " " + execute); + + if (execute) { + // BT Core v5.3, 3.4.6.3, Vol 3, Part F + // Execute all pending prepared writes for the provided 'device' + for (WriteEntry entry : mPendingPreparedWrites) { + if (!entry.remoteDevice.equals(device)) + continue; + + byte[] newValue = null; + // The target can be a descriptor or a characteristic + byte[] currentValue = (entry.target instanceof BluetoothGattCharacteristic) + ? ((QtBluetoothGattCharacteristic)entry.target).getLocalValue() + : ((QtBluetoothGattDescriptor)entry.target).getLocalValue(); + + // Iterate writes and apply them to the currentValue in received order + for (Pair<byte[], Integer> write : entry.writes) { + // write.first is data, write.second.intValue() is offset. Check + // that the offset is not beyond the length of the current value + if (write.second.intValue() > currentValue.length) { + clearPendingPreparedWrites(device); + // BT Core v5.3, 3.4.6.3, Vol 3, Part F + mGattServer.sendResponse(device, requestId, + BluetoothGatt.GATT_INVALID_OFFSET, + 0, null); + return; + } + + // User may have defined value minimum and maximum sizes for + // characteristics, which we enforce here. If the user has not defined + // these limits, the default values 0..INT_MAX do not limit anything. + // The value size cannot decrease in prepared write (small write is a + // partial update) => no check for the minimum size limit here. + if (entry.target instanceof QtBluetoothGattCharacteristic && + (write.second.intValue() + write.first.length > + ((QtBluetoothGattCharacteristic)entry.target).maxValueLength)) { + clearPendingPreparedWrites(device); + // BT Core v5.3, 3.4.6.3, Vol 3, Part F + mGattServer.sendResponse(device, requestId, + BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH, + 0, null); + return; + } + + // Determine the size of the new value as we may be extending the + // current value size + newValue = new byte[Math.max(write.second.intValue() + + write.first.length, currentValue.length)]; + // Copy the current value to the newValue. We can't use the currentValue + // directly because the length of value might increase by this write + System.arraycopy(currentValue, 0, newValue, 0, currentValue.length); + // Apply this iteration's write to the newValue + System.arraycopy(write.first, 0, newValue, write.second.intValue(), + write.first.length); + // Update the currentValue as there may be more writes to apply + currentValue = newValue; + } + + // Update value and inform the Qt/C++ side on the update + if (entry.target instanceof BluetoothGattCharacteristic) { + ((QtBluetoothGattCharacteristic)entry.target).setLocalValue(newValue); + leServerCharacteristicChanged( + qtObject, (BluetoothGattCharacteristic)entry.target, newValue); + } else { + ((QtBluetoothGattDescriptor)entry.target).setLocalValue(newValue); + leServerDescriptorWritten( + qtObject, (BluetoothGattDescriptor)entry.target, newValue); + } + } + } + // Either we executed all writes or were asked to cancel. + // In any case clear writes and respond. + clearPendingPreparedWrites(device); + mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null); + } + + public synchronized void handleOnMtuChanged(BluetoothDevice device, int mtu) + { + if (mSupportedMtu == mtu) + return; + mSupportedMtu = mtu; + leMtuChanged(qtObject, mSupportedMtu); + } + + /* + * Call back handler for the Gatt Server. + */ + private BluetoothGattServerCallback mGattServerListener = new BluetoothGattServerCallback() + { + @Override + public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { + super.onConnectionStateChange(device, status, newState); + handleOnConnectionStateChange(device, status, newState); + } + + @Override + public void onServiceAdded(int status, BluetoothGattService service) { + super.onServiceAdded(status, service); + handleOnServiceAdded(status, service); + } + + @Override + public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) + { + super.onCharacteristicReadRequest(device, requestId, offset, characteristic); + handleOnCharacteristicReadRequest(device, requestId, offset, characteristic); + } + + @Override + public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, + boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) + { + super.onCharacteristicWriteRequest(device, requestId, characteristic, + preparedWrite, responseNeeded, offset, value); + handleOnCharacteristicWriteRequest(device, requestId, characteristic, + preparedWrite, responseNeeded, offset, value); + } + + @Override + public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) + { + super.onDescriptorReadRequest(device, requestId, offset, descriptor); + handleOnDescriptorReadRequest(device, requestId, offset, descriptor); + } + + @Override + public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, + boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) + { + super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, + responseNeeded, offset, value); + handleOnDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, + responseNeeded, offset, value); + } + + @Override + public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) + { + super.onExecuteWrite(device, requestId, execute); + handleOnExecuteWrite(device, requestId, execute); + } + + @Override + public void onNotificationSent(BluetoothDevice device, int status) { + super.onNotificationSent(device, status); + Log.w(TAG, "onNotificationSent" + device + " " + status); + } + + @Override + public void onMtuChanged(BluetoothDevice device, int mtu) { + handleOnMtuChanged(device, mtu); + } + }; + + // This function is called from Qt thread + public synchronized int mtu() { + return mSupportedMtu; + } + + // This function is called from Qt thread + public synchronized boolean connectServer() + { + if (mGattServer != null) + return true; + + BluetoothManager manager = (BluetoothManager) qtContext.getSystemService(Context.BLUETOOTH_SERVICE); + if (manager == null) { + Log.w(TAG, "Bluetooth service not available."); + return false; + } + + mGattServer = manager.openGattServer(qtContext, mGattServerListener); + + return (mGattServer != null); + } + + // This function is called from Qt thread + public synchronized void disconnectServer() + { + if (mGattServer == null) + return; + + clearPendingPreparedWrites(null); + mPendingServiceAdditions.clear(); + mGattServer.close(); + mGattServer = null; + + mRemoteName = mRemoteAddress = ""; + leConnectionStateChange(qtObject, 0 /*NoError*/, + 0 /*QLowEnergyController::UnconnectedState*/); + } + + // This function is called from Qt thread + public boolean startAdvertising(AdvertiseData advertiseData, + AdvertiseData scanResponse, + AdvertiseSettings settings) + { + // Check that the bluetooth is on + if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) { + Log.w(TAG, "StartAdvertising: Bluetooth not available or offline"); + return false; + } + + // According to Android doc this check should always precede the advertiser creation + if (mLeAdvertiser == null && mBluetoothAdapter.isMultipleAdvertisementSupported()) + mLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); + + if (mLeAdvertiser == null) { + Log.w(TAG, "StartAdvertising: LE advertisement not supported"); + return false; + } + + if (!connectServer()) { + Log.w(TAG, "Server::startAdvertising: Cannot open GATT server"); + return false; + } + + Log.w(TAG, "Starting to advertise."); + mLeAdvertiser.startAdvertising(settings, advertiseData, scanResponse, mAdvertiseListener); + + return true; + } + + // This function is called from Qt thread + public void stopAdvertising() + { + if (mLeAdvertiser == null) + return; + + mLeAdvertiser.stopAdvertising(mAdvertiseListener); + Log.w(TAG, "Advertisement stopped."); + } + + // This function is called from Qt thread + public synchronized void addService(BluetoothGattService service) + { + if (!connectServer()) { + Log.w(TAG, "Server::addService: Cannot open GATT server"); + return; + } + + // When we add a service, we must wait for onServiceAdded callback before adding the + // next one. If the pending service queue is empty it means that there are no ongoing + // service additions => add the service to the server. If there are services in the + // queue it means there is an initiated addition ongoing, and we only add to the queue. + if (mPendingServiceAdditions.isEmpty()) { + if (mGattServer.addService(service)) + mPendingServiceAdditions.add(service); + else + Log.w(TAG, "Adding service " + service.getUuid().toString() + " failed."); + } else { + mPendingServiceAdditions.add(service); + } + } + + /* + Check the client characteristics configuration for the given characteristic + and sends notifications or indications as per required. + + This function is called from Qt and Java threads and calls must be protected + */ + private void sendNotificationsOrIndications(BluetoothGattCharacteristic characteristic) + { + final ListIterator<BluetoothDevice> iter = + clientCharacteristicManager.getToBeUpdatedDevices(characteristic).listIterator(); + + // TODO This quick loop over multiple devices should be synced with onNotificationSent(). + // The next notifyCharacteristicChanged() call must wait until onNotificationSent() + // was received. At this becomes an issue when the server accepts multiple remote + // devices at the same time. + while (iter.hasNext()) { + final BluetoothDevice device = iter.next(); + final byte[] clientCharacteristicConfig = + clientCharacteristicManager.valueFor(characteristic, device); + if (clientCharacteristicConfig != null) { + if (Arrays.equals(clientCharacteristicConfig, + BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) { + if (Build.VERSION.SDK_INT >= 33) { + mGattServer.notifyCharacteristicChanged(device, characteristic, false, + ((QtBluetoothGattCharacteristic)characteristic).getLocalValue()); + } else { + mGattServer.notifyCharacteristicChanged(device, characteristic, false); + } + } else if (Arrays.equals(clientCharacteristicConfig, + BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)) { + if (Build.VERSION.SDK_INT >= 33) { + mGattServer.notifyCharacteristicChanged(device, characteristic, true, + ((QtBluetoothGattCharacteristic)characteristic).getLocalValue()); + } else { + mGattServer.notifyCharacteristicChanged(device, characteristic, true); + } + } + } + } + } + + /* + Updates the local database value for the given characteristic with \a charUuid and + \a newValue. If notifications for this task are enabled an appropriate notification will + be send to the remote client. + + This function is called from the Qt thread. + */ + public boolean writeCharacteristic(BluetoothGattService service, UUID charUuid, byte[] newValue) + { + BluetoothGattCharacteristic foundChar = null; + List<BluetoothGattCharacteristic> charList = service.getCharacteristics(); + for (BluetoothGattCharacteristic iter: charList) { + if (iter.getUuid().equals(charUuid) && foundChar == null) { + foundChar = iter; + // don't break here since we want to check next condition below on next iteration + } else if (iter.getUuid().equals(charUuid)) { + Log.w(TAG, "Found second char with same UUID. Wrong char may have been selected."); + break; + } + } + + if (foundChar == null) { + Log.w(TAG, "writeCharacteristic: update for unknown characteristic failed"); + return false; + } + + // User may have set minimum and/or maximum characteristic value size. Enforce + // them here. If the user has not defined these limits, the default values 0..INT_MAX + // do not limit anything. + final int minValueLength = ((QtBluetoothGattCharacteristic)foundChar).minValueLength; + final int maxValueLength = ((QtBluetoothGattCharacteristic)foundChar).maxValueLength; + if (newValue.length < minValueLength || newValue.length > maxValueLength) { + Log.w(TAG, "writeCharacteristic: invalid value length: " + + newValue.length + ", min: " + minValueLength + ", max: " + maxValueLength); + return false; + } + + synchronized (this) // a value update might be in progress + { + ((QtBluetoothGattCharacteristic)foundChar).setLocalValue(newValue); + // Value is updated even if server is not connected, but notifying is not possible + if (mGattServer != null) + sendNotificationsOrIndications(foundChar); + } + + return true; + } + + /* + Updates the local database value for the given \a descUuid to \a newValue. + + This function is called from the Qt thread. + */ + public boolean writeDescriptor(BluetoothGattService service, UUID charUuid, UUID descUuid, + byte[] newValue) + { + BluetoothGattDescriptor foundDesc = null; + BluetoothGattCharacteristic foundChar = null; + final List<BluetoothGattCharacteristic> charList = service.getCharacteristics(); + for (BluetoothGattCharacteristic iter: charList) { + if (!iter.getUuid().equals(charUuid)) + continue; + + if (foundChar == null) { + foundChar = iter; + } else { + Log.w(TAG, "Found second char with same UUID. Wrong char may have been selected."); + break; + } + } + + if (foundChar != null) + foundDesc = foundChar.getDescriptor(descUuid); + + if (foundChar == null || foundDesc == null) { + Log.w(TAG, "writeDescriptor: update for unknown char or desc failed (" + foundChar + ")"); + return false; + } + + // we even write CLIENT_CHARACTERISTIC_CONFIGURATION_UUID this way as we choose + // to interpret the server's call as a change of the default value. + synchronized (this) // a value update might be in progress + { + ((QtBluetoothGattDescriptor)foundDesc).setLocalValue(newValue); + } + + return true; + } + + /* + * Call back handler for Advertisement requests. + */ + private AdvertiseCallback mAdvertiseListener = new AdvertiseCallback() + { + @Override + public void onStartSuccess(AdvertiseSettings settingsInEffect) { + super.onStartSuccess(settingsInEffect); + } + + @Override + public void onStartFailure(int errorCode) { + Log.e(TAG, "Advertising failure: " + errorCode); + super.onStartFailure(errorCode); + + // changing errorCode here implies changes to errorCode handling on Qt side + int qtErrorCode = 0; + switch (errorCode) { + case AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED: + return; // ignore -> noop + case AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE: + Log.e(TAG, "Please reduce size of advertising data."); + qtErrorCode = 1; + break; + case AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED: + qtErrorCode = 2; + break; + default: // default maps to internal error + case AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR: + qtErrorCode = 3; + break; + case AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS: + qtErrorCode = 4; + break; + } + + if (qtErrorCode > 0) + leServerAdvertisementError(qtObject, qtErrorCode); + } + }; + + public native void leConnectionStateChange(long qtObject, int errorCode, int newState); + public native void leMtuChanged(long qtObject, int mtu); + public native void leServerAdvertisementError(long qtObject, int status); + public native void leServerCharacteristicChanged(long qtObject, + BluetoothGattCharacteristic characteristic, + byte[] newValue); + public native void leServerDescriptorWritten(long qtObject, + BluetoothGattDescriptor descriptor, + byte[] newValue); +} |