diff --git a/sample_app/AndroidManifest.xml b/sample_app/AndroidManifest.xml index 4b46797..72924ee 100644 --- a/sample_app/AndroidManifest.xml +++ b/sample_app/AndroidManifest.xml @@ -33,6 +33,11 @@ android:name="uk.co.alt236.btlescan.activities.DetailsActivity" android:label="@string/app_name" > + + + \ No newline at end of file diff --git a/sample_app/res/layout/activity_gatt_services.xml b/sample_app/res/layout/activity_gatt_services.xml new file mode 100644 index 0000000..9fdc916 --- /dev/null +++ b/sample_app/res/layout/activity_gatt_services.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample_app/res/menu/details.xml b/sample_app/res/menu/details.xml new file mode 100644 index 0000000..4bf3056 --- /dev/null +++ b/sample_app/res/menu/details.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/sample_app/res/menu/gatt_services.xml b/sample_app/res/menu/gatt_services.xml new file mode 100644 index 0000000..0661f05 --- /dev/null +++ b/sample_app/res/menu/gatt_services.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/sample_app/res/values/strings.xml b/sample_app/res/values/strings.xml index ab8ac0e..7ceae33 100644 --- a/sample_app/res/values/strings.xml +++ b/sample_app/res/values/strings.xml @@ -1,15 +1,19 @@ - Bluetooth LE Scanner Settings + Bluetooth LE Scanner + Connected + Disconnected Hello world! + No data + Not supported Off On - Not supported Supported - No data + Unknown characteristic Unknown Device + Unknown service %sm @@ -22,12 +26,15 @@ Stop + Data: + Device address: Distance: - TX Power: - Minor: - Major: - UUID: - RSSI: MAC: + Major: + Minor: + RSSI: + State: + TX Power: + UUID: \ No newline at end of file diff --git a/sample_app/src/uk/co/alt236/btlescan/activities/DetailsActivity.java b/sample_app/src/uk/co/alt236/btlescan/activities/DetailsActivity.java index 3b8bacd..99dd2fd 100644 --- a/sample_app/src/uk/co/alt236/btlescan/activities/DetailsActivity.java +++ b/sample_app/src/uk/co/alt236/btlescan/activities/DetailsActivity.java @@ -11,7 +11,10 @@ import uk.co.alt236.bluetoothlelib.util.IBeaconUtils; import uk.co.alt236.btlescan.R; import uk.co.alt236.btlescan.util.Constants; import android.app.Activity; +import android.content.Intent; import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; import android.widget.TextView; import butterknife.ButterKnife; import butterknife.InjectView; @@ -24,6 +27,19 @@ public class DetailsActivity extends Activity{ private BluetoothLeDevice mDevice; + private String formatRssi(double rssi){ + return getString(R.string.formatter_db, String.valueOf(rssi)); + } + + private String formatRssi(int rssi){ + return getString(R.string.formatter_db, String.valueOf(rssi)); + } + + private String formatTime(long time){ + return android.text.format.DateFormat.format( + Constants.TIME_FORMAT, new java.util.Date(time)).toString(); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -35,17 +51,26 @@ public class DetailsActivity extends Activity{ pupulateDetails(mDevice); } - private String formatTime(long time){ - return android.text.format.DateFormat.format( - Constants.TIME_FORMAT, new java.util.Date(time)).toString(); + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.details, menu); + return true; } - private String formatRssi(int rssi){ - return getString(R.string.formatter_db, String.valueOf(rssi)); - } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_connect: - private String formatRssi(double rssi){ - return getString(R.string.formatter_db, String.valueOf(rssi)); + final Intent intent = new Intent(this, DeviceControlActivity.class); + intent.putExtra(DeviceControlActivity.EXTRAS_DEVICE_NAME, mDevice.getName()); + intent.putExtra(DeviceControlActivity.EXTRAS_DEVICE_ADDRESS, mDevice.getAddress()); + + startActivity(intent); + + break; + } + return true; } private void pupulateDetails(BluetoothLeDevice device) { @@ -116,22 +141,6 @@ public class DetailsActivity extends Activity{ append(sb, label, String.valueOf(value)); } -// private static void append(StringBuilder sb, String label, double value) { -// append(sb, label, String.valueOf(value)); -// } -// -// private static void append(StringBuilder sb, String label, int value) { -// append(sb, label, String.valueOf(value)); -// } -// -// private static void append(StringBuilder sb, String label, long value) { -// append(sb, label, String.valueOf(value)); -// } - - public static String padRight(String s, int n) { - return String.format("%1$-" + n + "s", s); - } - private static void append(StringBuilder sb, String label, String value){ if(value != null){ sb.append("\u2022" + padRight(label, 10) +":\t" + value + "\n"); @@ -139,4 +148,8 @@ public class DetailsActivity extends Activity{ sb.append(label + "\n"); } } + + public static String padRight(String s, int n) { + return String.format("%1$-" + n + "s", s); + } } diff --git a/sample_app/src/uk/co/alt236/btlescan/activities/DeviceControlActivity.java b/sample_app/src/uk/co/alt236/btlescan/activities/DeviceControlActivity.java new file mode 100644 index 0000000..b33e6c6 --- /dev/null +++ b/sample_app/src/uk/co/alt236/btlescan/activities/DeviceControlActivity.java @@ -0,0 +1,312 @@ +/* + * 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 uk.co.alt236.btlescan.activities; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import uk.co.alt236.btlescan.R; +import uk.co.alt236.btlescan.services.BluetoothLeService; +import uk.co.alt236.btlescan.util.SampleGattAttributes; +import android.app.Activity; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.ExpandableListView; +import android.widget.SimpleExpandableListAdapter; +import android.widget.TextView; + +/** + * For a given BLE device, this Activity provides the user interface to connect, display data, + * and display GATT services and characteristics supported by the device. The Activity + * communicates with {@code BluetoothLeService}, which in turn interacts with the + * Bluetooth LE API. + */ +public class DeviceControlActivity extends Activity { + private final static String TAG = DeviceControlActivity.class.getSimpleName(); + + public static final String EXTRAS_DEVICE_NAME = "DEVICE_NAME"; + public static final String EXTRAS_DEVICE_ADDRESS = "DEVICE_ADDRESS"; + + private TextView mConnectionState; + private TextView mDataField; + private String mDeviceName; + private String mDeviceAddress; + private ExpandableListView mGattServicesList; + private BluetoothLeService mBluetoothLeService; + private ArrayList> mGattCharacteristics = + new ArrayList>(); + private boolean mConnected = false; + private BluetoothGattCharacteristic mNotifyCharacteristic; + + private final String LIST_NAME = "NAME"; + private final String LIST_UUID = "UUID"; + + // Code to manage Service lifecycle. + private final ServiceConnection mServiceConnection = new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName componentName, IBinder service) { + mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService(); + if (!mBluetoothLeService.initialize()) { + Log.e(TAG, "Unable to initialize Bluetooth"); + finish(); + } + // Automatically connects to the device upon successful start-up initialization. + mBluetoothLeService.connect(mDeviceAddress); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + mBluetoothLeService = null; + } + }; + + // Handles various events fired by the Service. + // ACTION_GATT_CONNECTED: connected to a GATT server. + // ACTION_GATT_DISCONNECTED: disconnected from a GATT server. + // ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services. + // ACTION_DATA_AVAILABLE: received data from the device. This can be a result of read + // or notification operations. + private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) { + mConnected = true; + updateConnectionState(R.string.connected); + invalidateOptionsMenu(); + } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) { + mConnected = false; + updateConnectionState(R.string.disconnected); + invalidateOptionsMenu(); + clearUI(); + } else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) { + // Show all the supported services and characteristics on the user interface. + displayGattServices(mBluetoothLeService.getSupportedGattServices()); + } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) { + displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA)); + } + } + }; + + // If a given GATT characteristic is selected, check for supported features. This sample + // demonstrates 'Read' and 'Notify' features. See + // http://d.android.com/reference/android/bluetooth/BluetoothGatt.html for the complete + // list of supported characteristic features. + private final ExpandableListView.OnChildClickListener servicesListClickListner = + new ExpandableListView.OnChildClickListener() { + @Override + public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, + int childPosition, long id) { + if (mGattCharacteristics != null) { + final BluetoothGattCharacteristic characteristic = + mGattCharacteristics.get(groupPosition).get(childPosition); + final int charaProp = characteristic.getProperties(); + if ((charaProp | BluetoothGattCharacteristic.PROPERTY_READ) > 0) { + // If there is an active notification on a characteristic, clear + // it first so it doesn't update the data field on the user interface. + if (mNotifyCharacteristic != null) { + mBluetoothLeService.setCharacteristicNotification( + mNotifyCharacteristic, false); + mNotifyCharacteristic = null; + } + mBluetoothLeService.readCharacteristic(characteristic); + } + if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { + mNotifyCharacteristic = characteristic; + mBluetoothLeService.setCharacteristicNotification( + characteristic, true); + } + return true; + } + return false; + } + }; + + private void clearUI() { + mGattServicesList.setAdapter((SimpleExpandableListAdapter) null); + mDataField.setText(R.string.no_data); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_gatt_services); + + final Intent intent = getIntent(); + mDeviceName = intent.getStringExtra(EXTRAS_DEVICE_NAME); + mDeviceAddress = intent.getStringExtra(EXTRAS_DEVICE_ADDRESS); + + // Sets up UI references. + ((TextView) findViewById(R.id.device_address)).setText(mDeviceAddress); + mGattServicesList = (ExpandableListView) findViewById(R.id.gatt_services_list); + mGattServicesList.setOnChildClickListener(servicesListClickListner); + mConnectionState = (TextView) findViewById(R.id.connection_state); + mDataField = (TextView) findViewById(R.id.data_value); + + getActionBar().setTitle(mDeviceName); + getActionBar().setDisplayHomeAsUpEnabled(true); + Intent gattServiceIntent = new Intent(this, BluetoothLeService.class); + bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE); + } + + @Override + protected void onResume() { + super.onResume(); + registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter()); + if (mBluetoothLeService != null) { + final boolean result = mBluetoothLeService.connect(mDeviceAddress); + Log.d(TAG, "Connect request result=" + result); + } + } + + @Override + protected void onPause() { + super.onPause(); + unregisterReceiver(mGattUpdateReceiver); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + unbindService(mServiceConnection); + mBluetoothLeService = null; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.gatt_services, menu); + if (mConnected) { + menu.findItem(R.id.menu_connect).setVisible(false); + menu.findItem(R.id.menu_disconnect).setVisible(true); + } else { + menu.findItem(R.id.menu_connect).setVisible(true); + menu.findItem(R.id.menu_disconnect).setVisible(false); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.menu_connect: + mBluetoothLeService.connect(mDeviceAddress); + return true; + case R.id.menu_disconnect: + mBluetoothLeService.disconnect(); + return true; + case android.R.id.home: + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void updateConnectionState(final int resourceId) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mConnectionState.setText(resourceId); + } + }); + } + + private void displayData(String data) { + if (data != null) { + mDataField.setText(data); + } + } + + // Demonstrates how to iterate through the supported GATT Services/Characteristics. + // In this sample, we populate the data structure that is bound to the ExpandableListView + // on the UI. + private void displayGattServices(List gattServices) { + if (gattServices == null) return; + String uuid = null; + String unknownServiceString = getResources().getString(R.string.unknown_service); + String unknownCharaString = getResources().getString(R.string.unknown_characteristic); + ArrayList> gattServiceData = new ArrayList>(); + ArrayList>> gattCharacteristicData + = new ArrayList>>(); + mGattCharacteristics = new ArrayList>(); + + // Loops through available GATT Services. + for (BluetoothGattService gattService : gattServices) { + HashMap currentServiceData = new HashMap(); + uuid = gattService.getUuid().toString(); + currentServiceData.put( + LIST_NAME, SampleGattAttributes.lookup(uuid, unknownServiceString)); + currentServiceData.put(LIST_UUID, uuid); + gattServiceData.add(currentServiceData); + + ArrayList> gattCharacteristicGroupData = + new ArrayList>(); + List gattCharacteristics = + gattService.getCharacteristics(); + ArrayList charas = + new ArrayList(); + + // Loops through available Characteristics. + for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { + charas.add(gattCharacteristic); + HashMap currentCharaData = new HashMap(); + uuid = gattCharacteristic.getUuid().toString(); + currentCharaData.put( + LIST_NAME, SampleGattAttributes.lookup(uuid, unknownCharaString)); + currentCharaData.put(LIST_UUID, uuid); + gattCharacteristicGroupData.add(currentCharaData); + } + mGattCharacteristics.add(charas); + gattCharacteristicData.add(gattCharacteristicGroupData); + } + + SimpleExpandableListAdapter gattServiceAdapter = new SimpleExpandableListAdapter( + this, + gattServiceData, + android.R.layout.simple_expandable_list_item_2, + new String[] {LIST_NAME, LIST_UUID}, + new int[] { android.R.id.text1, android.R.id.text2 }, + gattCharacteristicData, + android.R.layout.simple_expandable_list_item_2, + new String[] {LIST_NAME, LIST_UUID}, + new int[] { android.R.id.text1, android.R.id.text2 } + ); + mGattServicesList.setAdapter(gattServiceAdapter); + } + + private static IntentFilter makeGattUpdateIntentFilter() { + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED); + intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED); + intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED); + intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE); + return intentFilter; + } +} \ No newline at end of file diff --git a/sample_app/src/uk/co/alt236/btlescan/services/BluetoothLeService.java b/sample_app/src/uk/co/alt236/btlescan/services/BluetoothLeService.java new file mode 100644 index 0000000..f1d4829 --- /dev/null +++ b/sample_app/src/uk/co/alt236/btlescan/services/BluetoothLeService.java @@ -0,0 +1,320 @@ +/* + * 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 uk.co.alt236.btlescan.services; + +import java.util.List; +import java.util.UUID; + +import uk.co.alt236.btlescan.util.SampleGattAttributes; +import android.app.Service; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.util.Log; + +/** + * Service for managing connection and data communication with a GATT server hosted on a + * given Bluetooth LE device. + */ +public class BluetoothLeService extends Service { + private final static String TAG = BluetoothLeService.class.getSimpleName(); + + private BluetoothManager mBluetoothManager; + private BluetoothAdapter mBluetoothAdapter; + private String mBluetoothDeviceAddress; + private BluetoothGatt mBluetoothGatt; + private int mConnectionState = STATE_DISCONNECTED; + + private static final int STATE_DISCONNECTED = 0; + private static final int STATE_CONNECTING = 1; + private static final int STATE_CONNECTED = 2; + + public final static String ACTION_GATT_CONNECTED = + "com.example.bluetooth.le.ACTION_GATT_CONNECTED"; + public final static String ACTION_GATT_DISCONNECTED = + "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED"; + public final static String ACTION_GATT_SERVICES_DISCOVERED = + "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED"; + public final static String ACTION_DATA_AVAILABLE = + "com.example.bluetooth.le.ACTION_DATA_AVAILABLE"; + public final static String EXTRA_DATA = + "com.example.bluetooth.le.EXTRA_DATA"; + + public final static UUID UUID_HEART_RATE_MEASUREMENT = + UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT); + + // Implements callback methods for GATT events that the app cares about. For example, + // connection change and services discovered. + private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + String intentAction; + if (newState == BluetoothProfile.STATE_CONNECTED) { + intentAction = ACTION_GATT_CONNECTED; + mConnectionState = STATE_CONNECTED; + broadcastUpdate(intentAction); + Log.i(TAG, "Connected to GATT server."); + // Attempts to discover services after successful connection. + Log.i(TAG, "Attempting to start service discovery:" + + mBluetoothGatt.discoverServices()); + + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + intentAction = ACTION_GATT_DISCONNECTED; + mConnectionState = STATE_DISCONNECTED; + Log.i(TAG, "Disconnected from GATT server."); + broadcastUpdate(intentAction); + } + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED); + } else { + Log.w(TAG, "onServicesDiscovered received: " + status); + } + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); + } + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); + } + }; + + private void broadcastUpdate(final String action) { + final Intent intent = new Intent(action); + sendBroadcast(intent); + } + + private void broadcastUpdate(final String action, + final BluetoothGattCharacteristic characteristic) { + final Intent intent = new Intent(action); + + // This is special handling for the Heart Rate Measurement profile. Data parsing is + // carried out as per profile specifications: + // http://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml + if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { + int flag = characteristic.getProperties(); + int format = -1; + if ((flag & 0x01) != 0) { + format = BluetoothGattCharacteristic.FORMAT_UINT16; + Log.d(TAG, "Heart rate format UINT16."); + } else { + format = BluetoothGattCharacteristic.FORMAT_UINT8; + Log.d(TAG, "Heart rate format UINT8."); + } + final int heartRate = characteristic.getIntValue(format, 1); + Log.d(TAG, String.format("Received heart rate: %d", heartRate)); + intent.putExtra(EXTRA_DATA, String.valueOf(heartRate)); + } else { + // For all other profiles, writes the data formatted in HEX. + final byte[] data = characteristic.getValue(); + if (data != null && data.length > 0) { + final StringBuilder stringBuilder = new StringBuilder(data.length); + for(byte byteChar : data) + stringBuilder.append(String.format("%02X ", byteChar)); + intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString()); + } + } + sendBroadcast(intent); + } + + public class LocalBinder extends Binder { + public BluetoothLeService getService() { + return BluetoothLeService.this; + } + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public boolean onUnbind(Intent intent) { + // After using a given device, you should make sure that BluetoothGatt.close() is called + // such that resources are cleaned up properly. In this particular example, close() is + // invoked when the UI is disconnected from the Service. + close(); + return super.onUnbind(intent); + } + + private final IBinder mBinder = new LocalBinder(); + + /** + * Initializes a reference to the local Bluetooth adapter. + * + * @return Return true if the initialization is successful. + */ + public boolean initialize() { + // For API level 18 and above, get a reference to BluetoothAdapter through + // BluetoothManager. + if (mBluetoothManager == null) { + mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); + if (mBluetoothManager == null) { + Log.e(TAG, "Unable to initialize BluetoothManager."); + return false; + } + } + + mBluetoothAdapter = mBluetoothManager.getAdapter(); + if (mBluetoothAdapter == null) { + Log.e(TAG, "Unable to obtain a BluetoothAdapter."); + return false; + } + + return true; + } + + /** + * Connects to the GATT server hosted on the Bluetooth LE device. + * + * @param address The device address of the destination device. + * + * @return Return true if the connection is initiated successfully. The connection result + * is reported asynchronously through the + * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)} + * callback. + */ + public boolean connect(final String address) { + if (mBluetoothAdapter == null || address == null) { + Log.w(TAG, "BluetoothAdapter not initialized or unspecified address."); + return false; + } + + // Previously connected device. Try to reconnect. + if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress) + && mBluetoothGatt != null) { + Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection."); + if (mBluetoothGatt.connect()) { + mConnectionState = STATE_CONNECTING; + return true; + } else { + return false; + } + } + + final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); + if (device == null) { + Log.w(TAG, "Device not found. Unable to connect."); + return false; + } + // We want to directly connect to the device, so we are setting the autoConnect + // parameter to false. + mBluetoothGatt = device.connectGatt(this, false, mGattCallback); + Log.d(TAG, "Trying to create a new connection."); + mBluetoothDeviceAddress = address; + mConnectionState = STATE_CONNECTING; + return true; + } + + /** + * Disconnects an existing connection or cancel a pending connection. The disconnection result + * is reported asynchronously through the + * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)} + * callback. + */ + public void disconnect() { + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + Log.w(TAG, "BluetoothAdapter not initialized"); + return; + } + mBluetoothGatt.disconnect(); + } + + /** + * After using a given BLE device, the app must call this method to ensure resources are + * released properly. + */ + public void close() { + if (mBluetoothGatt == null) { + return; + } + mBluetoothGatt.close(); + mBluetoothGatt = null; + } + + /** + * Request a read on a given {@code BluetoothGattCharacteristic}. The read result is reported + * asynchronously through the {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)} + * callback. + * + * @param characteristic The characteristic to read from. + */ + public void readCharacteristic(BluetoothGattCharacteristic characteristic) { + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + Log.w(TAG, "BluetoothAdapter not initialized"); + return; + } + mBluetoothGatt.readCharacteristic(characteristic); + } + + /** + * Enables or disables notification on a give characteristic. + * + * @param characteristic Characteristic to act on. + * @param enabled If true, enable notification. False otherwise. + */ + public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, + boolean enabled) { + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + Log.w(TAG, "BluetoothAdapter not initialized"); + return; + } + mBluetoothGatt.setCharacteristicNotification(characteristic, enabled); + + // This is specific to Heart Rate Measurement. + if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { + BluetoothGattDescriptor descriptor = characteristic.getDescriptor( + UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)); + descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + mBluetoothGatt.writeDescriptor(descriptor); + } + } + + /** + * Retrieves a list of supported GATT services on the connected device. This should be + * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully. + * + * @return A {@code List} of supported services. + */ + public List getSupportedGattServices() { + if (mBluetoothGatt == null) return null; + + return mBluetoothGatt.getServices(); + } +} \ No newline at end of file diff --git a/sample_app/src/uk/co/alt236/btlescan/util/SampleGattAttributes.java b/sample_app/src/uk/co/alt236/btlescan/util/SampleGattAttributes.java new file mode 100644 index 0000000..42919c7 --- /dev/null +++ b/sample_app/src/uk/co/alt236/btlescan/util/SampleGattAttributes.java @@ -0,0 +1,42 @@ +/* + * 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 uk.co.alt236.btlescan.util; + +import java.util.HashMap; + +/** + * This class includes a small subset of standard GATT attributes for demonstration purposes. + */ +public class SampleGattAttributes { + private static HashMap attributes = new HashMap(); + public static String HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb"; + public static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"; + + static { + // Sample Services. + attributes.put("0000180d-0000-1000-8000-00805f9b34fb", "Heart Rate Service"); + attributes.put("0000180a-0000-1000-8000-00805f9b34fb", "Device Information Service"); + // Sample Characteristics. + attributes.put(HEART_RATE_MEASUREMENT, "Heart Rate Measurement"); + attributes.put("00002a29-0000-1000-8000-00805f9b34fb", "Manufacturer Name String"); + } + + public static String lookup(String uuid, String defaultName) { + String name = attributes.get(uuid); + return name == null ? defaultName : name; + } +} \ No newline at end of file