projects / bluetooth-low-energy-android-project-zero-sample-code / bluetooth-low-energy-android-project-zero-sample-code.git / commitdiff
summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: 8b474d6)
raw | patch | inline | side by side (parent: 8b474d6)
author | Jarle Boe <j.boe@ti.com> | |
Wed, 17 Aug 2016 13:11:25 +0000 (15:11 +0200) | ||
committer | Jarle Boe <j.boe@ti.com> | |
Wed, 17 Aug 2016 13:11:25 +0000 (15:11 +0200) |
24 files changed:
diff --git a/app/build.gradle b/app/build.gradle
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion "23.0.3"
+
+ defaultConfig {
+ applicationId "com.example.ti.bleprojectzero"
+ minSdkVersion 21
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ testCompile 'junit:junit:4.12'
+ compile 'com.android.support:appcompat-v7:23.3.0'
+ compile 'com.android.support:design:23.3.0'
+}
diff --git a/app/build/outputs/apk/app-debug.apk b/app/build/outputs/apk/app-debug.apk
new file mode 100644 (file)
index 0000000..d828787
Binary files /dev/null and b/app/build/outputs/apk/app-debug.apk differ
index 0000000..d828787
Binary files /dev/null and b/app/build/outputs/apk/app-debug.apk differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.ti.bleprojectzero">
+
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+
+ <uses-feature
+ android:name="android.hardware.bluetooth_le"
+ android:required="true" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme">
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme.NoActionBar">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:windowSoftInputMode="adjustPan"
+ android:name=".SelectedDeviceActivity"
+ android:label="@string/title_activity_selected_device"
+ android:parentActivityName=".MainActivity"
+ android:theme="@style/AppTheme.NoActionBar"></activity>
+ </application>
+
+</manifest>
diff --git a/app/src/main/java/com/example/ti/bleprojectzero/MainActivity.java b/app/src/main/java/com/example/ti/bleprojectzero/MainActivity.java
--- /dev/null
@@ -0,0 +1,651 @@
+/*
+ * Filename: MainActivity.java
+ *
+ * Copyright (C) 2016 Texas Instruments Incorporated - http://www.ti.com/
+ *
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the
+ * distribution.
+ *
+ * Neither the name of Texas Instruments Incorporated nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+*/
+
+package com.example.ti.bleprojectzero;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AlertDialog;
+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.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.UUID;
+
+public class MainActivity extends AppCompatActivity {
+
+ public static Activity activity = null;
+
+ private boolean mScanning = false;
+ private BluetoothAdapter mBluetoothAdapter;
+ private BluetoothLeScanner mLEScanner;
+ private ScanSettings mScanSettings;
+ private BluetoothGatt mBluetoothGatt;
+ private ArrayList<ScanFilter> mScanFilters = new ArrayList<>();
+ private Map<BluetoothDevice, Integer> mBtDevices = new HashMap<>();
+ private TableLayout mTableDevices;
+
+ // Tag used for logging
+ private static final String TAG = "MainActivity";
+
+ // Request codes
+ private static final int MY_PERMISSIONS_REQUEST_ACCESS_COARSE = 1;
+ private final static int MY_PERMISSIONS_REQUEST_ENABLE_BT = 2;
+
+ // Intent actions
+ public final static String ACTION_GATT_CONNECTED =
+ "com.example.ti.bleprojectzero.ACTION_GATT_CONNECTED";
+ public final static String ACTION_GATT_DISCONNECTED =
+ "com.example.ti.bleprojectzero.ACTION_GATT_DISCONNECTED";
+ public final static String ACTION_GATT_SERVICES_DISCOVERED =
+ "com.example.ti.bleprojectzero.ACTION_GATT_SERVICES_DISCOVERED";
+ public final static String ACTION_DATA_AVAILABLE =
+ "com.example.ti.bleprojectzero.ACTION_DATA_AVAILABLE";
+ public final static String ACTION_WRITE_SUCCESS =
+ "com.example.ti.bleprojectzero.ACTION_WRITE_SUCCESS";
+
+ // Intent extras
+ public final static String EXTRA_DATA =
+ "com.example.ti.bleprojectzero.EXTRA_DATA";
+ public final static String EXTRA_LED0 =
+ "com.example.ti.bleprojectzero.EXTRA_LED0";
+ public final static String EXTRA_LED1 =
+ "com.example.ti.bleprojectzero.EXTRA_LED1";
+ public final static String EXTRA_BUTTON0 =
+ "com.example.ti.bleprojectzero.EXTRA_BUTTON0";
+ public final static String EXTRA_BUTTON1 =
+ "com.example.ti.bleprojectzero.EXTRA_BUTTON1";
+
+ // Queue for writing descriptors
+ private Queue<BluetoothGattDescriptor> descriptorWriteQueue = new LinkedList<>();
+ private Queue<BluetoothGattCharacteristic> characteristicQueue = new LinkedList<>();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ activity = this;
+
+ // Get UI elements
+ mTableDevices = (TableLayout) findViewById(R.id.devicesFound);
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ // Initializes Bluetooth adapter.
+ final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
+ mBluetoothAdapter = bluetoothManager.getAdapter();
+
+ // For Android M: Check if app have location permission
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_COARSE_LOCATION)) {
+ // Show explanation on why this is needed
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("This app needs location access");
+ builder.setMessage("Please grant location access so this app can discover bluetooth devices");
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ // Prompt the user once explanation has been shown
+ ActivityCompat.requestPermissions(MainActivity.this,
+ new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
+ MY_PERMISSIONS_REQUEST_ACCESS_COARSE);
+ }
+ });
+ builder.show();
+
+ } else {
+ // Prompt user for location access
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
+ MY_PERMISSIONS_REQUEST_ACCESS_COARSE);
+ }
+ }
+ }
+
+ // Configure scan filter on device name, so the scan result
+ // only displays devices with Project Zero running.
+ ScanFilter filter = new ScanFilter.Builder()
+ .setDeviceName("Project Zero")
+ .build();
+ mScanFilters.add(filter);
+
+ // Configure default scan settings
+ mScanSettings = new ScanSettings.Builder().build();
+
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
+ // Bluetooth is not available or disabled. Display
+ // a dialog requesting user permission to enable Bluetooth.
+ Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ startActivityForResult(enableBtIntent, MY_PERMISSIONS_REQUEST_ENABLE_BT);
+ } else if (!mScanning) {
+ // Start scanning
+ mLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
+ scanLeDevice(true);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled() && mScanning) {
+ // Stop scanning
+ scanLeDevice(false);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ String permissions[], int[] grantResults) {
+ // Callback have been received from a permission request
+ switch (requestCode) {
+ case MY_PERMISSIONS_REQUEST_ACCESS_COARSE: {
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "Coarse location permission granted");
+ } else {
+ // Access location was not granted. Display a warning.
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Functionality limited");
+ builder.setMessage("Since location access has not been granted, this app will not display any bluetooth scan results.");
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ }
+ });
+ builder.show();
+ }
+ return;
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == MY_PERMISSIONS_REQUEST_ENABLE_BT) {
+ if (resultCode == Activity.RESULT_CANCELED) {
+ // Bluetooth was not enabled, end activity
+ finish();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Connect to a bluetooth device. The connection result is reported asynchronously through the
+ * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
+ * callback.
+ *
+ * @param btDevice instance of device to connect to.
+ */
+ public void connectToDevice(BluetoothDevice btDevice) {
+
+ if (mBluetoothGatt == null) {
+ mBluetoothGatt = btDevice.connectGatt(this, false, mGattCallback);
+
+ // Stop scanning
+ if (mScanning) {
+ scanLeDevice(false);
+ }
+ }
+ }
+
+ /**
+ * Disconnect from a device. 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, "Bluetooth not initialized");
+ return;
+ }
+ mBluetoothGatt.disconnect();
+ }
+
+ /**
+ * Retrieve 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<BluetoothGattService> getSupportedGattServices() {
+ if (mBluetoothGatt == null){
+ return null;
+ }
+
+ return mBluetoothGatt.getServices();
+ }
+
+ /**
+ * 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, "Bluetooth not initialized");
+ return;
+ }
+
+ // Queue the characteristic to read, since several reads are done on startup
+ characteristicQueue.add(characteristic);
+
+ // If there is only 1 item in the queue, then read it. If more than 1, it is handled
+ // asynchronously in the callback
+ if((characteristicQueue.size() == 1)) {
+ mBluetoothGatt.readCharacteristic(characteristic);
+ }
+ }
+
+ /**
+ * Request a write on a given {@code BluetoothGattCharacteristic}. The write result is reported
+ * asynchronously through the
+ * {@code BluetoothGattCallback#onCharacteristicWrite(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
+ * callback.
+ *
+ * @param characteristic The characteristic to write to.
+ */
+ public void writeCharacteristic(BluetoothGattCharacteristic characteristic) {
+ if (mBluetoothAdapter == null || mBluetoothGatt == null) {
+ Log.w(TAG, "BluetoothAdapter not initialized");
+ return;
+ }
+
+ mBluetoothGatt.writeCharacteristic(characteristic);
+ }
+
+ /**
+ * Enable or disable notification on a give characteristic.
+ *
+ * @param characteristic Characteristic to act on.
+ * @param enable If true, enable notification. Otherwise, disable it.
+ */
+ public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
+ boolean enable) {
+ if (mBluetoothAdapter == null || mBluetoothGatt == null) {
+ Log.w(TAG, "Bluetooth not initialized");
+ return;
+ }
+ // Enable/disable notification
+ mBluetoothGatt.setCharacteristicNotification(characteristic, enable);
+
+ // Write descriptor for notification
+ BluetoothGattDescriptor descriptor = characteristic.getDescriptor(ProjectZeroAttributes.UUID_CHARACTERISTIC_UPDATE_NOTIFICATION_DESCRIPTOR);
+ descriptor.setValue(enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : new byte[] { 0x00, 0x00 });
+ writeGattDescriptor(descriptor);
+
+ }
+
+ /**
+ * Scan for bluetooth devices
+ */
+ private void scanLeDevice(final boolean enable) {
+
+ if(mLEScanner == null) {
+ Log.d(TAG, "Could not get LEScanner object");
+ return;
+ }
+
+ if (enable) {
+ // Clear list of scanned devices
+ mBtDevices.clear();
+ mScanning = true;
+ mLEScanner.startScan(mScanFilters, mScanSettings, mScanCallback);
+ } else {
+ mScanning = false;
+ mLEScanner.stopScan(mScanCallback);
+ }
+ }
+
+ /**
+ * Bluetooth gatt callback function
+ */
+ private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
+
+ /**
+ * Enable notifications on the button clicks
+ */
+ private void enableButtonNotifications(BluetoothGatt gatt) {
+ // Loop through the characteristics for the button service
+ for(BluetoothGattCharacteristic characteristic : gatt.getService(ProjectZeroAttributes.UUID_BUTTON_SERVICE).getCharacteristics()){
+ // Enable notification on the characteristic
+ final int charaProp = characteristic.getProperties();
+ if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
+ setCharacteristicNotification(characteristic, true);
+ }
+ }
+ }
+
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ Log.i(TAG, "onConnectionStateChange. Status: " + status);
+ String intentAction;
+ switch (newState) {
+ case BluetoothProfile.STATE_CONNECTED:
+ intentAction = ACTION_GATT_CONNECTED;
+ broadcastUpdate(intentAction);
+ Log.i("gattCallback", "STATE_CONNECTED");
+ gatt.discoverServices();
+ break;
+ case BluetoothProfile.STATE_DISCONNECTED:
+ Log.i("gattCallback", "STATE_DISCONNECTED");
+ intentAction = ACTION_GATT_DISCONNECTED;
+ broadcastUpdate(intentAction);
+ // Close connection completely after disconnect, to be able
+ // to start clean.
+ if (mBluetoothGatt != null) {
+ mBluetoothGatt.close();
+ mBluetoothGatt = null;
+ }
+ break;
+ default:
+ Log.e("gattCallback", "STATE_OTHER");
+ }
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ Log.i(TAG, "onServicesDiscovered: " + status);
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ // Broadcast that services has successfully been discovered
+ broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
+ // Enable notification on button services
+ enableButtonNotifications(gatt);
+ } else {
+ Log.w(TAG, "onServicesDiscovered received with error: " + status);
+ }
+ }
+
+ @Override
+ public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
+ int status) {
+
+ // Read action has finished, remove from queue
+ characteristicQueue.remove();
+
+ // Broadcast the results
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
+ }
+ else{
+ Log.d(TAG, "onCharacteristicRead error: " + status);
+ }
+
+ // Handle the next element from the queues
+ if(characteristicQueue.size() > 0)
+ mBluetoothGatt.readCharacteristic(characteristicQueue.element());
+ else if(descriptorWriteQueue.size() > 0)
+ mBluetoothGatt.writeDescriptor(descriptorWriteQueue.element());
+ }
+
+ @Override
+ public void onCharacteristicWrite(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ // Broadcast data written to the data service string characteristic
+ if ((UUID.fromString(ProjectZeroAttributes.STRING_CHAR)).equals(characteristic.getUuid())) {
+ broadcastUpdate(ACTION_WRITE_SUCCESS);
+ }
+ }
+ }
+
+ @Override
+ public void onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ // Broadcast the received notification
+ broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
+ }
+
+ @Override
+ public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.d(TAG, "Callback: Error writing GATT Descriptor: "+ status);
+ }
+
+ // Pop the item that we just finishing writing
+ descriptorWriteQueue.remove();
+
+ // Continue handling items if there is more in the queues
+ if(descriptorWriteQueue.size() > 0)
+ mBluetoothGatt.writeDescriptor(descriptorWriteQueue.element());
+ else if(characteristicQueue.size() > 0)
+ mBluetoothGatt.readCharacteristic(characteristicQueue.element());
+ };
+
+ };
+
+ /**
+ * Device scan callback
+ */
+ private ScanCallback mScanCallback = new ScanCallback() {
+
+ @Override
+ public void onScanResult(int callbackType, ScanResult result) {
+
+ final BluetoothDevice btDevice = result.getDevice();
+ if (btDevice == null){
+ Log.e("ScanCallback", "Could not get bluetooth device");
+ return;
+ }
+
+ // Check if device already added to list of scanned devices
+ String macAddress = btDevice.getAddress();
+ for(BluetoothDevice dev : mBtDevices.keySet())
+ {
+ // Device already added, do nothing
+ if(dev.getAddress().equals(macAddress) ){
+ return;
+ }
+ }
+
+ // Add device to list of scanned devices
+ mBtDevices.put(btDevice, result.getRssi());
+
+ // Update the device table with the new device
+ updateDeviceTable();
+ }
+ };
+
+ /**
+ * Update the table view displaying all scanned devices.
+ * This function will clean the current table view and re-add all items that has been scanned
+ */
+ private void updateDeviceTable() {
+
+ // Clean current table view
+ mTableDevices.removeAllViews();
+
+ for(final BluetoothDevice savedDevice : mBtDevices.keySet()) {
+
+ // Get RSSI of this device
+ int rssi = mBtDevices.get(savedDevice);
+
+ // Create a new row
+ final TableRow tr = new TableRow(MainActivity.this);
+ tr.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT));
+
+ // Add Text view for rssi
+ TextView tvRssi = new TextView(MainActivity.this);
+ tvRssi.setText(Integer.toString(rssi));
+ TableRow.LayoutParams params = new TableRow.LayoutParams(0);
+ params.setMargins(20,0,20,0);
+ tvRssi.setLayoutParams(params);
+
+ // Add Text view for device, displaying name and address
+ TextView tvDevice = new TextView(MainActivity.this);
+ tvDevice.setText(savedDevice.getName() + "\r\n" + savedDevice.getAddress());
+ tvDevice.setLayoutParams(new TableRow.LayoutParams(1));
+
+ // Add a connect button to the right
+ Button b = new Button(MainActivity.this);
+ b.setText(R.string.button_connect);
+ b.setGravity(Gravity.RIGHT);
+
+ // Create action when clicking the connect button
+ b.setOnClickListener(new View.OnClickListener() {
+
+ public void onClick(View v) {
+
+ // Create the activity for the selected device
+ final Intent intent = new Intent(MainActivity.this, SelectedDeviceActivity.class);
+ intent.putExtra(SelectedDeviceActivity.EXTRAS_DEVICE_NAME, savedDevice.getName());
+
+ // Connect to device
+ connectToDevice(savedDevice);
+
+ // start activity
+ startActivity(intent);
+ }
+ });
+
+ // Add items to the row
+ tr.addView(tvRssi);
+ tr.addView(tvDevice);
+ tr.addView(b);
+
+ // Add row to the table layout
+ MainActivity.this.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTableDevices.addView(tr);
+ }
+ });
+ }
+ }
+
+ /**
+ * Broadcast an update on the specified action
+ */
+ private void broadcastUpdate(final String action) {
+ final Intent intent = new Intent(action);
+ sendBroadcast(intent);
+ }
+
+ /**
+ * Broadcast an update on the specified action
+ */
+ private void broadcastUpdate(final String action,
+ final BluetoothGattCharacteristic characteristic) {
+
+ final Intent intent = new Intent(action);
+
+ if ((UUID.fromString(ProjectZeroAttributes.BUTTON0_STATE)).equals(characteristic.getUuid())) {
+ // State of button 0 has changed. Add id and value to broadcast
+ intent.putExtra(EXTRA_BUTTON0, characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8,0));
+ }
+ else if ((UUID.fromString(ProjectZeroAttributes.BUTTON1_STATE)).equals(characteristic.getUuid())) {
+ // State of button 1 has changed. Add id and value to broadcast
+ intent.putExtra(EXTRA_BUTTON1, characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8,0));
+ }
+ else if ((UUID.fromString(ProjectZeroAttributes.LED0_STATE)).equals(characteristic.getUuid())) {
+ // State of led 0 has changed. Add id and value to broadcast
+ intent.putExtra(EXTRA_LED0, characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8,0));
+ }
+ else if ((UUID.fromString(ProjectZeroAttributes.LED1_STATE)).equals(characteristic.getUuid())) {
+ // State of led 1 has changed. Add id and value to broadcast
+ intent.putExtra(EXTRA_LED1, characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8,0));
+ }
+ else {
+ // Write the data formatted as a string
+ final byte[] data = characteristic.getValue();
+ if (data != null && data.length > 0) {
+ intent.putExtra(EXTRA_DATA, new String(data));
+ }
+ }
+
+ sendBroadcast(intent);
+ }
+
+ /**
+ * Write gatt descriptor if queue is ready.
+ */
+ private void writeGattDescriptor(BluetoothGattDescriptor d){
+ // Add descriptor to the write queue
+ descriptorWriteQueue.add(d);
+ // If there is only 1 item in the queue, then write it. If more than 1, it will be handled
+ // in the onDescriptorWrite callback
+ if(descriptorWriteQueue.size() == 1){
+ mBluetoothGatt.writeDescriptor(d);
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/ti/bleprojectzero/ProjectZeroAttributes.java b/app/src/main/java/com/example/ti/bleprojectzero/ProjectZeroAttributes.java
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Filename: ProjectZeroAttributes.java
+ *
+ * This class contains GATT attributes for the Project Zero Demo
+ *
+ * Copyright (C) 2016 Texas Instruments Incorporated - http://www.ti.com/
+ *
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the
+ * distribution.
+ *
+ * Neither the name of Texas Instruments Incorporated nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+*/
+
+package com.example.ti.bleprojectzero;
+
+import java.util.HashMap;
+import java.util.UUID;
+
+/**
+ *
+ */
+public class ProjectZeroAttributes {
+
+ public static String LED_SERVICE = "F0001110-0451-4000-B000-000000000000";
+ public static String BUTTON_SERVICE = "F0001120-0451-4000-B000-000000000000";
+ public static String DATA_SERVICE = "F0001130-0451-4000-B000-000000000000";
+ public static String LED0_STATE = "F0001111-0451-4000-B000-000000000000";
+ public static String LED1_STATE = "F0001112-0451-4000-B000-000000000000";
+ public static String BUTTON0_STATE = "F0001121-0451-4000-B000-000000000000";
+ public static String BUTTON1_STATE = "F0001122-0451-4000-B000-000000000000";
+ public static String STRING_CHAR = "F0001131-0451-4000-B000-000000000000";
+ public static String STREAM_CHAR = "F0001132-0451-4000-B000-000000000000";
+
+ public final static UUID UUID_CHARACTERISTIC_UPDATE_NOTIFICATION_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); // UUID for notification descriptor
+ public final static UUID UUID_BUTTON_SERVICE = UUID.fromString(BUTTON_SERVICE);
+
+ private static HashMap<String, String> gattAttributes = new HashMap();
+ static {
+ // Services
+ gattAttributes.put(LED_SERVICE.toLowerCase(), "Led Service");
+ gattAttributes.put(BUTTON_SERVICE.toLowerCase(), "Button Service");
+ gattAttributes.put(DATA_SERVICE.toLowerCase(), "Data Service");
+ // Characteristics
+ gattAttributes.put(LED0_STATE.toLowerCase(), "Led0 State");
+ gattAttributes.put(LED1_STATE.toLowerCase(), "Led1 State");
+ gattAttributes.put(BUTTON0_STATE.toLowerCase(), "Button0 State");
+ gattAttributes.put(BUTTON1_STATE.toLowerCase(), "Button1 State");
+ gattAttributes.put(STRING_CHAR.toLowerCase(), "String char");
+ gattAttributes.put(STREAM_CHAR.toLowerCase(), "Stream char");
+ }
+
+ /**
+ * Search the map for the attribute name of a given UUID
+ *
+ * @param uuid UUID to search for
+ * @param defaultName Name to return if the UUID is not found in the map
+ *
+ * @return Name of attribute with given UUID
+ */
+ public static String lookup(String uuid, String defaultName) {
+ String name = gattAttributes.get(uuid);
+ return name == null ? defaultName : name;
+ }
+
+ /**
+ * @return Map of UUIDs and attribute names used in the Project Zero demo
+ */
+ public static HashMap<String, String> gattAttributes(){
+ return gattAttributes;
+ }
+
+}
diff --git a/app/src/main/java/com/example/ti/bleprojectzero/SelectedDeviceActivity.java b/app/src/main/java/com/example/ti/bleprojectzero/SelectedDeviceActivity.java
--- /dev/null
@@ -0,0 +1,394 @@
+/*
+ * Filename: SelectedDeviceActivity.java
+ *
+ * Copyright (C) 2016 Texas Instruments Incorporated - http://www.ti.com/
+ *
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the
+ * distribution.
+ *
+ * Neither the name of Texas Instruments Incorporated nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+*/
+package com.example.ti.bleprojectzero;
+
+import android.app.AlertDialog;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.View;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class SelectedDeviceActivity extends AppCompatActivity {
+
+ public static final String EXTRAS_DEVICE_NAME = "DEVICE_NAME";
+
+ private TextView mConnectionState;
+ private EditText mDataField;
+ private String mDeviceName;
+ private MainActivity mMainActivity;
+ private HashMap<String, BluetoothGattCharacteristic> mGattCharacteristics = new HashMap<>();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_selected_device);
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ // Add back-button to parent activity
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ // Get name of device that this intent is opened for
+ Intent intent = getIntent();
+ mDeviceName = intent.getStringExtra(EXTRAS_DEVICE_NAME);
+
+ // Set activity title to the device name
+ setTitle(mDeviceName);
+
+ // Get UI elements
+ mConnectionState = (TextView) findViewById(R.id.connection_state);
+ mDataField = (EditText) findViewById(R.id.data_value);
+
+ // Get instance of main activity
+ mMainActivity = (MainActivity) MainActivity.activity;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // Register a receiver for broadcast updates
+ registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter());
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ // Do not receive broadcast updates when paused
+ unregisterReceiver(mGattUpdateReceiver);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mMainActivity = null;
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ // Disconnect when leaving activity
+ mMainActivity.disconnect();
+ }
+
+ /**
+ * Handles various events fired by MainActivity
+ * 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.
+ * ACTION_WRITE_SUCCESS: data string was successfully written to device
+ */
+ private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ switch(action){
+ case MainActivity.ACTION_GATT_CONNECTED:
+ updateConnectionState(R.string.connected);
+ invalidateOptionsMenu();
+ break;
+ case MainActivity.ACTION_GATT_DISCONNECTED:
+ updateConnectionState(R.string.disconnected);
+ invalidateOptionsMenu();
+ clearUI();
+ break;
+ case MainActivity.ACTION_GATT_SERVICES_DISCOVERED:
+ initializeGattServiceUIElements(mMainActivity.getSupportedGattServices());
+ break;
+ case MainActivity.ACTION_DATA_AVAILABLE:
+ // If the a toggle button ID is sent as extra data, call a function to display the button state.
+ // Otherwise, display the extra data in the data service text field
+ if(intent.hasExtra(MainActivity.EXTRA_BUTTON0)) {
+ setToggleButtonState(R.id.button0_value, intent.getIntExtra(MainActivity.EXTRA_BUTTON0, 0));
+ }
+ else if(intent.hasExtra(MainActivity.EXTRA_BUTTON1)) {
+ setToggleButtonState(R.id.button1_value, intent.getIntExtra(MainActivity.EXTRA_BUTTON1, 0));
+ }
+ else if(intent.hasExtra(MainActivity.EXTRA_LED0)) {
+ setToggleButtonState(R.id.led0_value, intent.getIntExtra(MainActivity.EXTRA_LED0, 0));
+ }
+ else if(intent.hasExtra(MainActivity.EXTRA_LED1)) {
+ setToggleButtonState(R.id.led1_value, intent.getIntExtra(MainActivity.EXTRA_LED1, 0));
+ }
+ else {
+ displayData(intent.getStringExtra(MainActivity.EXTRA_DATA));
+ }
+ break;
+ case MainActivity.ACTION_WRITE_SUCCESS:
+ // Clear the data service text field when a write has finished
+ displayData("");
+ break;
+ }
+ }
+ };
+
+ /**
+ * Update the connection status field
+ */
+ private void updateConnectionState(final int resourceId) {
+ SelectedDeviceActivity.this.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mConnectionState.setText(resourceId);
+ }
+ });
+ }
+
+ /**
+ * Display a text string in the data service edit view
+ *
+ * @param text The text to display
+ */
+ private void displayData(String text) {
+ if (text != null) {
+ mDataField.setText(text);
+ }
+ }
+
+ /**
+ * Display the state of a toggle button. Toggle buttons
+ * are used for both LED and button Services.
+ *
+ * @param id The ID of button that was clicked
+ * @param value Value of the button
+ */
+ private void setToggleButtonState(int id, int value) {
+ if (id != -1) {
+ ToggleButton b = (ToggleButton) findViewById(id);
+ if(value == 1){
+ b.setChecked(true);
+ }
+ else{
+ b.setChecked(false);
+ }
+ }
+ }
+
+ /**
+ * Create an intent filter for actions broadcasted
+ * by MainActivity
+ *
+ * @return The created IntenFilter object
+ */
+ private static IntentFilter makeGattUpdateIntentFilter() {
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(MainActivity.ACTION_GATT_CONNECTED);
+ intentFilter.addAction(MainActivity.ACTION_GATT_DISCONNECTED);
+ intentFilter.addAction(MainActivity.ACTION_GATT_SERVICES_DISCOVERED);
+ intentFilter.addAction(MainActivity.ACTION_DATA_AVAILABLE);
+ intentFilter.addAction(MainActivity.ACTION_WRITE_SUCCESS);
+ return intentFilter;
+ }
+
+
+ /**
+ * Cleanup function called on a disconnect
+ */
+ private void clearUI() {
+ mDataField.setText("");
+ // Make sure LEDs and Button elemtents are unchecked
+ ToggleButton b = (ToggleButton) findViewById(R.id.led0_value);
+ b.setChecked(false);
+ b = (ToggleButton) findViewById(R.id.led1_value);
+ b.setChecked(false);
+ b = (ToggleButton) findViewById(R.id.button0_value);
+ b.setChecked(false);
+ b = (ToggleButton) findViewById(R.id.button1_value);
+ b.setChecked(false);
+ }
+
+ /**
+ * Iterate through the supported GATT Services/Characteristics,
+ * and initialize UI elements displaying them.
+ */
+ private void initializeGattServiceUIElements(List<BluetoothGattService> gattServices) {
+
+ String uuid;
+ String serviceName;
+ String unknownServiceString = getResources().getString(R.string.unknown_service);
+ String unknownCharaString = getResources().getString(R.string.unknown_characteristic);
+ mGattCharacteristics.clear();
+
+ // Create a set of all UUIDs of the discovered services and characteristics
+ Set<String> discoveredServiceUuids = new HashSet<>();
+ for (BluetoothGattService s : gattServices) {
+ discoveredServiceUuids.add(s.getUuid().toString());
+ for(BluetoothGattCharacteristic c :s.getCharacteristics()){
+ discoveredServiceUuids.add(c.getUuid().toString());
+ }
+ }
+
+ // Loop through the Project Zero service/characteristic UUIDs, and verify that all
+ // are discovered for current device
+ for(String projZeroUuid : ProjectZeroAttributes.gattAttributes().keySet()){
+ if(!discoveredServiceUuids.contains(projZeroUuid))
+ {
+ // The Project Zero attribute is not discovered for this device.
+ // Display an error and return to main screen.
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Error!");
+ builder.setMessage("The expected Project Zero attribute " +
+ ProjectZeroAttributes.lookup(projZeroUuid, "with UUID = "+ projZeroUuid) +
+ " was not discovered. Device will be disconnected.");
+ builder.setPositiveButton("OK",new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // Return from activity
+ finish();
+ }
+ });
+ AlertDialog a = builder.create();
+ a.show();
+ }
+ }
+
+ // Loop through available GATT Services.
+ for (BluetoothGattService gattService : gattServices) {
+
+ // Get name of current service
+ uuid = gattService.getUuid().toString();
+ serviceName = ProjectZeroAttributes.lookup(uuid, unknownServiceString);
+
+ // Loop through available Characteristics for current service.
+ List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics();
+ for (final BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
+
+ // Get name of current characteristic
+ uuid = gattCharacteristic.getUuid().toString();
+ String characteristicName = ProjectZeroAttributes.lookup(uuid, unknownCharaString);
+
+ // Handle LED characteristics
+ if(serviceName.contains("Led")) {
+ // Get button instance
+ ToggleButton b;
+ if (characteristicName.contains("Led0")) {
+ b = (ToggleButton) findViewById(R.id.led0_value);
+ } else if (characteristicName.contains("Led1")) {
+ b = (ToggleButton) findViewById(R.id.led1_value);
+ } else{
+ continue;
+ }
+
+ // Add action for clicking the LED button
+ if(b!= null) {
+ b.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ // Write value to 1 if button is checked, and to 0 otherwise
+ byte[] value = new byte[1];
+ if (isChecked) {
+ value[0] = (byte) (1 & 0xFF);
+ } else {
+
+ value[0] = (byte) (0 & 0xFF);
+ }
+
+ // Write value
+ gattCharacteristic.setValue(value);
+ mMainActivity.writeCharacteristic(gattCharacteristic);
+ }
+ });
+ }
+
+ // Read initial values of the LEDs
+ mMainActivity.readCharacteristic(gattCharacteristic);
+ }
+ // save discovered characteristics
+ mGattCharacteristics.put(characteristicName, gattCharacteristic);
+ }
+ }
+ }
+
+ /**
+ * Called when the user clicks the read button for data service
+ */
+ public void onDataRead(View view) {
+ if (mGattCharacteristics != null) {
+ // Get characteristic for R/W string
+ final BluetoothGattCharacteristic characteristic = mGattCharacteristics.get("String char");
+ if(characteristic == null) {
+ return;
+ }
+ // Read it if readable
+ final int charaProp = characteristic.getProperties();
+ if ((charaProp | BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
+ mMainActivity.readCharacteristic(characteristic);
+ }
+ }
+ }
+
+ /**
+ * Called when the user clicks the write button for data service
+ */
+ public void onDataWrite(View view) {
+ if (mGattCharacteristics != null) {
+ // Get characteristic for R/W string
+ final BluetoothGattCharacteristic characteristic = mGattCharacteristics.get("String char");
+ if(characteristic == null)
+ return;
+
+ // Check if writable
+ final int charaProp = characteristic.getProperties();
+ if ((charaProp | BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {
+ // Get text
+ String strData = mDataField.getText().toString();
+ byte[] data = strData.getBytes();
+
+ // Write it
+ characteristic.setValue(data);
+ mMainActivity.writeCharacteristic(characteristic);
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/led_green.xml b/app/src/main/res/drawable/led_green.xml
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
+ <corners
+ android:radius="100dp"
+ />
+ <solid
+ android:color="#00FF00"
+ />
+ <padding
+ android:left="0dp"
+ android:top="0dp"
+ android:right="0dp"
+ android:bottom="0dp"
+ />
+ <size
+ android:width="100dp"
+ android:height="100dp"
+ />
+ </shape>
+ </item>
+ <item android:state_checked="false">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
+ <corners
+ android:radius="100dp"
+ />
+ <solid
+ android:color="#008500"
+ />
+ <padding
+ android:left="0dp"
+ android:top="0dp"
+ android:right="0dp"
+ android:bottom="0dp"
+ />
+ <size
+ android:width="100dp"
+ android:height="100dp"
+ />
+ </shape>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/led_red.xml b/app/src/main/res/drawable/led_red.xml
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
+ <corners
+ android:radius="100dp"
+ />
+ <solid
+ android:color="#FF0000"
+ />
+ <padding
+ android:left="0dp"
+ android:top="0dp"
+ android:right="0dp"
+ android:bottom="0dp"
+ />
+ <size
+ android:width="100dp"
+ android:height="100dp"
+ />
+ </shape>
+ </item>
+ <item android:state_checked="false">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
+ <corners
+ android:radius="100dp"
+ />
+ <solid
+ android:color="#850000"
+ />
+ <padding
+ android:left="40dp"
+ android:top="40dp"
+ android:right="40dp"
+ android:bottom="40dp"
+ />
+ <size
+ android:width="100dp"
+ android:height="100dp"
+ />
+ </shape>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ tools:context="com.example.ti.bleprojectzero.MainActivity">
+
+ <android.support.design.widget.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:theme="@style/AppTheme.AppBarOverlay">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ app:popupTheme="@style/AppTheme.PopupOverlay" />
+
+ </android.support.design.widget.AppBarLayout>
+
+ <include layout="@layout/content_main" />
+</android.support.design.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/activity_selected_device.xml b/app/src/main/res/layout/activity_selected_device.xml
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ tools:context="com.example.ti.bleprojectzero.SelectedDeviceActivity">
+
+ <android.support.design.widget.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:theme="@style/AppTheme.AppBarOverlay">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ app:popupTheme="@style/AppTheme.PopupOverlay" />
+
+ </android.support.design.widget.AppBarLayout>
+
+ <include layout="@layout/content_selected_device" />
+</android.support.design.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_marginTop="?android:attr/actionBarSize"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:scrollbars="none"
+ android:layout_weight="1">
+ <TableLayout
+ android:id="@+id/devicesFound"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:stretchColumns="1"
+ android:orientation="vertical"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
+ </TableLayout>
+</ScrollView>
+
diff --git a/app/src/main/res/layout/content_selected_device.xml b/app/src/main/res/layout/content_selected_device.xml
--- /dev/null
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ tools:context="com.example.ti.bleprojectzero.SelectedDeviceActivity"
+ tools:showIn="@layout/activity_selected_device">
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="0dp"
+ android:background="@color/primaryLight"
+ android:padding="10dp">
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/label_state"
+ android:textSize="16sp"/>
+ <Space android:layout_width="5dp"
+ android:layout_height="wrap_content"/>
+ <TextView android:id="@+id/connection_state"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/disconnected"
+ android:textSize="16sp"/>
+ </LinearLayout>
+ <RelativeLayout android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp">
+ <TextView android:id="@+id/led_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/label_led"
+ android:textSize="18sp"/>
+ <Space android:layout_width="5dp"
+ android:layout_height="wrap_content"/>
+ <ToggleButton android:id="@+id/led0_value"
+ android:layout_below="@id/led_label"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:layout_margin="10dp"
+ android:textOff=""
+ android:textOn=""
+ android:background="@drawable/led_red" />
+ <ToggleButton android:id="@+id/led1_value"
+ android:layout_below="@id/led_label"
+ android:layout_toRightOf="@id/led0_value"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:layout_margin="10dp"
+ android:textOff=""
+ android:textOn=""
+ android:background="@drawable/led_green"/>
+ </RelativeLayout>
+ <RelativeLayout android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp">
+ <TextView android:id="@+id/button_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/label_button"
+ android:textSize="18sp"/>
+ <Space
+ android:layout_below="@id/button_label"
+ android:layout_width="5dp"
+ android:layout_height="wrap_content"/>
+ <ToggleButton android:id="@+id/button0_value"
+ android:layout_below="@id/button_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textOff=""
+ android:textOn="@string/pressed"
+ android:textColor="#000000"
+ android:enabled="false"/>
+ <ToggleButton android:id="@+id/button1_value"
+ android:layout_below="@id/button_label"
+ android:layout_toRightOf="@id/button0_value"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textOff=""
+ android:textOn="@string/pressed"
+ android:textColor="#000000"
+ android:enabled="false"/>
+ </RelativeLayout>
+ <RelativeLayout android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp">
+ <TextView android:id="@+id/label_data"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/label_data"
+ android:textSize="18sp"/>
+ <Space android:layout_width="5dp"
+ android:layout_height="wrap_content"/>
+ <EditText android:id="@+id/data_value"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:textSize="18sp"
+ android:layout_below="@id/label_data"
+ android:inputType="text" />
+ <Button android:id="@+id/data_read"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/read"
+ android:layout_below="@id/data_value"
+ android:onClick="onDataRead"/>
+ <Button android:id="@+id/data_write"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/write"
+ android:layout_toRightOf="@id/data_read"
+ android:layout_below="@id/data_value"
+ android:onClick="onDataWrite"/>
+ </RelativeLayout>
+</LinearLayout >
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..cde69bc
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
index 0000000..cde69bc
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..c133a0c
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
index 0000000..c133a0c
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..bfa42f0
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
index 0000000..bfa42f0
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..324e72c
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
index 0000000..324e72c
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..aee44e1
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
index 0000000..aee44e1
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorPrimaryDark">#303F9F</color>
+ <color name="colorAccent">#FF4081</color>
+ <color name="primaryLight">#553f51b5</color>
+</resources>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
--- /dev/null
@@ -0,0 +1,6 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+ <dimen name="fab_margin">16dp</dimen>
+</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
--- /dev/null
@@ -0,0 +1,25 @@
+<resources>
+ <!-- Main activity -->
+ <string name="app_name">BLE Project Zero</string>
+ <string name="button_connect">Connect</string>
+
+ <!-- Second activity -->
+ <string name="title_activity_selected_device">SelectedDeviceActivity</string>
+
+ <string name="label_state">State:</string>
+ <string name="label_data">Data:</string>
+ <string name="label_led">LED:</string>
+ <string name="label_button">Button:</string>
+
+ <string name="disconnected">Disconnected</string>
+ <string name="connected">Connected</string>
+ <string name="read">Read</string>
+ <string name="write">Write</string>
+ <string name="pressed">Pressed</string>
+ <string name="menu_connect">CONNECT</string>
+ <string name="menu_disconnect">DISCONNECT</string>
+
+ <string name="unknown_device">Unknown device</string>
+ <string name="unknown_characteristic">Unknown characteristic</string>
+ <string name="unknown_service">Unknown service</string>
+</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
--- /dev/null
@@ -0,0 +1,15 @@
+<resources>
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+ <style name="AppTheme.NoActionBar">
+ <item name="windowActionBar">false</item>
+ <item name="windowNoTitle">true</item>
+ </style>
+ <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
+ <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
+</resources>
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644 (file)
index 0000000..13372ae
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
index 0000000..13372ae
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
--- /dev/null
@@ -0,0 +1,6 @@
+#Mon Dec 28 10:00:20 PST 2015
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/gradlew b/gradlew
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"