Nougat has now been released in to the wild and there are a number of new features which developers should be considering. In this loosely-related series of articles we’re going to be looking at various aspects of these new features to see how we can best make use of them. In this article we’re going to look at some optimisations we can make to ensure that our app can function properly following a reboot but before the user has unlocked the device.
Back in the first article in this series about using GCM Network Manager to schedule periodic tasks we discussed how GCM Network Manager has a setting when scheduling jobs which will automatically re-schedule them following a reboot. However, I did not make use of this because there is something of a complication for us. In order to store the service state we use SharedPreferences – without this we could not track whether the Service was running. GCMNetWorkManager does not contain a facility to query whether a specific Service has been scheduled, even if we know the TAG – so we need to track this ourselves. By default SharedPreferences get stored in Credential Encrypted Storage on the device. This means that we cannot retrieve values from these until the user has logged in to the device – by unlocking it. For a messaging service, we don’t necessarily want to wait until the user has unlocked the device following a reboot before we can know whether we should start the MessengerService. OK, our app is using a fake Service, but for a real messaging app we’d need to be able to receive messages as soon as the device has rebooted. Nougat introduces Direct Boot which enables us to get the MessengerService up and running before the user has unlocked the device, and that’s what we’ll be covering in this article.
So lets first discuss what Credential Encrypted Storage actually is. As the name suggests, it is a security mechanism whereby the data is encrypted with keys which are tied to the user’s login credentials. Until the user actually logs in to the device by unlocking it via biometrics, pin code, pattern then the keys to decrypt the data are simply not available. This can become problematic in some cases – consider the example of when the phone randomly re-boots (yes – it does happen!) and the user hasn’t realised. If there are messaging apps which cannot access their storage until the user has logged in following the reboot, then they cannot run and the user does not get notified of incoming messages.
Direct Boot is a mechanism for such apps to store data in Device Encrypted Storage rather than Credential Encrypted Storage. The difference here is that Device Encrypted Storage can be accessed as soon as the device has booted. Device Encrypted Storage is less secure than Credential Encrypted Storage – anything stored in Device Encrypted Storage could be access by all users of the device whereas Credential Encrypted Storage can only be accessed by the specific user whose credentials it is associated with. Therefore it is important to keep any sensitive data in Credential Encrypted Storage, and only use Device Encrypted Storage for the bare minimum required for Direct Boot.
By default app storage goes to Credential Encrypted Storage so how we do we go about using Device Encrypted Storage instead? Actually it’s really easy – we just need to get a Context object which will access Device Encrypted Storage instead. There’s a new method call in API 24 for Context#createDeviceProtectedStorageContext()
and all we need to do is obtain this, and use this Context to retrieve our SharedPreferences. For simplicity we’ll use the ContextCompat implementation which does what we require on Nougat and later but has no effect on earlier versions:
public final class ServiceScheduler { . . . public static ServiceScheduler newInstance(Context context) { Context appContext = context.getApplicationContext(); Context safeContext = ContextCompat.createDeviceProtectedStorageContext(appContext); if (safeContext == null) { safeContext = appContext; } GcmNetworkManager networkManager = GcmNetworkManager.getInstance(safeContext); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(safeContext); boolean isEnabled = sharedPreferences.getBoolean(MESSENGER_ENABLED, true); return new ServiceScheduler(networkManager, sharedPreferences, isEnabled); } . . . }
All we’ve done here is add the highlighted line and we’re now using Device Encrypted Storage for our SharedPreferences.
It’s worth pointing out that it’s easy to have two distinct sets of SharedPreferences within any app – on in Device Encrypted Storage and another in Credential Encrypted Storage. We can therefore store sensitive data in the more secure Credential Encrypted Storage. Also this is not limited to SharedPreferences – any SQLite databases, internal or external storage, or cache storage accessed using this Context will also be Device Encrypted.
The next thing we need is a BroadcastRecevier to handle the boot broadcasts:
public class BootReceiver extends BroadcastReceiver { public BootReceiver() { } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(Intent.ACTION_BOOT_COMPLETED) || action.equals(Intent.ACTION_LOCKED_BOOT_COMPLETED)) { ServiceScheduler serviceScheduler = ServiceScheduler.newInstance(context); if (serviceScheduler.isEnabled()) { serviceScheduler.scheduleService(); } } } }
For anyone that has used the ACTION_BOOT_COMPLETED
broadcast in the past to detect a device boot then this should be straightforward enough. We need to add this to our Manifest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.stylingandroid.nougat"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <application android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> . . . <receiver android:name=".BootReceiver" android:directBootAware="true" android:enabled="true" tools:ignore="UnusedAttribute"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" /> </intent-filter> </receiver> </application> </manifest>
Once again this is almost identical to standard BOOT_COMPLETED
handling – we need the appropriate permission, and we specify the required actions in the IntentFilter for the Receiver. However we also need to add the directBootAware="true"
attribute and the LOCKED_BOOT_COMPLETED
action to get the broadcast as soon as the device has booted.
So here we’ve registered for both – this is to provide legacy support for devices which don’t support Direct Boot. It doesn’t matter if we try and schedule the Service twice – we already added protections against that.
That’s pretty much it. Once the device finishes booting on a Nougat or later device we’ll get woken following a boot, and we can access our SharedPreferences which are now stored in Device Encrypted Storage.
In the next article we’ll turn our attention to Notifications and how these have changed in Nougat.
The source code for this article is available here.
© 2016, Mark Allison. All rights reserved.
Copyright © 2016 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
1 Comment