/* * 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); } } }