Create a sensor extension for IoTool using Bluetooth
When developing an IoTool sensor extension for a Bluetooth sensor, this documentation, provided demo code packages and Bluetooth sensor documentation should be used.
IoToolSensorServiceDemoBLE.zip demo code package should be used when developing an IoTool sensor extension for a Bluetooth LE sensor.
IoTool app and IoTool sensor extensions
IoTool app acquires the readings from any sensor device (may include one or more sensors) only using IoTool sensor extensions. For each device should be produced an IoTool sensor extension. Many IoTool sensor extensions are already available, e.g. IoToolSensorServiceDevice.apk for Android device internal sensors. If your device with sensors is not yet supported an IoTool sensor extension should be produced.
Note: IoTool app should not be changed to support new sensor devices. Only an IoTool sensor extension should be developed. IoTool sensor extension itself implements everything used by IoTool app and their communication.
Choosing a service ID
Many developers are producing IoTool sensor extensions and some of them may become public and used by many end users. So, it is preferred that all developed IoTool sensor extensions may run on the same device not interfering one with each other.
As IoTool sensor extensions are actually apk’s they should be named differently for each sensor device.
IoTool sensor extensions are distinct only by service ID which is then used in package name, class names, reading ids etc. throughout IoTool sensor extension.
Service ID should consist only of alphanumeric characters without spaces and special chars. Service ID may consist of:
-
sensor device name,
-
sensor name,
-
sensor model name,
-
device manufacturer name,
-
any combination of above. Note: service ID is case sensitive, except in the package name where lo case service ID should be used.
Where service ID should be used
Demo code package for a Bluetooth LE sensor uses "DemoBLE" as a service ID.
Demo code package name for a Bluetooth LE sensor is "com.example.servicedemoble".
Each IoTool sensor extension needs 3 classes and their names consist of service ID:
-
service for sensor: "IoToolSensorService" + service ID + ".java",
-
preferences activity: "IoToolSensorService" + service ID + "Preferences.java",
-
a provider for various properties: "IoToolSensorService" + service ID + "Provider.java".
Demo code package for a Bluetooth LE sensor consist of the following classes: IoToolSensorServiceDemoBLE.java, IoToolSensorServiceDemoBLEPreferences.java, IoToolSensorServiceDemoBLEProvider.java
Also in each mentioned class service ID is used many times.
How to start developing a new IoTool sensor extension
First a unique service ID should be choosen.
After that a copy of a demo code package should be made.
Choosen service ID should replace the service ID used in the demo code package. If the demo code package for a Bluetooth LE sensor is used to develop a new IoTool sensor extension then all occurences of "DemoBLE" in filenames and file contents should be replaced by choosen service ID.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicedemoble"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="18"
android:targetSdkVersion="18" />
<!--
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
>
<service android:name=".IoToolSensorServiceDemoBLE" android:exported="true"></service>
<provider android:name=".IoToolSensorServiceDemoBLEProvider" android:exported="true" android:authorities="com.example.servicedemoble.IoToolSensorServiceDemoBLEProvider"></provider>
<activity android:name=".IoToolSensorServiceDemoBLEPreferences" android:exported="true"/>
</application>
</manifest>
How to proceed
When a copy of a demo code package with a unique service ID is ready the rest of this developer guide should be used.
Note: Any change to the code may be needed only at the sections which start with "//TODO".
"IoToolSensorService" + service ID + ".java"
Define and implements all necessary setup and processing of data for readings that a sensor supports. In demo code packages this class already implements all service functionality such as connecting to a sensor, responding to the application commands etc.
The information that follows is related to usage of classes implemented in the only library "IoToolLibraryBase.aar".
IoToolReading - this class takes care of storing data in database and broadcasting to IoTool app
setup: Setup(String readingid, File appDir, boolean usedb, String datatype, int sampleinterval, boolean broadcastdata, boolean broadcastimmediately, Context broadcastcontext)
-
readingid - consists of reading name and service ID separated by @ character: readingName@serviceID (reading Name should consist only of alphanumeric characters without spaces and special chars; like service ID)
-
appDir - directory on external storage where databases should be stored (provided by the application that starts the service)
-
usedb - store data in database (provided by the application that starts the service)
-
datatype - IoToolReading can use three different ways to store records in database: for readings that occur at approximately 1 second or longer interval uses dataTypeSingle, for readings that occur many times per second uses dataTypePeriodic if the interval is regular (for example exactly every 20ms) and dataTypeTimestamps when the interval is irregular
-
sampleinterval - if dataTypePeriodic datatype is used the interval in milliseconds should be provided, otherwise sampleinterval may be 0
-
broadcastdata - broadcasts data to application (provided by the application that starts the service)
-
broadcastimmediately - usualy data is broadcasted to application every 200ms, if readings occur at interval that is shorter than that, it’s best to set this to false
-
broadcastcontext - use "this" to select service as context for broadcasts
write and broadcast: writeData(long timestamp, double sample)
-
timestamp - current reading timestamp (unix time in milliseconds)
-
sample - current reading value (numeric)
package com.example.servicedemoble;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import io.senlab.iotool.library.base.IoToolReading;
import io.senlab.iotool.library.base.IoToolConstants;
import android.Manifest;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.widget.Toast;
/**
* Demo service for BLE sensors. (c) Senlab.
*/
public class IoToolSensorServiceDemoBLE extends Service {
private static final String serviceName = "IoToolSensorServiceDemoBLE";
protected static final String serviceID = "DemoBLE";
private BluetoothAdapter BTAdapter;
private BluetoothDevice BTDevice = null;
private BluetoothGatt BTGatt;
private String BTDeviceAddress = "";
private int connectionState = STATE_STARTING;
private static final int STATE_STARTING = -1;
private static final int STATE_DISCONNECTED = 0;
private static final int STATE_CONNECTING = 1;
private static final int STATE_CONNECTED = 2;
private static final int STATE_STOPPING = 9;
private ConnectServiceCommandReceiver connectReceiver;
private NotificationManager mNM;
private Notification notification;
private PendingIntent contentIntent;
private boolean broadcastData = false;
private boolean useDB = false;
private File appDir;
private Queue<BluetoothGattDescriptor> descriptorWriteQueue = new LinkedList<BluetoothGattDescriptor>();
private Queue<BluetoothGattCharacteristic> characteristicWriteQueue = new LinkedList<BluetoothGattCharacteristic>();
private Queue<BluetoothGattCharacteristic> characteristicReadQueue = new LinkedList<BluetoothGattCharacteristic>();
private Handler reconnectHandler = new Handler();
private Runnable reconnectRunnable = null;
private int reconnectInterval = 3000;
//TODO define all readings
private IoToolReading reading1 = null;
private IoToolReading reading2 = null;
//TODO define all UUIDs
private UUID UUID_CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
private UUID UUID_READING1_CHARACTERISTIC = UUID.fromString("00001011-0000-0000-0000-000000000000");
private UUID UUID_READING1_SERVICE = UUID.fromString("00001010-0000-0000-0000-000000000000");
private UUID UUID_READING1_CONFIG = UUID.fromString("00001012-0000-0000-0000-000000000000");
private UUID UUID_READING2_CHARACTERISTIC = UUID.fromString("00001021-0000-0000-0000-000000000000");
private UUID UUID_READING2_SERVICE = UUID.fromString("00001020-0000-0000-0000-000000000000");
private UUID UUID_READING2_CONFIG = UUID.fromString("00001022-0000-0000-0000-000000000000");
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (connectionState == STATE_STARTING) {
try {
boolean useServicePreferences = intent.getBooleanExtra(IoToolConstants.UseServicePreferencesExtra, true);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
CharSequence text = serviceName+" "+getString(R.string.started);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
notification = builder.setContentIntent(contentIntent)
.setSmallIcon(R.drawable.status_disconnected)
.setWhen(System.currentTimeMillis())
.setAutoCancel(true)
.setContentTitle(serviceName)
.setContentText(text)
.build();
contentIntent = PendingIntent.getActivity(this, 0, new Intent(), 0);
startForeground(R.string.foreground_service_id, notification);
initializeBT();
setConnectionState(STATE_DISCONNECTED);
connectReceiver = new ConnectServiceCommandReceiver(this);
registerReceiver(connectReceiver, new IntentFilter(IoToolConstants.ServiceCommand));
if (useServicePreferences) BTDeviceAddress = sp.getString(getString(R.string.preference_key_mac), getString(R.string.preference_default_mac));
else BTDeviceAddress = intent.getStringExtra(IoToolConstants.MACAddressExtra);
broadcastData = intent.getBooleanExtra(IoToolConstants.ServiceSetupBroadcast, false);
useDB = intent.getBooleanExtra(IoToolConstants.ServiceSetupDB, false);
appDir = new File(Environment.getExternalStorageDirectory().toString()+"/"+intent.getStringExtra(IoToolConstants.FileDirExtra));
if (! appDir.exists()) appDir.mkdir();
boolean permissions = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) permissions = false;
}
if (!permissions) {
Intent i = new Intent();
i.setClass(this, IoToolSensorServiceDemoBLEPreferences.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
} else {
initServiceReadings();
setupServiceReadings(intent);
reconnectRunnable = new Runnable() {
@Override
public void run() {
if ((connectionState != STATE_CONNECTED) && (connectionState != STATE_STOPPING)) {
connectDevice();
}
}
};
connectDevice();
}
} catch (Exception e) {
stop();
return START_NOT_STICKY;
}
}
if (!BTAdapter.isEnabled()) {
Toast.makeText(this, getResources().getString(R.string.toasts_bt_unavailable_enable_in_settings), Toast.LENGTH_LONG).show();
stop();
return START_NOT_STICKY;
} else {
return START_STICKY;
}
}
public void onDestroy() {
super.onDestroy();
unregisterReceiver(connectReceiver);
//TODO null all readings
reading1 = null;
reading2 = null;
}
public IBinder onBind(final Intent intent) {
return null;
}
private final BluetoothGattCallback BTGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
BTGatt.discoverServices();
} else {
setConnectionState(STATE_DISCONNECTED);
reconnectDevice();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
setConnectionState(STATE_CONNECTED);
enableReadings();
} else {
BTGatt.discoverServices();
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
characteristicReadQueue.remove();
if (status == BluetoothGatt.GATT_SUCCESS) {
newData(characteristic);
}
sendNextCommand();
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
newData(characteristic);
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
characteristicWriteQueue.remove();
sendNextCommand();
}
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
descriptorWriteQueue.remove();
sendNextCommand();
}
};
private void queueWriteDescriptor(BluetoothGattDescriptor descriptor) {
descriptorWriteQueue.add(descriptor);
if (queuedCommands() == 1) {
sendNextCommand();
}
}
private void queueWriteCharacteristic(BluetoothGattCharacteristic characteristic) {
characteristicWriteQueue.add(characteristic);
if (queuedCommands() == 1) {
sendNextCommand();
}
}
private void queueReadCharacteristic(BluetoothGattCharacteristic characteristic) {
characteristicReadQueue.add(characteristic);
if (queuedCommands() == 1) {
sendNextCommand();
}
}
private int queuedCommands() {
return (descriptorWriteQueue.size() + characteristicWriteQueue.size() + characteristicReadQueue.size());
}
private void sendNextCommand() {
if (!characteristicWriteQueue.isEmpty()) {
BTGatt.writeCharacteristic(characteristicWriteQueue.peek());
} else if (!descriptorWriteQueue.isEmpty()) {
BTGatt.writeDescriptor(descriptorWriteQueue.peek());
} else if (!characteristicReadQueue.isEmpty()) {
BTGatt.readCharacteristic(characteristicReadQueue.peek());
}
}
private void stop() {
setConnectionState(STATE_STOPPING);
if (BTGatt != null) {
BTGatt.disconnect();
BTGatt.close();
BTGatt = null;
}
try {
reconnectHandler.removeCallbacks(reconnectRunnable);
} catch(Exception e) {
}
reconnectRunnable = null;
//TODO close all readings
if (reading1.isEnabled()) reading1.close();
if (reading2.isEnabled()) reading2.close();
BTDevice = null;
BTDeviceAddress = "";
stopForeground(true);
stopSelf();
}
private void connectDevice() {
if (BTGatt != null) {
if (BTGatt.connect()) {
setConnectionState(STATE_CONNECTING);
}
} else {
BTDevice = BTAdapter.getRemoteDevice(BTDeviceAddress);
if (BTDevice != null) {
BTGatt = BTDevice.connectGatt(this, true, BTGattCallback);
setConnectionState(STATE_CONNECTING);
}
}
}
private void reconnectDevice() {
reconnectHandler.postDelayed(reconnectRunnable, reconnectInterval);
}
private void enableReadings() {
//TODO enable / disable readings from sensor - set notifications and any characteristics (if necessary)
if (reading1.isEnabled()) {
setCharacteristic(UUID_READING1_SERVICE, UUID_READING1_CONFIG, new byte[]{1});
setNotifications(UUID_READING1_SERVICE, UUID_READING1_CHARACTERISTIC, true);
} else {
setCharacteristic(UUID_READING1_SERVICE, UUID_READING1_CONFIG, new byte[]{0});
setNotifications(UUID_READING1_SERVICE, UUID_READING1_CHARACTERISTIC, false);
}
if (reading2.isEnabled()) {
setCharacteristic(UUID_READING2_SERVICE, UUID_READING2_CONFIG, new byte[]{1});
setNotifications(UUID_READING2_SERVICE, UUID_READING2_CHARACTERISTIC, true);
} else {
setCharacteristic(UUID_READING2_SERVICE, UUID_READING2_CONFIG, new byte[]{0});
setNotifications(UUID_READING2_SERVICE, UUID_READING2_CHARACTERISTIC, false);
}
}
private void setNotifications(UUID service_uuid, UUID characteristic_uuid, boolean enable) {
BluetoothGattCharacteristic characteristic = BTGatt.getService(service_uuid).getCharacteristic(characteristic_uuid);
BTGatt.setCharacteristicNotification(characteristic, enable);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID_CLIENT_CHARACTERISTIC_CONFIG);
descriptor.setValue(enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
queueWriteDescriptor(descriptor);
}
private void setCharacteristic(UUID service_uuid, UUID characteristic_uuid, byte[] value) {
BluetoothGattCharacteristic characteristic = BTGatt.getService(service_uuid).getCharacteristic(characteristic_uuid);
characteristic.setValue(value);
queueWriteCharacteristic(characteristic);
}
private boolean checkFlag(int flag, int flags) {
if ((flag & flags) != 0) return true;
else return false;
}
private void newData(BluetoothGattCharacteristic characteristic) {
final byte[] data = characteristic.getValue();
if (data != null && data.length > 0) {
//TODO process received data for each reading, in this example variable val is set to some characteristic value, variable now is set to current time
if (characteristic.getUuid().equals(UUID_READING1_CHARACTERISTIC)) {
double val = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 0);
long now = System.currentTimeMillis();
if (reading1.isEnabled()) {
reading1.writeData(now, val);
}
} else if (characteristic.getUuid().equals(UUID_READING2_CHARACTERISTIC)) {
double val = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
long now = System.currentTimeMillis();
if (reading2.isEnabled()) {
reading2.writeData(now, val);
}
}
}
}
private static class ConnectServiceCommandReceiver extends BroadcastReceiver {
private WeakReference<IoToolSensorServiceDemoBLE> w;
public ConnectServiceCommandReceiver(IoToolSensorServiceDemoBLE a) {
w = new WeakReference<IoToolSensorServiceDemoBLE>(a);
}
public void onReceive(Context context, Intent intent) {
if(intent==null || !intent.hasExtra(IoToolConstants.ServiceCommandType))
return;
IoToolSensorServiceDemoBLE a = w.get();
if(a!=null) {
if (intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(IoToolConstants.ServiceCommandStopAllServices) ||
intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(IoToolConstants.ServiceCommandStopService+serviceID)) {
a.stop();
}
else if(intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(IoToolConstants.ServiceCommandSensorsChanged+serviceID)) {
a.setupServiceReadings(intent);
if (a.connectionState == STATE_CONNECTED) a.enableReadings();
}
}
}
}
private void setConnectionState(int newConnectionState) {
NotificationCompat.Builder builder;
connectionState = newConnectionState;
switch (connectionState) {
case STATE_STARTING:
break;
case STATE_DISCONNECTED:
builder = new NotificationCompat.Builder(this);
notification = builder.setContentIntent(contentIntent)
.setSmallIcon(R.drawable.status_disconnected)
.setWhen(System.currentTimeMillis())
.setAutoCancel(true)
.setContentTitle(serviceName)
.setContentText(getResources().getString(R.string.service_device)+BTDeviceAddress+getResources().getString(R.string.service_has_been_disconnected))
.build();
mNM.notify(R.string.foreground_service_id, notification);
break;
case STATE_CONNECTING:
builder = new NotificationCompat.Builder(this);
notification = builder.setContentIntent(contentIntent)
.setSmallIcon(R.drawable.status_connecting)
.setWhen(System.currentTimeMillis())
.setAutoCancel(true)
.setContentTitle(serviceName)
.setContentText(getResources().getString(R.string.service_trying_to_connect_to_device)+BTDeviceAddress+"!")
.build();
mNM.notify(R.string.foreground_service_id, notification);
break;
case STATE_CONNECTED:
builder = new NotificationCompat.Builder(this);
notification = builder.setContentIntent(contentIntent)
.setSmallIcon(R.drawable.status_connected)
.setWhen(System.currentTimeMillis())
.setAutoCancel(true)
.setContentTitle(serviceName)
.setContentText(getResources().getString(R.string.service_device)+BTDeviceAddress+getResources().getString(R.string.service_has_been_reconnected))
.build();
mNM.notify(R.string.foreground_service_id, notification);
break;
case STATE_STOPPING:
builder = new NotificationCompat.Builder(this);
notification = builder.setContentIntent(contentIntent)
.setSmallIcon(R.drawable.status_disconnected)
.setWhen(System.currentTimeMillis())
.setAutoCancel(true)
.setContentTitle(serviceName)
.setContentText(getResources().getString(R.string.service_got_stop_order))
.build();
mNM.notify(R.string.foreground_service_id, notification);
break;
}
}
private void initServiceReadings() {
//TODO initialize all readings
reading1 = new IoToolReading();
reading2 = new IoToolReading();
}
private void setupServiceReadings(Intent intent) {
//TODO setup readings
if (intent.getBooleanExtra(IoToolConstants.Data+"Reading1@"+serviceID, false)) {
reading1.Setup("Reading1@"+serviceID, appDir, useDB, IoToolReading.dataTypeSingle, 0, broadcastData, true, this);
reading1.enable();
} else reading1.disable();
if (intent.getBooleanExtra(IoToolConstants.Data+"Reading2@"+serviceID, false)) {
reading2.Setup("Reading2@"+serviceID, appDir, useDB, IoToolReading.dataTypeSingle, 0, broadcastData, true, this);
reading2.enable();
} else reading2.disable();
}
private void initializeBT() {
BluetoothManager BTManager;
BTManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
if (BTManager != null) {
BTAdapter = BTManager.getAdapter();
if (BTAdapter == null) {
stop();
}
} else {
stop();
}
}
}
"IoToolSensorService" + service ID + "Preferences.java"
This class implements preferences for a sensor which are accessible from IoTool app.
The only indispensable preference for a Bluetooth sensor is its MAC address as the IoTool sensor extension should know from which sensor device it should acquire readings.
Sensor’s MAC address input or selection is already implemented as a preference.
Note: This class may stay unchanged if only selection of MAC address is needed.
But any additional preferences could be added. These are usually related to the functionality of a sensor, i.e. a list of commands which are used to set up the sensor. E.g. preference for reading frequency could be added when a command to change the reading frequency of a sensor exists.
package com.example.servicedemoble;
import java.lang.ref.WeakReference;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.widget.Toast;
import io.senlab.iotool.library.base.DeviceList;
import io.senlab.iotool.library.base.IoToolConstants;
public class IoToolSensorServiceDemoBLEPreferences extends PreferenceActivity {
private PreferenceManager pm;
private static final int DEVICE_LIST_INTENT_CODE = 10001;
private EditTextPreference SensorMAC;
private PreferenceScreen SensorMACChooser;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.servicepreferences);
pm = getPreferenceManager();
initPreferences();
loadSettings();
}
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case IoToolConstants.PERMISSIONS_REQUEST_CODE: {
boolean alert = false;
if (grantResults.length > 0) {
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) alert = true;
}
} else alert = true;
if (alert) {
Toast.makeText(getApplicationContext(), IoToolSensorServiceDemoBLE.serviceID + " " + getString(R.string.request_permission_explanation), Toast.LENGTH_LONG).show();
finish();
}
return;
}
}
}
@Override
public void onResume() {
super.onResume();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
boolean requestPermissions = false;
if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) requestPermissions = true;
if(checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) requestPermissions = true;
if(requestPermissions) {
(new Handler()).postDelayed(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.ACCESS_COARSE_LOCATION}, IoToolConstants.PERMISSIONS_REQUEST_CODE);
}
}
}, IoToolConstants.PERMISSIONS_DIALOG_DISPLAY_TIME_MILLIS);
}
}
}
@Override
public void onBackPressed() {
super.onBackPressed();
saveSettings();
}
@Override
public void onDestroy() {
super.onDestroy();
new SaveSettingsInBackground(this).execute();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case DEVICE_LIST_INTENT_CODE:
if (resultCode == Activity.RESULT_OK) {
if(data.getStringExtra(IoToolConstants.SENSOR_CATEGORY_TYPE).compareTo(IoToolSensorServiceDemoBLE.serviceID)==0) {
SensorMAC.setText(data.getExtras().getString(DeviceList.EXTRA_DEVICE_ADDRESS));
SensorMAC.setSummary(getString(R.string.pref_str_et_sensor_mac_summary, data.getExtras().getString(DeviceList.EXTRA_DEVICE_ADDRESS)));
}
}
break;
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
private void getAllDevices(String sensor_category) {
Intent devIntent = new Intent(this, DeviceList.class);
devIntent.putExtra(IoToolConstants.SENSOR_CATEGORY_TYPE, sensor_category);
startActivityForResult(devIntent, DEVICE_LIST_INTENT_CODE);
}
private void initPreferences() {
SensorMAC = (EditTextPreference) pm.findPreference(getString(R.string.preference_key_mac));
SensorMAC.setOnPreferenceChangeListener(new MACChangeListener(this));
SensorMACChooser = (PreferenceScreen) pm.findPreference(getString(R.string.preference_key_screen_choose_mac_from_list));
SensorMACChooser.setOnPreferenceClickListener(new DeviceListClickListener(this, IoToolSensorServiceDemoBLE.serviceID));
}
private void loadSettings() {
SensorMAC.setSummary(getString(R.string.pref_str_et_sensor_mac_summary, SensorMAC.getText()));
}
private void saveSettings() {
try {
if(SensorMAC.getText().toString().toUpperCase().equals(""))
SensorMAC.setText(getString(R.string.preference_default_mac));
System.gc();
} catch (Exception e) {
e.printStackTrace();
}
}
private static class MACChangeListener implements OnPreferenceChangeListener {
private WeakReference<IoToolSensorServiceDemoBLEPreferences> w;
public MACChangeListener(IoToolSensorServiceDemoBLEPreferences a) {
w = new WeakReference<IoToolSensorServiceDemoBLEPreferences>(a);
}
public boolean onPreferenceChange(Preference preference, Object newValue) {
IoToolSensorServiceDemoBLEPreferences a = w.get();
if(a!=null) {
preference.setSummary(a.getString(R.string.pref_str_set_to, newValue.toString()));
return true;
} else {
return false;
}
}
}
private static class DeviceListClickListener implements OnPreferenceClickListener {
private String sensor_category;
private WeakReference<IoToolSensorServiceDemoBLEPreferences> w;
public DeviceListClickListener(IoToolSensorServiceDemoBLEPreferences a, String sensor_category) {
this.sensor_category = sensor_category;
w = new WeakReference<IoToolSensorServiceDemoBLEPreferences>(a);
}
public boolean onPreferenceClick(Preference preference) {
IoToolSensorServiceDemoBLEPreferences a = w.get();
if(a!=null) {
a.getAllDevices(sensor_category);
return true;
} else {
return false;
}
}
}
private static class SaveSettingsInBackground extends AsyncTask<Void, Void, Void> {
private WeakReference<IoToolSensorServiceDemoBLEPreferences> w;
public SaveSettingsInBackground(IoToolSensorServiceDemoBLEPreferences a) {
w = new WeakReference<IoToolSensorServiceDemoBLEPreferences>(a);
}
protected Void doInBackground(Void... params) {
IoToolSensorServiceDemoBLEPreferences a = w.get();
if(a!=null) {
a.saveSettings();
}
return null;
}
}
}
"IoToolSensorService" + service ID + "Provider.java"
It must provide info for sensor properties, any dashboard profiles, sensor readings properties. For each of those, info is stored in two arrays, one for headers and one for actual data.
PROPERTIES - sensor properties, 1 row only, columns:
-
IoToolConstants.PROVIDER_ROWID - unique row id (integer)
-
IoToolConstants.PROVIDER_PROPERTIES_ID - service ID
-
IoToolConstants.PROVIDER_PROPERTIES_NAME - full sensor Name, may include manufacturer
-
IoToolConstants.PROVIDER_PROPERTIES_DEVELOPER - extension developer name (company)
-
(optional) IoToolConstants.PROVIDER_PROPERTIES_BLUETOOTHMODE - default is IoToolConstants.BLUETOOTH_MODE_NORMAL, for BT Low Energy devices it should be set to IoToolConstants.BLUETOOTH_MODE_BLE
-
(optional) IoToolConstants.PROVIDER_PROPERTIES_PREFIX - when choosing a device (sensor) from a list of all found bluetooth devices in preferences, filtering can be applied to shorten the list (if prefix is set the name of device has to start with it)
PROFILES - provide at least one for easy dashboard setting, with some typical readings, columns:
-
IoToolConstants.PROVIDER_ROWID - unique row id (integer)
-
IoToolConstants.PROVIDER_PROFILES_NAME - profile name
-
IoToolConstants.PROVIDER_PROFILES_READINGS - array of reading IDs
READINGS - reading properties - one for each reading that extension implements (if data for column is set to null, default value is used), columns:
-
IoToolConstants.PROVIDER_ROWID - unique row id (integer)
-
IoToolConstants.PROVIDER_READINGS_ID - reading id
-
IoToolConstants.PROVIDER_READINGS_NAME - reading full name
-
IoToolConstants.PROVIDER_READINGS_SHORTNAME - reading short name (max ~8 characters), for dashboard buttons
-
(optional) IoToolConstants.PROVIDER_READINGS_UNIT - reading unit
-
(optional) IoToolConstants.PROVIDER_READINGS_INTERVAL - approx. reading interval (default 1000ms)
-
(optional) IoToolConstants.PROVIDER_READINGS_DECIMALS - only for display on dashboard buttons (default 0)
-
(optional) IoToolConstants.PROVIDER_READINGS_DISPLAYNUMBER - display value on dashboard button (set to false to hide, e.g. for reading which varies a lot in less than one second)
-
(optional) IoToolConstants.PROVIDER_READINGS_CHARTTYPE - chart display type, default is IoToolConstants.CHART_TYPE_DEFAULT which equals IoToolConstants.CHART_TYPE_LINE, other available options: IoToolConstants.CHART_TYPE_SPLINE, IoToolConstants.CHART_TYPE_COLUMN, IoToolConstants.CHART_TYPE_POINT
-
(optional) IoToolConstants.PROVIDER_READINGS_SETYRANGE - fixed y range on chart (default false)
-
(optional) IoToolConstants.PROVIDER_READINGS_MINYRANGE - chart y range starts with (min value to be displayed)
-
(optional) IoToolConstants.PROVIDER_READINGS_MAXYRANGE - chart y range end with (max value to be displayed)
package com.example.servicedemoble;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.SQLException;
import android.net.Uri;
import io.senlab.iotool.library.base.IoToolConstants;
import io.senlab.iotool.library.base.IoToolFunctions;
public class IoToolSensorServiceDemoBLEProvider extends ContentProvider {
static final String PROVIDER_NAME = "com.example.servicedemoble.IoToolSensorServiceDemoBLEProvider";
//TODO sensor properties
static final String[] properties_header = new String[] {IoToolConstants.PROVIDER_ROWID, IoToolConstants.PROVIDER_PROPERTIES_ID, IoToolConstants.PROVIDER_PROPERTIES_NAME, IoToolConstants.PROVIDER_PROPERTIES_DEVELOPER, IoToolConstants.PROVIDER_PROPERTIES_BLUETOOTHMODE};
static final Object[] properties = new Object[] {1, "DemoBLE", "Demo Bluetooth Low Energy", "Example.com", IoToolConstants.BLUETOOTH_MODE_BLE};
//TODO dashboard profiles
static final String[] profiles_header = new String[] {IoToolConstants.PROVIDER_ROWID, IoToolConstants.PROVIDER_PROFILES_NAME, IoToolConstants.PROVIDER_PROFILES_READINGS};
static final Object[][] profiles = new Object[][] {
{1, "Demo BLE", new String[] {"Reading1@DemoBLE", "Reading2@DemoBLE"}}
};
//TODO readings properties
static final String[] readings_header = new String[] {IoToolConstants.PROVIDER_ROWID, IoToolConstants.PROVIDER_READINGS_ID, IoToolConstants.PROVIDER_READINGS_NAME, IoToolConstants.PROVIDER_READINGS_SHORTNAME, IoToolConstants.PROVIDER_READINGS_UNIT, IoToolConstants.PROVIDER_READINGS_INTERVAL, IoToolConstants.PROVIDER_READINGS_DECIMALS};
static final Object[][] readings = new Object[][] {
{1, "Reading1@DemoBLE", "Reading 1", "Read.1", "unit1", 1000, 2},
{2, "Reading2@DemoBLE", "Reading 2", "Read.2", "unit2", null, null}
};
static final int PROPERTIES = 1;
static final int PROFILES = 2;
static final int READINGS = 3;
static final UriMatcher uriMatcher;
static{
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(PROVIDER_NAME, "properties", PROPERTIES);
uriMatcher.addURI(PROVIDER_NAME, "profiles", PROFILES);
uriMatcher.addURI(PROVIDER_NAME, "readings", READINGS);
}
@Override
public boolean onCreate() {
return true;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new SQLException("Failed to add a record into " + uri);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
MatrixCursor mc = null;
switch (uriMatcher.match(uri)){
case PROPERTIES:
mc = new MatrixCursor(properties_header);
mc.addRow(properties);
break;
case PROFILES:
mc = new MatrixCursor(profiles_header);
for (int i=0; i<profiles.length; i++) mc.addRow(new Object[] {profiles[i][0], profiles[i][1], IoToolFunctions.providerReadingsToString((String[]) profiles[i][2])});
break;
case READINGS:
mc = new MatrixCursor(readings_header);
for (int i=0; i<readings.length; i++) mc.addRow(new Object[] {readings[i][0], readings[i][1], readings[i][2], readings[i][3], readings[i][4], readings[i][5], readings[i][6]});
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
return mc;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)){
case PROPERTIES:
return "vnd.android.cursor.dir/vnd.iotool.properties";
case PROFILES:
return "vnd.android.cursor.dir/vnd.iotool.profiles";
case READINGS:
return "vnd.android.cursor.dir/vnd.iotool.readings";
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
}
}