/*
* 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.ui.control;
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.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
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;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice;
import uk.co.alt236.bluetoothlelib.resolvers.GattAttributeResolver;
import uk.co.alt236.bluetoothlelib.util.ByteUtils;
import uk.co.alt236.btlescan.R;
import uk.co.alt236.btlescan.services.BluetoothLeService;
/**
* 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 AppCompatActivity {
private static final String EXTRA_DEVICE = DeviceControlActivity.class.getName() + ".EXTRA_DEVICE";
private final static String TAG = DeviceControlActivity.class.getSimpleName();
@Bind(R.id.gatt_services_list)
protected ExpandableListView mGattServicesList;
@Bind(R.id.connection_state)
protected TextView mConnectionState;
@Bind(R.id.uuid)
protected TextView mGattUUID;
@Bind(R.id.description)
protected TextView mGattUUIDDesc;
@Bind(R.id.data_as_string)
protected TextView mDataAsString;
@Bind(R.id.data_as_array)
protected TextView mDataAsArray;
private Exporter mExporter;
private BluetoothGattCharacteristic mNotifyCharacteristic;
private BluetoothLeService mBluetoothLeService;
// 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(final ExpandableListView parent, final View v, final int groupPosition, final int childPosition, final long id) {
final GattDataAdapterFactory.GattDataAdapter adapter =
(GattDataAdapterFactory.GattDataAdapter) parent.getExpandableListAdapter();
final BluetoothGattCharacteristic characteristic =
adapter.getBluetoothGattCharacteristic(groupPosition, 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;
}
};
// Code to manage Service lifecycle.
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(final ComponentName componentName, final 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(mDevice.getAddress());
}
@Override
public void onServiceDisconnected(final ComponentName componentName) {
mBluetoothLeService = null;
}
};
private BluetoothLeDevice mDevice;
private State mCurrentState = State.DISCONNECTED;
private String mExportString;
// 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(final Context context, final Intent intent) {
final String action = intent.getAction();
if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
updateConnectionState(State.CONNECTED);
invalidateOptionsMenu();
} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
clearUI();
updateConnectionState(State.DISCONNECTED);
invalidateOptionsMenu();
} else if (BluetoothLeService.ACTION_GATT_CONNECTING.equals(action)) {
clearUI();
updateConnectionState(State.CONNECTING);
invalidateOptionsMenu();
} 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)) {
final String noData = getString(R.string.no_data);
final String uuid = intent.getStringExtra(BluetoothLeService.EXTRA_UUID_CHAR);
final byte[] dataArr = intent.getByteArrayExtra(BluetoothLeService.EXTRA_DATA_RAW);
mGattUUID.setText(tryString(uuid, noData));
mGattUUIDDesc.setText(GattAttributeResolver.getAttributeName(uuid, getString(R.string.unknown)));
mDataAsArray.setText(ByteUtils.byteArrayToHexString(dataArr));
mDataAsString.setText(new String(dataArr));
}
}
};
private void clearUI() {
mExportString = null;
mGattServicesList.setAdapter((SimpleExpandableListAdapter) null);
mGattUUID.setText(R.string.no_data);
mGattUUIDDesc.setText(R.string.no_data);
mDataAsArray.setText(R.string.no_data);
mDataAsString.setText(R.string.no_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(final List<BluetoothGattService> gattServices) {
if (gattServices == null) return;
mExportString = mExporter.generateExportString(
mDevice.getName(),
mDevice.getAddress(),
gattServices);
final GattDataAdapterFactory.GattDataAdapter adapter = GattDataAdapterFactory.createAdapter(this, gattServices);
mGattServicesList.setAdapter(adapter);
invalidateOptionsMenu();
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_gatt_services);
final Intent intent = getIntent();
mDevice = intent.getParcelableExtra(EXTRA_DEVICE);
ButterKnife.bind(this);
// Sets up UI references.
((TextView) findViewById(R.id.device_address)).setText(mDevice.getAddress());
mGattServicesList.setOnChildClickListener(servicesListClickListner);
getSupportActionBar().setTitle(mDevice.getName());
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mExporter = new Exporter(this);
final Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);
bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.gatt_services, menu);
switch (mCurrentState) {
case DISCONNECTED:
menu.findItem(R.id.menu_connect).setVisible(true);
menu.findItem(R.id.menu_disconnect).setVisible(false);
menu.findItem(R.id.menu_refresh).setActionView(null);
break;
case CONNECTING:
menu.findItem(R.id.menu_connect).setVisible(false);
menu.findItem(R.id.menu_disconnect).setVisible(false);
menu.findItem(R.id.menu_refresh).setActionView(R.layout.actionbar_progress_indeterminate);
break;
case CONNECTED:
menu.findItem(R.id.menu_connect).setVisible(false);
menu.findItem(R.id.menu_disconnect).setVisible(true);
menu.findItem(R.id.menu_refresh).setActionView(null);
break;
default:
throw new IllegalStateException("Don't know how to handle: " + mCurrentState);
}
if (mExportString == null) {
menu.findItem(R.id.menu_share).setVisible(false);
} else {
menu.findItem(R.id.menu_share).setVisible(true);
}
return true;
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(mServiceConnection);
mBluetoothLeService = null;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_connect:
mBluetoothLeService.connect(mDevice.getAddress());
return true;
case R.id.menu_disconnect:
mBluetoothLeService.disconnect();
return true;
case android.R.id.home:
onBackPressed();
return true;
case R.id.menu_share:
final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
final String subject = getString(
R.string.exporter_email_device_services_subject,
mDevice.getName(),
mDevice.getAddress());
intent.setType("text/plain");
intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
intent.putExtra(android.content.Intent.EXTRA_TEXT, mExportString);
startActivity(Intent.createChooser(
intent,
getString(R.string.exporter_email_device_list_picker_text)));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mGattUpdateReceiver);
}
@Override
protected void onResume() {
super.onResume();
registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter());
if (mBluetoothLeService != null) {
final boolean result = mBluetoothLeService.connect(mDevice.getAddress());
Log.d(TAG, "Connect request result=" + result);
}
}
private void updateConnectionState(final State state) {
mCurrentState = state;
runOnUiThread(new Runnable() {
@Override
public void run() {
final int colourId;
final int resId;
switch (state) {
case CONNECTED:
colourId = android.R.color.holo_green_dark;
resId = R.string.connected;
break;
case DISCONNECTED:
colourId = android.R.color.holo_red_dark;
resId = R.string.disconnected;
break;
case CONNECTING:
colourId = android.R.color.black;
resId = R.string.connecting;
break;
default:
colourId = android.R.color.black;
resId = 0;
break;
}
mConnectionState.setText(resId);
mConnectionState.setTextColor(ContextCompat.getColor(DeviceControlActivity.this, colourId));
}
});
}
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);
intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTING);
return intentFilter;
}
private static String tryString(final String string, final String fallback) {
if (string == null) {
return fallback;
} else {
return string;
}
}
public static Intent createIntent(final Context context, final BluetoothLeDevice device) {
final Intent intent = new Intent(context, DeviceControlActivity.class);
intent.putExtra(DeviceControlActivity.EXTRA_DEVICE, device);
return intent;
}
private enum State {
DISCONNECTED,
CONNECTING,
CONNECTED
}
}