/*
* Copyright 2015 Google 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 io.github.webbluetoothcg.bletestperipheral;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;
import java.util.Arrays;
import java.util.HashSet;
import java.util.UUID;
import io.github.webbluetoothcg.bletestperipheral.ServiceFragment.ServiceFragmentDelegate;
public class Peripheral extends Activity implements ServiceFragmentDelegate {
private static final int REQUEST_ENABLE_BT = 1;
private static final String TAG = Peripheral.class.getCanonicalName();
private static final String CURRENT_FRAGMENT_TAG = "CURRENT_FRAGMENT";
private static final UUID CHARACTERISTIC_USER_DESCRIPTION_UUID = UUID
.fromString("00002901-0000-1000-8000-00805f9b34fb");
private static final UUID CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = UUID
.fromString("00002902-0000-1000-8000-00805f9b34fb");
private TextView mAdvStatus;
private TextView mConnectionStatus;
private ServiceFragment mCurrentServiceFragment;
private BluetoothGattService mBluetoothGattService;
private HashSet<BluetoothDevice> mBluetoothDevices;
private BluetoothManager mBluetoothManager;
private BluetoothAdapter mBluetoothAdapter;
private AdvertiseData mAdvData;
private AdvertiseData mAdvScanResponse;
private AdvertiseSettings mAdvSettings;
private BluetoothLeAdvertiser mAdvertiser;
private final AdvertiseCallback mAdvCallback = new AdvertiseCallback() {
@Override
public void onStartFailure(int errorCode) {
super.onStartFailure(errorCode);
Log.e(TAG, "Not broadcasting: " + errorCode);
int statusText;
switch (errorCode) {
case ADVERTISE_FAILED_ALREADY_STARTED:
statusText = R.string.status_advertising;
Log.w(TAG, "App was already advertising");
break;
case ADVERTISE_FAILED_DATA_TOO_LARGE:
statusText = R.string.status_advDataTooLarge;
break;
case ADVERTISE_FAILED_FEATURE_UNSUPPORTED:
statusText = R.string.status_advFeatureUnsupported;
break;
case ADVERTISE_FAILED_INTERNAL_ERROR:
statusText = R.string.status_advInternalError;
break;
case ADVERTISE_FAILED_TOO_MANY_ADVERTISERS:
statusText = R.string.status_advTooManyAdvertisers;
break;
default:
statusText = R.string.status_notAdvertising;
Log.wtf(TAG, "Unhandled error: " + errorCode);
}
mAdvStatus.setText(statusText);
}
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
super.onStartSuccess(settingsInEffect);
Log.v(TAG, "Broadcasting");
mAdvStatus.setText(R.string.status_advertising);
}
};
private BluetoothGattServer mGattServer;
private final BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
@Override
public void onConnectionStateChange(BluetoothDevice device, final int status, int newState) {
super.onConnectionStateChange(device, status, newState);
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothGatt.STATE_CONNECTED) {
mBluetoothDevices.add(device);
updateConnectedDevicesStatus();
Log.v(TAG, "Connected to device: " + device.getAddress());
} else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
mBluetoothDevices.remove(device);
updateConnectedDevicesStatus();
Log.v(TAG, "Disconnected from device");
}
} else {
mBluetoothDevices.remove(device);
updateConnectedDevicesStatus();
// There are too many gatt errors (some of them not even in the documentation) so we just
// show the error to the user.
final String errorMessage = getString(R.string.status_errorWhenConnecting) + ": " + status;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(Peripheral.this, errorMessage, Toast.LENGTH_LONG).show();
}
});
Log.e(TAG, "Error when connecting: " + status);
}
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattCharacteristic characteristic) {
super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
Log.d(TAG, "Device tried to read characteristic: " + characteristic.getUuid());
Log.d(TAG, "Value: " + Arrays.toString(characteristic.getValue()));
if (offset != 0) {
mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_INVALID_OFFSET, offset,
/* value (optional) */ null);
return;
}
mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
offset, characteristic.getValue());
}
@Override
public void onNotificationSent(BluetoothDevice device, int status) {
super.onNotificationSent(device, status);
Log.v(TAG, "Notification sent. Status: " + status);
}
@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);
Log.v(TAG, "Characteristic Write request: " + Arrays.toString(value));
int status = mCurrentServiceFragment.writeCharacteristic(characteristic, offset, value);
if (responseNeeded) {
mGattServer.sendResponse(device, requestId, status,
/* No need to respond with an offset */ 0,
/* No need to respond with a value */ null);
}
}
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId,
int offset, BluetoothGattDescriptor descriptor) {
super.onDescriptorReadRequest(device, requestId, offset, descriptor);
Log.d(TAG, "Device tried to read descriptor: " + descriptor.getUuid());
Log.d(TAG, "Value: " + Arrays.toString(descriptor.getValue()));
if (offset != 0) {
mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_INVALID_OFFSET, offset,
/* value (optional) */ null);
return;
}
mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
descriptor.getValue());
}
@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);
Log.v(TAG, "Descriptor Write Request " + descriptor.getUuid() + " " + Arrays.toString(value));
int status = BluetoothGatt.GATT_SUCCESS;
if (descriptor.getUuid() == CLIENT_CHARACTERISTIC_CONFIGURATION_UUID) {
BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
boolean supportsNotifications = (characteristic.getProperties() &
BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0;
boolean supportsIndications = (characteristic.getProperties() &
BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0;
if (!(supportsNotifications || supportsIndications)) {
status = BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED;
} else if (value.length != 2) {
status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH;
} else if (Arrays.equals(value, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) {
status = BluetoothGatt.GATT_SUCCESS;
mCurrentServiceFragment.notificationsDisabled(characteristic);
descriptor.setValue(value);
} else if (supportsNotifications &&
Arrays.equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
status = BluetoothGatt.GATT_SUCCESS;
mCurrentServiceFragment.notificationsEnabled(characteristic, false /* indicate */);
descriptor.setValue(value);
} else if (supportsIndications &&
Arrays.equals(value, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)) {
status = BluetoothGatt.GATT_SUCCESS;
mCurrentServiceFragment.notificationsEnabled(characteristic, true /* indicate */);
descriptor.setValue(value);
} else {
status = BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED;
}
} else {
status = BluetoothGatt.GATT_SUCCESS;
descriptor.setValue(value);
}
if (responseNeeded) {
mGattServer.sendResponse(device, requestId, status,
/* No need to respond with offset */ 0,
/* No need to respond with a value */ null);
}
}
};
/////////////////////////////////
////// Lifecycle Callbacks //////
/////////////////////////////////
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_peripherals);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
mAdvStatus = (TextView) findViewById(R.id.textView_advertisingStatus);
mConnectionStatus = (TextView) findViewById(R.id.textView_connectionStatus);
mBluetoothDevices = new HashSet<>();
mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = mBluetoothManager.getAdapter();
// If we are not being restored from a previous state then create and add the fragment.
if (savedInstanceState == null) {
int peripheralIndex = getIntent().getIntExtra(Peripherals.EXTRA_PERIPHERAL_INDEX,
/* default */ -1);
if (peripheralIndex == 0) {
mCurrentServiceFragment = new BatteryServiceFragment();
} else if (peripheralIndex == 1) {
mCurrentServiceFragment = new HeartRateServiceFragment();
} else if (peripheralIndex == 2) {
mCurrentServiceFragment = new HealthThermometerServiceFragment();
} else {
Log.wtf(TAG, "Service doesn't exist");
}
getFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, mCurrentServiceFragment, CURRENT_FRAGMENT_TAG)
.commit();
} else {
mCurrentServiceFragment = (ServiceFragment) getFragmentManager()
.findFragmentByTag(CURRENT_FRAGMENT_TAG);
}
mBluetoothGattService = mCurrentServiceFragment.getBluetoothGattService();
mAdvSettings = new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
.setConnectable(true)
.build();
mAdvData = new AdvertiseData.Builder()
.setIncludeTxPowerLevel(true)
.addServiceUuid(mCurrentServiceFragment.getServiceUUID())
.build();
mAdvScanResponse = new AdvertiseData.Builder()
.setIncludeDeviceName(true)
.build();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_peripheral, menu);
return true /* show menu */;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == RESULT_OK) {
if (!mBluetoothAdapter.isMultipleAdvertisementSupported()) {
Toast.makeText(this, R.string.bluetoothAdvertisingNotSupported, Toast.LENGTH_LONG).show();
Log.e(TAG, "Advertising not supported");
}
onStart();
} else {
//TODO(g-ortuno): UX for asking the user to activate bt
Toast.makeText(this, R.string.bluetoothNotEnabled, Toast.LENGTH_LONG).show();
Log.e(TAG, "Bluetooth not enabled");
finish();
}
}
}
@Override
protected void onStart() {
super.onStart();
resetStatusViews();
// If the user disabled Bluetooth when the app was in the background,
// openGattServer() will return null.
mGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
if (mGattServer == null) {
ensureBleFeaturesAvailable();
return;
}
// Add a service for a total of three services (Generic Attribute and Generic Access
// are present by default).
mGattServer.addService(mBluetoothGattService);
if (mBluetoothAdapter.isMultipleAdvertisementSupported()) {
mAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
mAdvertiser.startAdvertising(mAdvSettings, mAdvData, mAdvScanResponse, mAdvCallback);
} else {
mAdvStatus.setText(R.string.status_noLeAdv);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_disconnect_devices) {
disconnectFromDevices();
return true /* event_consumed */;
}
return false /* event_consumed */;
}
@Override
protected void onStop() {
super.onStop();
if (mGattServer != null) {
mGattServer.close();
}
if (mBluetoothAdapter.isEnabled() && mAdvertiser != null) {
// If stopAdvertising() gets called before close() a null
// pointer exception is raised.
mAdvertiser.stopAdvertising(mAdvCallback);
}
resetStatusViews();
}
@Override
public void sendNotificationToDevices(BluetoothGattCharacteristic characteristic) {
boolean indicate = (characteristic.getProperties()
& BluetoothGattCharacteristic.PROPERTY_INDICATE)
== BluetoothGattCharacteristic.PROPERTY_INDICATE;
for (BluetoothDevice device : mBluetoothDevices) {
// true for indication (acknowledge) and false for notification (unacknowledge).
mGattServer.notifyCharacteristicChanged(device, characteristic, indicate);
}
}
private void resetStatusViews() {
mAdvStatus.setText(R.string.status_notAdvertising);
updateConnectedDevicesStatus();
}
private void updateConnectedDevicesStatus() {
final String message = getString(R.string.status_devicesConnected) + " "
+ mBluetoothManager.getConnectedDevices(BluetoothGattServer.GATT).size();
runOnUiThread(new Runnable() {
@Override
public void run() {
mConnectionStatus.setText(message);
}
});
}
///////////////////////
////// Bluetooth //////
///////////////////////
public static BluetoothGattDescriptor getClientCharacteristicConfigurationDescriptor() {
BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(
CLIENT_CHARACTERISTIC_CONFIGURATION_UUID,
(BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE));
descriptor.setValue(new byte[]{0, 0});
return descriptor;
}
public static BluetoothGattDescriptor getCharacteristicUserDescriptionDescriptor(String defaultValue) {
BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(
CHARACTERISTIC_USER_DESCRIPTION_UUID,
(BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE));
try {
descriptor.setValue(defaultValue.getBytes("UTF-8"));
} finally {
return descriptor;
}
}
private void ensureBleFeaturesAvailable() {
if (mBluetoothAdapter == null) {
Toast.makeText(this, R.string.bluetoothNotSupported, Toast.LENGTH_LONG).show();
Log.e(TAG, "Bluetooth not supported");
finish();
} else if (!mBluetoothAdapter.isEnabled()) {
// Make sure bluetooth is enabled.
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
}
private void disconnectFromDevices() {
Log.d(TAG, "Disconnecting devices...");
for (BluetoothDevice device : mBluetoothManager.getConnectedDevices(
BluetoothGattServer.GATT)) {
Log.d(TAG, "Devices: " + device.getAddress() + " " + device.getName());
mGattServer.cancelConnection(device);
}
}
}