====== GiraffPlus on Android ======
Make sure you’ve obtained the archive interfaces.middleware.zip (it is in the Android\giraff.android folder from the svn checkout). This file contains the AIDL interface needed to communicate with the middleware. In order to setup your application you need to extract this file to the src/ directory of the project.
===== GiraffPlus packages ====
Middleware:
* https://www.dropbox.com/s/zfcqrvcqtlz934v/gp-communicationconnector-1.0.2.apk (last update on 15-May-2014)
* https://www.dropbox.com/s/pwet4ise2ta3k46/gp-middleware-1.0.2.apk (last update on 15-May-2014)
Previous versions:
* https://www.dropbox.com/s/gmnlbcx3gb2gn6v/communicationconnector.mqtt.apk (last update on 06-Feb-2014 15.30)
* https://www.dropbox.com/s/8wh3o1km1g61j0i/middleware.apk (last update on 06-Feb-2014 15.30)
===== One-time setup =====
Applications that use this infrastructure require that the middleware application (Android/giraff.android/middleware folder from the svn checkout) and at least one communication connector application (there is only one by now in the communicationconnector.mqtt Android/giraff.android/ folder from the svn checkout) are previously installed to the device.
Also, the middleware needs to be configured **before** using any of the clients applications, this can be done using the middleware launcher icon. The configuration includes the mandatory parameters:
* URI of the MQTT broker ("ssl://xxx.yyy.zzz.www");
* Trust keystore password
* Client keystore password
* Path to the trust keystore file (bks format)
* Path to the client keystore file (bks format)
**Notice.** In order to convert a jks keystore to a bks keystore:
- Download the utility Portecle (http://portecle.sourceforge.net/)
- Open the *.jks keystore
- Select //Tools// -> //Change Keystore Type// -> //BKS//
- Enter the password if asked and save the newly created bks keystore. If during the process an exception like “Illegal key size or default parameters” occurs, the Java Cryptography Extension (JCE) unlimited strength jurisdiction policy files has to be installed. In case, follow this steps:
* On the Oracle Java Downloads webpage (http://www.oracle.com/technetwork/java/javase/downloads/index.html), at the bottom of the page under “Additional Resources” download the JCE package.
* Unzip the archive and copy the local_policy.jar and US_export_policy.jar files to the $JAVA_HOME/jre/lib/security overwriting those already present.
* Restart Portecle
Make sure to kill the "middleware" Android process after further changes. Note that these parameters may change in the future. Refer to the [[middleware|wiki]] for more information on the Giraff+ system.
===== Service Binding =====
The middleware is implemented as an Android’s bound service that can be activated with the action **"it.cnr.isti.giraff.android.BIND_TO_MIDDLEWARE"**.
===== Interface API =====
Files provided in the archive come with in-code documentation.
===== Implementing a GiraffPlus Android Application =====
Follows a quick tutorial that explains step-by-step how to implement a full working example application (source code can be found in the [[https://giraff.xlab.si/svn/giraff/trunk/android/giraff.android/sensorsservice | folder]] from the svn checkout). This application probes the sensors of the device and announces the ones it finds, then starts sending the gathered values to the middleware.service.
Note that all you need to know is the documentation of the interface, everything else follows the standard Android’s **AIDL** practices so you can refer the [[http://developer.android.com/guide/components/aidl.html|official documentation]].
==== The MainActivity, SensorsService, and SensorsDescriptor Classes ====
The example application is composed of a main activity that launches a service in charge of listening on sensors changes:
public class MainActivity extends Activity implements OnCheckedChangeListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ToggleButton startstop = (ToggleButton) findViewById(R.id.startstop);
// order matters to avoid unwanted onCheckedChanged
startstop.setChecked(SensorsService.isEnabled());
startstop.setOnCheckedChangeListener(this);
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// start
if (isChecked) {
startService(new Intent(this, SensorsService.class));
}
// stop
else {
stopService(new Intent(this, SensorsService.class));
}
}
}
A ToggleButton starts or stops the service:
{{:giraffplus:screenshot_2013-07-24-15-11-27.png?direct&300|}} {{:giraffplus:screenshot_2013-07-24-15-30-53.png?direct&300|}}
The SensorsService class initializes the accelerometer sensor (creating a descriptor by means of android Bundle done in the SensorsDescriptors class), then calls methods of the middleware to announce it and publishes data to the context bus. To do this the SensorsService follows these steps:
- Import middleware classes
- Implement a service connection
- Implement the callback logic (optional)
- Implement an error callback (optional)
- Request a connection to the middleware
- Call methods of the middleware interface
==== Import classes ====
----
Make sure to add the following import:
import it.cnr.isti.giraff.android.interfaces.middleware.IMiddleware;
==== Implement a service connection ====
----
In order to request the binding to the service you need to implement a **ServiceConnection** object that manages the connection to the middleware:
private IMiddleware middleware;
private SensorsDescriptors sensorsDescriptors;
private ServiceConnection middlewareConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder binder) {
middleware = IMiddleware.Stub.asInterface(binder);
// enter foreground state
startForeground(1, notificationBuilder.build());
// announce all descriptors and register sensor listener
sensorsDescriptors.initialize(middleware);
}
@Override
public void onServiceDisconnected(ComponentName className) {
middleware = null;
// unregister sensor listener and remove all descriptors
sensorsDescriptors.finalize();
// exit foreground state
stopForeground(true);
}
};
==== Implement the callback logic ====
----
Some methods requires a listener that need to be implemented on the service application:
private IMiddlewareCallback.Stub callback = new IMiddlewareCallback.Stub() {
@Override
public void serviceFound(Bundle descriptor) throws RemoteException {
// ...
}
@Override
public void serviceRemoved(Bundle descriptor) throws RemoteException {
// ...
}
@Override
public void serviceChanged(Bundle descriptor) throws RemoteException {
// ...
}
@Override
public void messageReceived(String topic, String payload) throws RemoteException {
// ...
}
};
==== Implement an error callback ====
----
Each method in the API takes an optional (can be null) resultListener parameter used to asynchronously report successes or errors:
private IMiddlewareResultCallback.Stub resultCallback = new IMiddlewareResultCallback.Stub() {
@Override
public void error(Bundle info) throws RemoteException {
// ...
}
@Override
public void success () throws RemoteException {
// ...
}
};
==== Request a connection to the middleware ====
----
Your application must request a connection to the middleware by using an intent with a specific action string and passing the previously created **ServiceConnection** object:
Intent intent = new Intent("it.cnr.isti.giraff.android.BIND_TO_MIDDLEWARE");
bindService(intent, middlewareConnection, Context.BIND_AUTO_CREATE);
Make sure to unbind from the middleware when you’re done, for example in the **onDestroy** method of your main activity:
@Override
protected void onDestroy() {
super.onDestroy();
if (middlewareConnection != null) {
unbindService(middlewareConnection);
}
}
==== Call methods ====
----
Once you have the **IMiddleware** object you can call remote methods on it, for example for announcing sensors on the sensor bus or to publish data on the context bus.
=== Announce ===
In the onCreate() method of the SensorsService class we create the SensorsDescriptors class:
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate()");
// foreground notification builder
Intent intent = new Intent(SensorsService.this, MainActivity.class);
notificationBuilder = new NotificationCompat.Builder(SensorsService.this);
notificationBuilder.setSmallIcon(R.drawable.icon);
notificationBuilder.setContentTitle(getResources().getText(R.string.app_name));
notificationBuilder.setContentText(getResources().getText(R.string.foreground_message));
notificationBuilder.setContentIntent(PendingIntent.getActivity(SensorsService.this, -1, intent, 0));
sensorsDescriptors = new SensorsDescriptors(this);
}
We called the method sensorsDescriptors.initialize(middleware) in the SensorsService class when we implemented the service connection. This method calls the method to create the sensor descriptors, announces them and then register a listener to be notified about the sensor changes:
public void initialize(IMiddleware middleware) {
this.middleware = middleware;
try {
createSensorDescriptors ();
announce();
register();
} catch (RemoteException e) {
Log.e(TAG, "cannot announce");
}
}
The createSensorDescriptors() creates a descriptor for the accelerometer sensor present on the device using the **id** taken directly from the database (for sake of consistency with the GiraffPlus ecosystem we choose to publish data on the subtree **sensor/** of the context bus, please respect this constraint):
private void createSensorDescriptors () throws RemoteException {
Sensor sensor;
try {
// Gets all the local sensors in a JSON array
JSONArray localSensors = new JSONArray (middleware.getSensorsByManufacturer ("ISTI-CNR"));
// for each desirable sensor
for (int i = 0; i < sensors.size(); i++)
{
int sensorCode = sensors.keyAt (i);
String sensorType = sensors.get (sensorCode);
// probe sensor and add to the list
sensor = sensorManager.getDefaultSensor (sensorCode);
if (sensor != null) {
JSONObject sensDesc = null;
Log.d(TAG, "found: " + sensorType);
for (int j = 0; j < localSensors.length (); j++)
if (((String)((JSONObject) localSensors.get (j)).get ("type")).toString ().equals (sensorType)) {
sensDesc = (JSONObject) localSensors.get (j);
break;
}
if (sensDesc != null) { // The sensor descriptor exists in the database
Bundle serviceDescriptor = new Bundle ();
serviceDescriptor.putString ("serviceBusTopic", "sensor/" + sensDesc.getString ("id"));
serviceDescriptor.putString ("id", sensDesc.getString ("id"));
serviceDescriptor.putString ("type", sensDesc.getString ("type"));
serviceDescriptor.putString ("category", "sensor");
serviceDescriptor.putString ("contextBusTopic", "sensor/" + sensDesc.getString ("id"));
serviceDescriptor.putString ("description", sensor.toString());
JSONArray params = sensDesc.getJSONArray ("messageFormat");
Bundle[] messageFormat = new Bundle[params.length ()];
for (int j = 0; j < params.length (); j++) {
messageFormat[j] = new Bundle ();
messageFormat[j].putString ("name", ((JSONObject) params.get (j)).getString ("name"));
messageFormat[j].putString ("unit", ((JSONObject) params.get (j)).getString ("unit"));
}
serviceDescriptor.putParcelableArray ("messageFormat", messageFormat);
serviceDescriptor.putParcelableArray ("recipient", new Bundle[] {});
serviceDescriptors.put(sensorCode, new Pair(sensor, serviceDescriptor));
}
}
}
}
catch (JSONException ex) {
Log.e (TAG, ex.getMessage ());
}
}
private void announce(IMiddleware middleware) throws RemoteException {
this.middleware = middleware;
for (int i = 0; i < SERVICE_DESCRIPTORS.size(); i++) {
Bundle serviceDescriptor = SERVICE_DESCRIPTORS.get(SERVICE_DESCRIPTORS.keyAt(i)).second;
// CALL TO THE MIDDLEWARE API - ANNOUNCE
middleware.announce(serviceDescriptor, new IMiddlewareResultCallback.Stub()
{
@Override
public void success () throws RemoteException {
Log.d (TAG, "Announce successful");
}
@Override
public void error (Bundle info) throws RemoteException {
Log.e (TAG, "Announce failed");
}
});
}
}
private void register() {
for (int i = 0; i < SERVICE_DESCRIPTORS.size(); i++) {
Sensor sensor = SERVICE_DESCRIPTORS.get(SERVICE_DESCRIPTORS.keyAt(i)).first;
sensorManager.registerListener(this, sensor, SENSORS_DELAY);
}
}
=== Publish ===
Once we have announced the sensors on the service bus we can begin to publish sensed data. We call the publish method of the middleware each time a sensor change is detected:
public void onSensorChanged(SensorEvent event) {
Bundle serviceDescriptor = SERVICE_DESCRIPTORS.get(event.sensor.getType()).second;
String topic = serviceDescriptor.getString("contextBusTopic");
// build payload
JSONObject payload = new JSONObject();
try {
payload.put("sensor_id", serviceDescriptor.getString("id"));
payload.put("timestamp", event.timestamp);
JSONObject values = new JSONObject();
values.put("Acc_X", event.values[0]);
values.put("Acc_Y", event.values[1]);
values.put("Acc_z", event.values[2]);
payload.put("values", values);
} catch (JSONException e) {
return;
}
// publish message
try {
middleware.publish(topic, payload.toString(), false, resultCallbackImpl);
} catch (RemoteException e) {
Log.e(TAG, "cannot publish");
}
}
=== Remove ===
When we close our application it is mandatory to remove announced sensors from the service bus (for sake of consistency in the GiraffPlus ecosystem). This is done when we disconnect from service in the onServiceDisconnected method, when we call the onServiceDisconnected method:
public void finalize() {
try {
remove();
unregister();
} catch (RemoteException e) {
Log.e(TAG, "cannot remove");
}
}
private void remove() throws RemoteException {
for (int i = 0; i < SERVICE_DESCRIPTORS.size(); i++) {
Bundle serviceDescriptor = SERVICE_DESCRIPTORS.get(SERVICE_DESCRIPTORS.keyAt(i)).second;
middleware.remove(serviceDescriptor, new IMiddlewareResultCallback.Stub()
{
@Override
public void success () throws RemoteException {
Log.d (TAG, "Remove successful");
}
@Override
public void error (Bundle info) throws RemoteException {
Log.e (TAG, "Remove failed");
}
});
}
}
private void unregister() {
sensorManager.unregisterListener(this);
}