/* * Copyright (C) 2013 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 android.bluetooth; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresNoPermission; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.bluetooth.annotations.RequiresBluetoothConnectPermission; import android.bluetooth.annotations.RequiresLegacyBluetoothPermission; import android.content.AttributionSource; import android.os.IBinder; import android.os.ParcelUuid; import android.os.RemoteException; import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.UUID; /** * Public API for the Bluetooth GATT Profile server role. * *
This class provides Bluetooth GATT server role functionality, allowing applications to create * Bluetooth Smart services and characteristics. * *
BluetoothGattServer is a proxy object for controlling the Bluetooth Service via IPC. Use
* {@link BluetoothManager#openGattServer} to get an instance of this class.
*/
public final class BluetoothGattServer implements BluetoothProfile {
private static final String TAG = "BluetoothGattServer";
private static final boolean DBG = true;
private static final boolean VDBG = false;
private final IBluetoothGatt mService;
private final BluetoothAdapter mAdapter;
private final AttributionSource mAttributionSource;
private BluetoothGattServerCallback mCallback;
private final Object mServerIfLock = new Object();
private int mServerIf;
private int mTransport;
private BluetoothGattService mPendingService;
private List Application should call this method as early as possible after it is done with this GATT
* server.
*/
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public void close() {
if (DBG) Log.d(TAG, "close()");
unregisterCallback();
}
/**
* Register an application callback to start using GattServer.
*
* This is an asynchronous call. The callback is used to notify success or failure if the
* function returns true.
*
* @param callback GATT callback handler that will receive asynchronous callbacks.
* @return true, the callback will be called to notify success or failure, false on immediate
* error
*/
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
/*package*/ boolean registerCallback(BluetoothGattServerCallback callback) {
return registerCallback(callback, false);
}
/**
* Register an application callback to start using GattServer.
*
* This is an asynchronous call. The callback is used to notify success or failure if the
* function returns true.
*
* @param callback GATT callback handler that will receive asynchronous callbacks.
* @param eattSupport indicates if server can use eatt
* @return true, the callback will be called to notify success or failure, false on immediate
* error
* @hide
*/
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
@SuppressWarnings("WaitNotInLoop") // TODO(b/314811467)
/*package*/ boolean registerCallback(
BluetoothGattServerCallback callback, boolean eattSupport) {
if (DBG) Log.d(TAG, "registerCallback()");
if (mService == null) {
Log.e(TAG, "GATT service not available");
return false;
}
UUID uuid = UUID.randomUUID();
if (DBG) Log.d(TAG, "registerCallback() - UUID=" + uuid);
synchronized (mServerIfLock) {
if (mCallback != null) {
Log.e(TAG, "App can register callback only once");
return false;
}
mCallback = callback;
try {
mService.registerServer(
new ParcelUuid(uuid),
mBluetoothGattServerCallback,
eattSupport,
mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
mCallback = null;
return false;
}
try {
mServerIfLock.wait(CALLBACK_REG_TIMEOUT);
} catch (InterruptedException e) {
Log.e(TAG, "" + e);
mCallback = null;
}
if (mServerIf == 0) {
mCallback = null;
return false;
} else {
return true;
}
}
}
/** Unregister the current application and callbacks. */
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
private void unregisterCallback() {
if (DBG) Log.d(TAG, "unregisterCallback() - mServerIf=" + mServerIf);
if (mService == null || mServerIf == 0) return;
try {
mCallback = null;
mService.unregisterServer(mServerIf, mAttributionSource);
mServerIf = 0;
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
/**
* Returns a service by UUID, instance and type.
*
* @hide
*/
/*package*/ BluetoothGattService getService(UUID uuid, int instanceId, int type) {
for (BluetoothGattService svc : mServices) {
if (svc.getType() == type
&& svc.getInstanceId() == instanceId
&& svc.getUuid().equals(uuid)) {
return svc;
}
}
return null;
}
/**
* Initiate a connection to a Bluetooth GATT capable device.
*
* The connection may not be established right away, but will be completed when the remote
* device is available. A {@link BluetoothGattServerCallback#onConnectionStateChange} callback
* will be invoked when the connection state changes as a result of this function.
*
* The autoConnect parameter determines whether to actively connect to the remote device, or
* rather passively scan and finalize the connection when the remote device is in
* range/available. Generally, the first ever connection to a device should be direct
* (autoConnect set to false) and subsequent connections to known devices should be invoked with
* the autoConnect parameter set to true.
*
* @param autoConnect Whether to directly connect to the remote device (false) or to
* automatically connect as soon as the remote device becomes available (true).
* @return true, if the connection attempt was initiated successfully
*/
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public boolean connect(BluetoothDevice device, boolean autoConnect) {
if (DBG) {
Log.d(TAG, "connect() - device: " + device + ", auto: " + autoConnect);
}
if (mService == null || mServerIf == 0) return false;
try {
// autoConnect is inverse of "isDirect"
mService.serverConnect(
mServerIf,
device.getAddress(),
device.getAddressType(),
!autoConnect,
mTransport,
mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
}
return true;
}
/**
* Disconnects an established connection, or cancels a connection attempt currently in progress.
*
* @param device Remote device
*/
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public void cancelConnection(BluetoothDevice device) {
if (DBG) Log.d(TAG, "cancelConnection() - device: " + device);
if (mService == null || mServerIf == 0) return;
try {
mService.serverDisconnect(mServerIf, device.getAddress(), mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
/**
* Set the preferred connection PHY for this app. Please note that this is just a
* recommendation, whether the PHY change will happen depends on other applications preferences,
* local and remote controller capabilities. Controller can override these settings.
*
* {@link BluetoothGattServerCallback#onPhyUpdate} will be triggered as a result of this
* call, even if no PHY change happens. It is also triggered when remote device updates the PHY.
*
* @param device The remote device to send this response to
* @param txPhy preferred transmitter PHY. Bitwise OR of any of {@link
* BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
* BluetoothDevice#PHY_LE_CODED_MASK}.
* @param rxPhy preferred receiver PHY. Bitwise OR of any of {@link
* BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
* BluetoothDevice#PHY_LE_CODED_MASK}.
* @param phyOptions preferred coding to use when transmitting on the LE Coded PHY. Can be one
* of {@link BluetoothDevice#PHY_OPTION_NO_PREFERRED}, {@link BluetoothDevice#PHY_OPTION_S2}
* or {@link BluetoothDevice#PHY_OPTION_S8}
*/
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public void setPreferredPhy(BluetoothDevice device, int txPhy, int rxPhy, int phyOptions) {
try {
mService.serverSetPreferredPhy(
mServerIf, device.getAddress(), txPhy, rxPhy, phyOptions, mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
/**
* Read the current transmitter PHY and receiver PHY of the connection. The values are returned
* in {@link BluetoothGattServerCallback#onPhyRead}
*
* @param device The remote device to send this response to
*/
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public void readPhy(BluetoothDevice device) {
try {
mService.serverReadPhy(mServerIf, device.getAddress(), mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
/**
* Send a response to a read or write request to a remote device.
*
* This function must be invoked in when a remote read/write request is received by one of
* these callback methods:
*
* A notification or indication is sent to the remote device to signal that the
* characteristic has been updated. This function should be invoked for every client that
* requests notifications/indications by writing to the "Client Configuration" descriptor for
* the given characteristic.
*
* @param device The remote device to receive the notification/indication
* @param characteristic The local characteristic that has been updated
* @param confirm true to request confirmation from the client (indication), false to send a
* notification
* @return true, if the notification has been triggered successfully
* @throws IllegalArgumentException if the characteristic value or service is null
* @deprecated Use {@link BluetoothGattServer#notifyCharacteristicChanged(BluetoothDevice,
* BluetoothGattCharacteristic, boolean, byte[])} as this is not memory safe.
*/
@Deprecated
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public boolean notifyCharacteristicChanged(
BluetoothDevice device, BluetoothGattCharacteristic characteristic, boolean confirm) {
return notifyCharacteristicChanged(
device, characteristic, confirm, characteristic.getValue())
== BluetoothStatusCodes.SUCCESS;
}
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
value = {
BluetoothStatusCodes.SUCCESS,
BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED,
BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
BluetoothStatusCodes.ERROR_UNKNOWN
})
public @interface NotifyCharacteristicReturnValues {}
/**
* Send a notification or indication that a local characteristic has been updated.
*
* A notification or indication is sent to the remote device to signal that the
* characteristic has been updated. This function should be invoked for every client that
* requests notifications/indications by writing to the "Client Configuration" descriptor for
* the given characteristic.
*
* @param device the remote device to receive the notification/indication
* @param characteristic the local characteristic that has been updated
* @param confirm {@code true} to request confirmation from the client (indication) or {@code
* false} to send a notification
* @param value the characteristic value
* @return whether the notification has been triggered successfully
* @throws IllegalArgumentException if the characteristic value or service is null
*/
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
@NotifyCharacteristicReturnValues
public int notifyCharacteristicChanged(
@NonNull BluetoothDevice device,
@NonNull BluetoothGattCharacteristic characteristic,
boolean confirm,
@NonNull byte[] value) {
if (VDBG) Log.d(TAG, "notifyCharacteristicChanged() - device: " + device);
if (mService == null || mServerIf == 0) {
return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
}
if (characteristic == null) {
throw new IllegalArgumentException("characteristic must not be null");
}
if (device == null) {
throw new IllegalArgumentException("device must not be null");
}
if (value.length > GATT_MAX_ATTR_LEN) {
throw new IllegalArgumentException(
"notification should not be longer than max length of an attribute value");
}
BluetoothGattService service = characteristic.getService();
if (service == null) {
throw new IllegalArgumentException("Characteristic must have a non-null service");
}
if (value == null) {
throw new IllegalArgumentException("Characteristic value must not be null");
}
try {
return mService.sendNotification(
mServerIf,
device.getAddress(),
characteristic.getInstanceId(),
confirm,
value,
mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
throw e.rethrowAsRuntimeException();
}
}
/**
* Add a service to the list of services to be hosted.
*
* Once a service has been added to the list, the service and its included characteristics
* will be provided by the local device.
*
* If the local device has already exposed services when this function is called, a service
* update notification will be sent to all clients.
*
* The {@link BluetoothGattServerCallback#onServiceAdded} callback will indicate whether this
* service has been added successfully. Do not add another service before this callback.
*
* @param service Service to be added to the list of services provided by this device.
* @return true, if the request to add service has been initiated
*/
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public boolean addService(BluetoothGattService service) {
if (DBG) Log.d(TAG, "addService() - service: " + service.getUuid());
if (mService == null || mServerIf == 0) return false;
mPendingService = service;
try {
mService.addService(mServerIf, service, mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
}
return true;
}
/**
* Removes a service from the list of services to be provided.
*
* @param service Service to be removed.
* @return true, if the service has been removed
*/
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public boolean removeService(BluetoothGattService service) {
if (DBG) Log.d(TAG, "removeService() - service: " + service.getUuid());
if (mService == null || mServerIf == 0) return false;
BluetoothGattService intService =
getService(service.getUuid(), service.getInstanceId(), service.getType());
if (intService == null) return false;
try {
mService.removeService(mServerIf, service.getInstanceId(), mAttributionSource);
mServices.remove(intService);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
}
return true;
}
/** Remove all services from the list of provided services. */
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public void clearServices() {
if (DBG) Log.d(TAG, "clearServices()");
if (mService == null || mServerIf == 0) return;
try {
mService.clearServices(mServerIf, mAttributionSource);
mServices.clear();
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
/**
* Returns a list of GATT services offered by this device.
*
* An application must call {@link #addService} to add a service to the list of services
* offered by this device.
*
* @return List of services. Returns an empty list if no services have been added yet.
*/
@RequiresLegacyBluetoothPermission
@RequiresNoPermission
public List If multiple instances of the same service (as identified by UUID) exist, the first
* instance of the service is returned.
*
* @param uuid UUID of the requested service
* @return BluetoothGattService if supported, or null if the requested service is not offered by
* this device.
*/
@RequiresLegacyBluetoothPermission
@RequiresNoPermission
public BluetoothGattService getService(UUID uuid) {
for (BluetoothGattService service : mServices) {
if (service.getUuid().equals(uuid)) {
return service;
}
}
return null;
}
/**
* Not supported - please use {@link BluetoothManager#getConnectedDevices(int)} with {@link
* BluetoothProfile#GATT} as argument
*
* @throws UnsupportedOperationException on every call
*/
@Override
@RequiresNoPermission
public int getConnectionState(BluetoothDevice device) {
throw new UnsupportedOperationException("Use BluetoothManager#getConnectionState instead.");
}
/**
* Not supported - please use {@link BluetoothManager#getConnectedDevices(int)} with {@link
* BluetoothProfile#GATT} as argument
*
* @throws UnsupportedOperationException on every call
*/
@Override
@RequiresNoPermission
public List
*
*
* @param device The remote device to send this response to
* @param requestId The ID of the request that was received with the callback
* @param status The status of the request to be sent to the remote devices
* @param offset Value offset for partial read/write response
* @param value The value of the attribute that was read/written (optional)
*/
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public boolean sendResponse(
BluetoothDevice device, int requestId, int status, int offset, byte[] value) {
if (VDBG) Log.d(TAG, "sendResponse() - device: " + device);
if (mService == null || mServerIf == 0) return false;
try {
mService.sendResponse(
mServerIf,
device.getAddress(),
requestId,
status,
offset,
value,
mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
}
return true;
}
/**
* Send a notification or indication that a local characteristic has been updated.
*
*