Welcome to the fourth part of my AndroidThings tutorial. I wanted to have a way of learning more myself, and also share some of the things I've picked up along the way about AndroidThings. This series is made for anyone who wants to learn more about AndroidThings and created by the HacksterMIA community. Hopefully this becomes useful for many others! If you missed the previous part, I recommend you check that out before continuing as this next part will build on top of what was covered in the previous one. With that lets continue!
RecapSo in the last part we went over how to use Android Things with an Android device and how to toggle a LED using the Android device through Bluetooth. We are finally taking a more advanced approach with working with Android Things and Android in general. Having the devices talk to each other one-way is very cool, and opens the door for a lot of interesting projects. With this next part we will take it a step further and expand a bit on what we've made so far, more specifically we will learn how to have both devices talking to each, bi-directional communication!
Getting StartedSo before we start, lets just make sure we have what we need. You will need an Android device, a physical device would be best as we will communicate with the Raspberry Pi over Bluetooth. As with the previous part of this series, this is actually another significant jump as we are not only mixing Android devices with Android Things devices but also exploring a more advanced features, communicating via Bluetooth, between these two devices. Hopefully you're excited to jump on in!
Update Android Things ProjectSo at this point I will assume that you have an Android device and it has been enabled for Developer mode. Now we're ready to update the code in the project. Go ahead and open up the current project and if you would like to checkout the final version for this part go here. As the project stands now, you should have a mobile module and a things module. If any of these are missing for you then I'd recommend checking out the previous parts.
So if you've been following along, then there's a good chance that you are on an old version of Android Things and your project may not be compiling now. Don't panic! I'll guide you through resolving this issue, unfortunately older branches are in this state and to fix that here's what you need to do:
You should some thing like the following:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.android.things.contrib:driver-button:0.3'
implementation 'com.android.support:support-v4:26.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
compileOnly 'com.google.android.things:androidthings:+'
compile 'com.google.android.things.contrib:driver-button:0.3'
}
So our problem is simply that Google has pushed out a new preview version and we are now out of date (the joys of non-finalized libraries). Fortunately this is an easy enough fix, first look for:
- driver-button:0.3 and change that value to driver-button:0.6 (this should be changed in two places)
- androidthings:+ should change to androidthings:0.7-devpreview
Alright almost there, we need to make the last important change; changing PeripheralManagerService! See what happened is in the new preview this class has been renamed to PeripheralManager and instead of creating an instance ourselves, there is now a new getInstance() method which takes care of this for us.
So head on over the MainActivity class and look for our instance of PeripheralManagerService. Should be within the onCreate method:
Ew its red and broken, so let's fix that! Remove that entire line where we are creating the instance, yup that's a good place to start. Wipe it out and then a few lines down look for your pioService and change it to the following:
mLedGpio = PeripheralManager.getInstance().openGpio(BoardDefaults.getGPIOForLED());
And it should come out a little something like this:
If you attempt to build your project and you see something like this:
Manifest merger failed : uses-sdk:minSdkVersion 24 cannot be smaller than version 26 declared in library [com.google.android.things.contrib:driver-button:0.6] C:\Users\brand\.gradle\caches\transforms-1\files-1.1\driver-button-0.6.aar\108a269112708e501d1464cebeab94f8\AndroidManifest.xml as the library might be using APIs not available in 24
Suggestion: use a compatible library with a minSdk of at most 24,
or increase this project's minSdk version to at least 26,
or use tools:overrideLibrary="com.google.android.things.contrib.driver.button" to force usage (may lead to runtime failures)
Just update the minSdkVersion for your project to 26 (remember in part 1 I had mentioned that Oreo was the minimum Android Things supported).
Right click your things project and Open Module Settings...
After you make the change to the min version, your project should be good to go again. Make the same change for the mobile module while you're at it.
Share ThingsBefore we add any code, I want to point out something interesting (which I might have spoiled back in part 1), Android Things is Android! (gross simplification but mostly true)
So this means that the Android code we write will most certainly work for our Android Things project! As cool as seeing both devices communicate over Bluetooth is, I think the magic of have both modules share the same code is even better.
Rather than duplicate the same classes between modules, we are going to create a shared module that both mobile and things will share. So let's create the new shared module: File -> New -> New Module and select Android Library.
Click Next and fill in the info like so:
It's really important the you make sure the Minimum SDK is set to API 26 otherwise you will not be able to use it within the other modules since we updated them to that API level. And click Finish and you should be all set!
Your project structure should now look like this:
Now we have a shared library module, we now need to add the dependency to the other two modules so that they can start using it.
So for each module do the following:
- Right click -> Open Module Settings
- Click the Dependencies tab
- Click the green plus button to the right -> Module dependency
- Select only sharedlib and click OK
That should be it! Now the shared library is part of the other two modules and now we can start adding in the shared code. Just a note: Make sure that your API versions match up across all three modules to make your life easier.
Some Android DevelopmentAlright, so let's get into some Android development. Our goal here will be to create our shared classes first within the shared library, then we'll go ahead and start cleaning up the other modules and implement the classes. Let's get started by creating a new class called BluetoothHelper:
public class BluetoothHelper {
private static final String TAG = "BluetoothHelper";
public static final String ANDROID_THINGS_DEVICE_NAME = "My Android Things device";
public static final String MOBILE_DEVICE_NAME = "Pixel 2";
public static final int REQUEST_ENABLE_BT = 1;
// Stops scanning after 10 seconds.
public static final long SCAN_PERIOD = 10000;
private static BluetoothManager mBluetoothManager;
private static BluetoothGattServer mBluetoothGattServer;
private static BluetoothLeAdvertiser mBluetoothLeAdvertiser;
public static BluetoothManager getBluetoothManager()
{
return mBluetoothManager;
}
public static BluetoothGattServer getBluetoothGattServer()
{
return mBluetoothGattServer;
}
public static void setBluetoothManager(BluetoothManager bluetoothManager)
{
mBluetoothManager = bluetoothManager;
}
public 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;
}
/**
* Begin advertising over Bluetooth that this device is connectable
* and supports the Remote LED Service.
*/
public static void startAdvertising() {
BluetoothAdapter bluetoothAdapter = BluetoothHelper.getBluetoothManager().getAdapter();
mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
if (mBluetoothLeAdvertiser == null) {
Log.w(TAG, "Failed to create advertiser");
return;
}
AdvertiseSettings settings = new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
.setConnectable(true)
.setTimeout(0)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
.build();
AdvertiseData data = new AdvertiseData.Builder()
.setIncludeDeviceName(true)
.setIncludeTxPowerLevel(false)
.addServiceUuid(new ParcelUuid(RemoteLedProfile.REMOTE_LED_SERVICE))
.build();
mBluetoothLeAdvertiser
.startAdvertising(settings, data, mAdvertiseCallback);
}
/**
* Stop Bluetooth advertisements.
*/
public static void stopAdvertising() {
if (mBluetoothLeAdvertiser == null) return;
mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
}
/**
* Initialize the GATT server instance with the services/characteristics
* from the Remote LED Profile.
*/
public static void startServer(Context context, BluetoothGattServerCallback mGattServerCallback) {
mBluetoothGattServer = mBluetoothManager.openGattServer(context, mGattServerCallback);
if (mBluetoothGattServer == null) {
Log.w(TAG, "Unable to create GATT server");
return;
}
mBluetoothGattServer.addService(RemoteLedProfile.createRemoteLedService());
}
/**
* Shut down the GATT server.
*/
public static void stopServer() {
if (mBluetoothGattServer == null) return;
mBluetoothGattServer.close();
}
/**
* Callback to receive information about the advertisement process.
*/
private static AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
Log.i(TAG, "LE Advertise Started.");
}
@Override
public void onStartFailure(int errorCode) {
Log.w(TAG, "LE Advertise Failed: "+errorCode);
}
};
}
Go ahead and drop in the above code, some of this might look familiar from previous we've written so far. We're moving a lot of the common code that would end up being used for both modules to this helper class and keeping it in the shared library. Since we are just really moving existing code, there isn't much here to go over that's new, but something to note are the two constants: ANDROID_THINGS_DEVICE_NAME and MOBILE_DEVICE_NAME.
Now that we have that done, let's create the next class: RemoteLedProfile.
public class RemoteLedProfile {
/* Remote LED Service UUID */
public static UUID REMOTE_LED_SERVICE = UUID.fromString("00001805-0000-1000-8000-00805f9b34fb");
/* Remote LED Data Characteristic */
public static UUID REMOTE_LED_DATA = UUID.fromString("00002a2b-0000-1000-8000-00805f9b34fb");
/**
* Return a configured {@link BluetoothGattService} instance for the
* Remote LED Service.
*/
public static BluetoothGattService createRemoteLedService() {
BluetoothGattService service = new BluetoothGattService(REMOTE_LED_SERVICE,
BluetoothGattService.SERVICE_TYPE_PRIMARY);
BluetoothGattCharacteristic ledData = new BluetoothGattCharacteristic(REMOTE_LED_DATA,
//Read-only characteristic, supports notifications
BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ);
service.addCharacteristic(ledData);
return service;
}
}
Create this new class in the shared library and drop in the code above. The same deal as the previous class; nothing new here and we'll remove old code later on.
Now for the last class of the shared library: BluetoothLeService.
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";
// 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);
// 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);
}
/**
* 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<BluetoothGattService> getSupportedGattServices() {
if (mBluetoothGatt == null) return null;
return mBluetoothGatt.getServices();
}
}
This is the same code from the things module, so again not much to go over here too.
Cleanup Time!Alright so now that we've set up the shared library and hooked it up with the other two modules, it's now time to cleanup the duplicate code we no longer need. We'll go ahead and start with the mobile module.
In the module, just go ahead and delete the RemoteLedProfile class. We're going to change the reference in the MainActivity class next to the the RemoteLedProfile class from the shared library. If things go smoothly the transition should be seamless, otherwise use Android Studio's auto import feature to clean up the reference for you.
Now go to the things module and from there you will need to delete the BluetoothLeService and again should be a seamless transition.
And that should be it! We're going to worry about the MainActivity next in the next section, but as far as the duplicate classes go, we've removed them and now we're leveraging the shared library in both other modules. Woot!
Updating All MainActivityWe're almost there! Next we need to update the MainActivity class for both the mobile and things modules.
Let's start again in the mobile module, open up the MainActivity class. First, we're going to replace our global variables, look for these:
private BluetoothManager mBluetoothManager;
private BluetoothGattServer mBluetoothGattServer;
private BluetoothLeAdvertiser mBluetoothLeAdvertiser;
private Set<BluetoothDevice> mRegisteredDevices = new HashSet<>();
Boolean toggleLight = false;
And replace the whole section there with the following:
private Set<BluetoothDevice> mRegisteredDevices = new HashSet<>();
Boolean toggleLight = false;
private BluetoothAdapter mBluetoothAdapter;
private boolean mScanning;
private Handler mHandler;
private BluetoothLeService mBluetoothLeService;
private boolean mConnected = false;
private BluetoothGattCharacteristic mNotifyCharacteristic;
private String mDeviceAddress;
Next we're going to update the onCreate method; here is the following code you will want to match up with:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new Handler();
BluetoothHelper.setBluetoothManager((BluetoothManager) getSystemService(BLUETOOTH_SERVICE));
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mBluetoothReceiver, filter);
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter != null) {
if (mBluetoothAdapter.isEnabled()) {
Log.d(TAG, "Bluetooth enabled...starting services");
BluetoothHelper.startAdvertising();
BluetoothHelper.startServer(this, mGattServerCallback);
}else {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, 0);
}
}
Button toggleButton = findViewById(R.id.toggle_button);
toggleButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
toggleLight = !toggleLight;
notifyRegisteredDevices(toggleLight);
}
});
}
So now the onCreate will setup the Handler and necessary receiver we need for picking up data from the Raspberry Pi later on.
Update the mBluetoothReceiver object like so:
private BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
switch (state) {
case BluetoothAdapter.STATE_ON:
BluetoothHelper.startAdvertising();
BluetoothHelper.startServer(MainActivity.this, mGattServerCallback);
break;
case BluetoothAdapter.STATE_OFF:
BluetoothHelper.stopServer();
BluetoothHelper.stopAdvertising();
break;
default:
// Do nothing
}
}
};
Now we have a little additional cleanup to do; completely remove the following methods and object:
- startAdveristing
- stopAdvertising
- startServer
- stopServer
- mAdvertiseCallback
Strip those out of the class and drop the following code in:
private void initScan() {
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Log.e(TAG, "Bluetooth adapter not available or not enabled.");
return;
}
//setupBTProfiles();
Log.d(TAG, "Set up Bluetooth Adapter name and profile");
mBluetoothAdapter.setName(BluetoothHelper.MOBILE_DEVICE_NAME);
scanLeDevice();
}
private void scanLeDevice() {
// Stops scanning after a pre-defined scan period.
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
}, BluetoothHelper.SCAN_PERIOD);
mScanning = true;
mBluetoothAdapter.startLeScan(mLeScanCallback);
}
private BluetoothAdapter.LeScanCallback mLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
Boolean isDeviceFound = false;
if(device != null){
final String deviceName = device.getName();
if (deviceName != null && deviceName.length() > 0) {
if(deviceName.equals(BluetoothHelper.ANDROID_THINGS_DEVICE_NAME)){
isDeviceFound = true;
mDeviceAddress = device.getAddress();
}
}
}
if(isDeviceFound && !mConnected){
Intent gattServiceIntent = new Intent(MainActivity.this, BluetoothLeService.class);
bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
mConnected = true;
}
}
};
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();
}
Boolean result = mBluetoothLeService.connect(mDeviceAddress);
Log.d(TAG, "Connect request result=" + result);
mConnected = true;
if(mScanning) {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
mBluetoothLeService = null;
}
};
This is more or less the same code (there's a slight deviation to make it a little more specific for the mobile side) from the things module.
Next update notifyRegisteredDevices with the following code:
private void notifyRegisteredDevices(Boolean toggle) {
if (mRegisteredDevices.isEmpty()) {
Log.i(TAG, "No subscribers registered");
return;
}
Log.i(TAG, "Sending update to " + mRegisteredDevices.size() + " subscribers");
for (BluetoothDevice device : mRegisteredDevices) {
BluetoothGattCharacteristic ledDataCharacteristic = BluetoothHelper.getBluetoothGattServer()
.getService(RemoteLedProfile.REMOTE_LED_SERVICE)
.getCharacteristic(RemoteLedProfile.REMOTE_LED_DATA);
ledDataCharacteristic.setValue(toggle.toString());
BluetoothHelper.getBluetoothGattServer().notifyCharacteristicChanged(device, ledDataCharacteristic, false);
}
}
Now we need to replace mGattServerCallback with the following updated one:
private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.i(TAG, "BluetoothDevice CONNECTED: " + device);
mRegisteredDevices.add(device);
initScan();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.i(TAG, "BluetoothDevice DISCONNECTED: " + device);
//Remove device from any active subscriptions
mRegisteredDevices.remove(device);
}
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattCharacteristic characteristic) {
long now = System.currentTimeMillis();
if (RemoteLedProfile.REMOTE_LED_DATA.equals(characteristic.getUuid())) {
Log.i(TAG, "Read data");
BluetoothHelper.getBluetoothGattServer().sendResponse(device,
requestId,
BluetoothGatt.GATT_SUCCESS,
0,
null);
} else {
// Invalid characteristic
Log.w(TAG, "Invalid Characteristic Read: " + characteristic.getUuid());
BluetoothHelper.getBluetoothGattServer().sendResponse(device,
requestId,
BluetoothGatt.GATT_FAILURE,
0,
null);
}
}
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattDescriptor descriptor) {
BluetoothHelper.getBluetoothGattServer().sendResponse(device,
requestId,
BluetoothGatt.GATT_FAILURE,
0,
null);
}
@Override
public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
BluetoothGattDescriptor descriptor,
boolean preparedWrite, boolean responseNeeded,
int offset, byte[] value) {
if (responseNeeded) {
BluetoothHelper.getBluetoothGattServer().sendResponse(device,
requestId,
BluetoothGatt.GATT_FAILURE,
0,
null);
}
}
};
We're almost done with the mobile module, next drop the following:
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;
} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
mConnected = false;
} else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
// Show all the supported services and characteristics on the user interface.
List<BluetoothGattService> services = mBluetoothLeService.getSupportedGattServices();
if(services != null){
for (BluetoothGattService gattService : services) {
if(gattService.getUuid().equals(RemoteLedProfile.REMOTE_LED_SERVICE)){
final BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(RemoteLedProfile.REMOTE_LED_DATA);
if (characteristic != null) {
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);
}
}
}
}
}
} else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
Toast.makeText(context, "Raspberry Pi Toggled!", Toast.LENGTH_LONG).show();
}
}
};
Alright so we've been changing and dumping a lot of code, but this chunk here is probably the most interesting and important; you may recognize it already but this is a similar receiver from the things module where we acting upon receiving data from the remote device. So now the mobile device can take action when it receives data from a remote device. In this example to keep things simple, I just have the mobile device display a toast message. You should totally feel free to change this and do something more interesting, but this is where you would want to do it.
To wrap up the changes for the mobile module, go ahead and replace the onDestroy method with the following:
@Override
protected void onStart() {
super.onStart();
registerReceiver(mGattUpdateReceiver, BluetoothHelper.makeGattUpdateIntentFilter());
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == BluetoothHelper.REQUEST_ENABLE_BT) {
Log.d(TAG, "Enable discoverable returned with result " + resultCode);
// ResultCode, as described in BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE, is either
// RESULT_CANCELED or the number of milliseconds that the device will stay in
// discoverable mode. In a regular Android device, the user will see a popup requesting
// authorization, and if they cancel, RESULT_CANCELED is returned. In Android Things,
// on the other hand, the authorization for pairing is always given without user
// interference, so RESULT_CANCELED should never be returned.
if (resultCode == RESULT_CANCELED) {
Log.e(TAG, "Enable discoverable has been cancelled by the user. " +
"This should never happen in an Android Things device.");
}
}
}
@Override
protected void onPause(){
super.onPause();
unregisterReceiver(mGattUpdateReceiver);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (BluetoothHelper.getBluetoothManager().getAdapter().isEnabled()) {
BluetoothHelper.stopServer();
BluetoothHelper.stopAdvertising();
}
unregisterReceiver(mBluetoothReceiver);
unbindService(mServiceConnection);
mBluetoothLeService = null;
}
There's a little more than the onDestroy but it's all useful for the new code we've added to the mobile module.
And that's it! Now we're ready to jump to the things module.
Open up the MainActivity class in the things module and we'll start here again with the global variables. Replace the following:
private Gpio mLedGpio;
private ButtonInputDriver mButtonInputDriver;
/* Remote LED Service UUID */
public static UUID REMOTE_LED_SERVICE = UUID.fromString("00001805-0000-1000-8000-00805f9b34fb");
/* Remote LED Data Characteristic */
public static UUID REMOTE_LED_DATA = UUID.fromString("00002a2b-0000-1000-8000-00805f9b34fb");
private static final String ANDROID_DEVICE_NAME = "Pixel 2";
private static final String ADAPTER_FRIENDLY_NAME = "My Android Things device";
private static final int REQUEST_ENABLE_BT = 1;
// Stops scanning after 10 seconds.
private static final long SCAN_PERIOD = 10000;
private BluetoothAdapter mBluetoothAdapter;
private boolean mScanning;
private Handler mHandler;
private BluetoothLeService mBluetoothLeService;
private boolean mConnected = false;
private BluetoothGattCharacteristic mNotifyCharacteristic;
private String mDeviceAddress;
With this:
private Set<BluetoothDevice> mRegisteredDevices = new HashSet<>();
private Gpio mLedGpio;
private ButtonInputDriver mButtonInputDriver;
private BluetoothAdapter mBluetoothAdapter;
private boolean mScanning;
private Handler mHandler;
private BluetoothLeService mBluetoothLeService;
private boolean mConnected = false;
private BluetoothGattCharacteristic mNotifyCharacteristic;
private String mDeviceAddress;
Now let's update the onCreate method with the following:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
Log.i(TAG, "Configuring GPIO pins");
mLedGpio = PeripheralManager.getInstance().openGpio(BoardDefaults.getGPIOForLED());
mLedGpio.setDirection(Gpio.DIRECTION_OUT_INITIALLY_LOW);
Log.i(TAG, "Registering button driver");
// Initialize and register the InputDriver that will emit SPACE key events
// on GPIO state changes.
mButtonInputDriver = new ButtonInputDriver(
BoardDefaults.getGPIOForButton(),
Button.LogicState.PRESSED_WHEN_LOW,
KeyEvent.KEYCODE_SPACE);
mHandler = new Handler();
BluetoothHelper.setBluetoothManager((BluetoothManager) getSystemService(BLUETOOTH_SERVICE));
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mBluetoothReceiver, filter);
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter != null) {
if (mBluetoothAdapter.isEnabled()) {
Log.d(TAG, "Bluetooth enabled...starting services");
initScan();
} else {
Log.d(TAG, "Bluetooth adapter not enabled. Enabling.");
mBluetoothAdapter.enable();
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, 0);
}
}
} catch (IOException e) {
Log.e(TAG, "Error configuring GPIO pins", e);
}
}
This now sets up the ability for the Raspberry Pi to now broadcast the Bluetooth service so the mobile device can hook up to it via Bluetooth and send data to it.
Next we to add the BroadcastReceiver so add the following code next:
/**
* Listens for Bluetooth adapter events to enable/disable
* advertising and server functionality.
*/
private BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
switch (state) {
case BluetoothAdapter.STATE_ON:
BluetoothHelper.startAdvertising();
BluetoothHelper.startServer(MainActivity.this, mGattServerCallback);
break;
case BluetoothAdapter.STATE_OFF:
BluetoothHelper.stopServer();
BluetoothHelper.stopAdvertising();
break;
default:
// Do nothing
}
}
};
Now update the following code:
private void initScan() {
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Log.e(TAG, "Bluetooth adapter not available or not enabled.");
return;
}
//setupBTProfiles();
Log.d(TAG, "Set up Bluetooth Adapter name and profile");
mBluetoothAdapter.setName(BluetoothHelper.ANDROID_THINGS_DEVICE_NAME);
scanLeDevice();
}
private void scanLeDevice() {
// Stops scanning after a pre-defined scan period.
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
}, BluetoothHelper.SCAN_PERIOD);
mScanning = true;
mBluetoothAdapter.startLeScan(mLeScanCallback);
}
private BluetoothAdapter.LeScanCallback mLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
Boolean isDeviceFound = false;
if(device != null){
final String deviceName = device.getName();
if (deviceName != null && deviceName.length() > 0) {
//Log.d(TAG, deviceName);
if(deviceName.equals(BluetoothHelper.MOBILE_DEVICE_NAME)){
isDeviceFound = true;
mDeviceAddress = device.getAddress();
}
}
}
if(isDeviceFound && !mConnected){
Intent gattServiceIntent = new Intent(MainActivity.this, BluetoothLeService.class);
bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
mConnected = true;
BluetoothHelper.startAdvertising();
BluetoothHelper.startServer(MainActivity.this, mGattServerCallback);
}
}
};
Next skip past the mServiceConnection object and drop in the following:
/**
* Send a remote led service notification to any devices that are subscribed
* to the characteristic.
*/
private void notifyRegisteredDevices(Boolean toggle) {
if (mRegisteredDevices.isEmpty()) {
Log.i(TAG, "No subscribers registered");
return;
}
Log.i(TAG, "Sending update to " + mRegisteredDevices.size() + " subscribers");
for (BluetoothDevice device : mRegisteredDevices) {
BluetoothGattCharacteristic ledDataCharacteristic = BluetoothHelper.getBluetoothGattServer()
.getService(RemoteLedProfile.REMOTE_LED_SERVICE)
.getCharacteristic(RemoteLedProfile.REMOTE_LED_DATA);
ledDataCharacteristic.setValue(toggle.toString());
BluetoothHelper.getBluetoothGattServer().notifyCharacteristicChanged(device, ledDataCharacteristic, false);
}
}
/**
* Callback to handle incoming requests to the GATT server.
* All read/write requests for characteristics and descriptors are handled here.
*/
private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.i(TAG, "BluetoothDevice CONNECTED: " + device);
mRegisteredDevices.add(device);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.i(TAG, "BluetoothDevice DISCONNECTED: " + device);
//Remove device from any active subscriptions
mRegisteredDevices.remove(device);
}
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattCharacteristic characteristic) {
long now = System.currentTimeMillis();
if (RemoteLedProfile.REMOTE_LED_DATA.equals(characteristic.getUuid())) {
Log.i(TAG, "Read data");
BluetoothHelper.getBluetoothGattServer().sendResponse(device,
requestId,
BluetoothGatt.GATT_SUCCESS,
0,
null);
} else {
// Invalid characteristic
Log.w(TAG, "Invalid Characteristic Read: " + characteristic.getUuid());
BluetoothHelper.getBluetoothGattServer().sendResponse(device,
requestId,
BluetoothGatt.GATT_FAILURE,
0,
null);
}
}
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattDescriptor descriptor) {
BluetoothHelper.getBluetoothGattServer().sendResponse(device,
requestId,
BluetoothGatt.GATT_FAILURE,
0,
null);
}
@Override
public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
BluetoothGattDescriptor descriptor,
boolean preparedWrite, boolean responseNeeded,
int offset, byte[] value) {
if (responseNeeded) {
BluetoothHelper.getBluetoothGattServer().sendResponse(device,
requestId,
BluetoothGatt.GATT_FAILURE,
0,
null);
}
}
};
Ok we're almost there, we've added quite a bit more changes and next we're going to add probably the more important change in the things module so far. Looking at the mGattUpdateReceiver make the following change:
String data = intent.getStringExtra(BluetoothLeService.EXTRA_DATA);
if(data != null) {
if (data.contains("false")) {
setLedValue(false);
} else if (data.contains("true")) {
setLedValue(true);
}
notifyRegisteredDevices(true);
}
The important thing to note here is the addition of notifyRegisteredDevices, this is what will send data out to remote devices listening to the service over Bluetooth.
Go ahead and remove the following method makeGattUpdateIntentFilter since we no longer need it here and we can reference it from the BluetoothHelper class we created earlier.
Finally update the onDestroy method with the following:
@Override
protected void onDestroy(){
super.onDestroy();
if (mButtonInputDriver != null) {
mButtonInputDriver.unregister();
try {
mButtonInputDriver.close();
} catch (IOException e) {
Log.e(TAG, "Error closing Button driver", e);
} finally{
mButtonInputDriver = null;
}
}
if (mLedGpio != null) {
try {
mLedGpio.close();
} catch (IOException e) {
Log.e(TAG, "Error closing LED GPIO", e);
} finally{
mLedGpio = null;
}
mLedGpio = null;
}
if (BluetoothHelper.getBluetoothManager().getAdapter().isEnabled()) {
BluetoothHelper.stopServer();
BluetoothHelper.stopAdvertising();
}
unregisterReceiver(mBluetoothReceiver);
unbindService(mServiceConnection);
mBluetoothLeService = null;
}
And that is finally it! I know it was a lot of changes and most of it wasn't very new, but we are using them in a new way at least which is awesome. We moved a good chunk of code to the shared library and referenced it both modules and removed the code we no longer need and then finally we updated both modules to support both sending and receiving data over Bluetooth. Finally we have bi-directional communication going!
Run it!Pat yourself on the back, you should feel proud. Now for the payoff, update your app on the Raspberry Pi, same as previous parts, just make sure you have things as your selected module before running. And now update your mobile app on the Android device. Pay attention to the logs in Android Studio and look out for the connection successful messages. I would recommend keeping logcat open for both projects for this. Once these two devices have made the connection, you should be able to hit the toggle button on the app and watch that LED turn on and off, and now you should also see a toast message displayed on the mobile device. Hopefully its something like this:
Level Up!This is a huge accomplishment and if you've made it this far, you definitely should be proud, and maybe give yourself a few more pats on the back...and brag..totally tell people about what you've made so far with Android Things! Stay tuned for more projects and congrats on making it to the this part of the series, and most of all I hope you enjoyed it and found it useful (and maybe mildly entertaining?). And a huge thanks for everyone following along and requested for this next part! Love the support and I will gladly make additional parts if requested. Happy hacking!
Comments