Two hectic months have passed since last post, in which we focused on the Android app we had prepared for a data laboratory for wearables. (The intention is still to do a small scale experiment with a number of wristbands, by the way.) However, while trying to tune the Android app, we realised there was still some work to be done to make it robust -i.e. the app must keep on capturing data and sending it upstream whatever happens in the smartphone, and regardless of how long it is since it was started. We figured we'd write an additional post, just focusing on what to do to make the app more robust, also hoping that the hectic months will pass soon and we'll get some time to analyse the data (and post about it maybe).
As usual, this post includes code snippets and links to the full solution (available in this github project).
Every Android developer must read and understand the details of this link about the process lifecycle. When we wrote our last post, we had actually made a naive mistake: we connected the wristband at the Activity level; thus it was the activity which was doing the following tasks:
That would have been not such a bad decision if the user would always had the app open while the data was being captured. However, in the scenario for our experiment, and actually in any scenario closer to real life, the user disregards the app completely and just wears the wristband and the smartphone while they are doing any other thing. (Note anyhow that in any real life scenario, the app would not upstream all the data, but only a pre-processed summary).
Anyway, even for the lab, we want the users to forget about the app. What's the problem then? For whatever the reason (e.g. if the smartphone is running out of resources) Android might decide to kill any Activity that the user is not looking at. Again from the link about the process lifecycle, a foreground process is one that is required for what the user is currently doing and they are not killed except as a last resort if memory is so low that not even these processes can continue to run. On the other hand, if a foreground process stops being what the user is currently doing, e.g. because it is not in the foreground anymore, it will be killed when resources are needed -in practice, depending on the smartphone of course, this means it will not stay alive forever.
Conceptually, capturing data from the wristband would belong to a service, since these processes are not directly visible to the user, they are generally doing things that the user cares about (such as background mp3 playback or background network data upload or download), so the system will always keep such processes running unless there is not enough memory to retain all foreground and visible process. Again from the explanations in the link about the process lifecycle, it is not easy at all to predict which processes will be killed first, so we experimented with it, and we came to the conclusion that, if the app is not the foreground, the services end up lasting a longer time; more importantly, if an Activity is killed, it might not get resumed automatically if it is not in the foreground, while for the services, even if they get killed eventually, Android does re-start them later, since services are generally in the background doing things that the user cares about.
Thus the strategy for the app became:
Let's review the integration points between each of the objects and how each of them behaves when Android restarts them.
The ScannerActivity (full code here) is the entry point for the app, it allows that the user selects the wristband to connect to, and once the connection is established, it:
Find below the relevant code for these two actions.
@Override public void connected() { connectDialog.dismiss(); Log.i(TAG, "start cache service 1st time"); Intent cacheServiceIntent = new Intent(ScannerActivity.this, CacheService.class); cacheServiceIntent.putExtra(CacheService.EXTRA_BT_DEVICE, btDevice); ScannerActivity.this.startService(cacheServiceIntent); Log.i(TAG, String.format("MAC Address: %s", mwBoard.getMacAddress())); Intent streamingActivityIntent = new Intent(ScannerActivity.this, StreamingActivity.class); streamingActivityIntent.putExtra(StreamingActivity.EXTRA_BT_DEVICE, btDevice); ScannerActivity.this.startActivity(streamingActivityIntent); }
Since this ScannerActivity will not be used again unless the wristband is disconnected, no worries for the Android restart.
The StreamingActivity (full code here) is bundled to the wristband device connection, thus if the activity is restarted it will invoke onServiceConnected,
@Override public void onServiceConnected(ComponentName name, IBinder service) { Log.i(TAG, "onServiceConnected"); ///< Typecast the binder to the service's LocalBinder class mwBoard = ((MetaWearBleService.LocalBinder) service).getMetaWearBoard(btDevice); if (! mwBoard.isConnected()) { // start cache service Log.i(TAG, "Restarting CacheService"); Intent cacheServiceIntent = new Intent(this, CacheService.class); cacheServiceIntent.putExtra(CacheService.EXTRA_BT_DEVICE, btDevice); startService(cacheServiceIntent); } }
If the activity has been restarted, the connection to the wristband must be set (if it was not set, the user would see the ScannerActivity instead). Thus, if the connection is off and the StreamingActivity has been restarted, the CacheService is restarted.
This service (full code here):
First, when the service is (re-)started, the intent may or may not be present:
@Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "startCommand"); int out = super.onStartCommand(intent, flags, startId); if (intent == null) { Log.e(TAG, "started with intent == null ????"); return START_REDELIVER_INTENT; } else { btDevice = intent.getParcelableExtra(EXTRA_BT_DEVICE); } bindService(new Intent(this, MetaWearBleService.class), this, BIND_AUTO_CREATE); return out; }
Second, when the service is bundled with the device, it may be that the bluetooth device is not present (in which case there's nothing to do and the service stops itself, so that Android re-starts it properly), or that the connection is not active, and anyhow the streaming must resume:
@Override public void onServiceConnected(ComponentName name, IBinder service) { Log.i(TAG, "onServiceConnected"); index = 1; ///< Typecast the binder to the service's LocalBinder class if (btDevice != null) { mwBoard = ((MetaWearBleService.LocalBinder) service).getMetaWearBoard(btDevice); mwBoard.setConnectionStateHandler(stateHandler); if (! mwBoard.isConnected()) { Log.d(TAG, "Reconnect device (only after restart?)"); mwBoard.connect(); } if (accelModule == null) { Log.d(TAG, "Get accelModule"); getAccelModule(); } Log.d(TAG, "(Re)start streaming"); startStreaming(); } else { this.stopSelf(); } }
Third, the singleton does not actually behave like a usual singleton... since, once again, Android might have killed it and the service might get a different instance of it at any time.
private AccDataCacheSingleton getCache() { AccDataCacheSingleton newCache = AccDataCacheSingleton.getInstance(this.getBaseContext()); if (cache != null && newCache.getHeaderId() != cache.getHeaderId()) { Log.d(TAG, "A new instance of the singleton has been created?"); cache.setStarted(false); } cache = newCache; return cache; }
This singleton object (full code here) takes care of the cache of data, and creates the UpstreamService
public void start(String macAddress) { Log.d(TAG, "Start MAC " + macAddress + " and create new header/service"); ... // service for uploading to server Intent serviceIntent = new Intent(context, UpstreamService.class); context.startService(serviceIntent); }
And finally this service (full code here) needs to take care of the situations in which it is re-started but the rest of the objects do not exist (actually, it only needs to worry about its creator, the Cache Singleton):
private AccDataCacheSingleton getCache() { AccDataCacheSingleton newCache = AccDataCacheSingleton.getInstance(); if (newCache == null) { Log.d(TAG, "Activity has been destroyed"); this.stopSelf(); return cache; } if (cache != null && newCache.getHeaderId() != cache.getHeaderId()) { Log.d(TAG, "A new instance of the service has been created"); this.stopSelf(); } cache = newCache; return cache; }
Hopefully in this quick review you get the message that any object of the app may be killed or re-started in any order, and that every object needs to be robust enough so that it deals with the state of the rest of the objects. One last resort it that the object can kill itself, so that Android re-starts it and, hopefully, whatever was missing is present in the next start.