Android N - Direct Boot
Android N – Direct Boot

Android N – Direct Boot

Android 7.0 named as Nougat has come up and it has brought with it, abundant exciting features to look for, like the following :

  • Notification enhancements includes Direct Reply, Bundled notifications, Custom Views, Message style customization, Template updates.
  • Direct Boot
  • Multi Window
  • Doze on the Go…
  • Vulkan API
  • Project Svelte: Background Optimizations
  • Quick Settings Tile and many more

In this post

Today in this post, we will discuss about Direct Boot and how we can leverage the benefit of Direct boot functionality. We shall also look into the simple example of setting alarm. As you all know, after setting the alarms, if the device is rebooted, the alarms won’t be preserved. You will have to set the alarms again after device is rebooted. Later in this post, we will see how we can reset the alarms during Direct Boot mode.

What is Direct Boot ?

At times, we wonder how an app should handle any process after a reboot and before user has unlocked the device. This situation occurs during Direct Boot mode in Android 7.0. By default, Android 7.0 runs in Direct Boot mode. Processes like setting alarms, setting reminders, sending messages or any other process that needs to be continued even after the device is rebooted has to be taken care of even in Direct Boot mode.

Get Started

First of all, let’s know how we can store the necessary data in different types of storage so that we can fetch them while the device is rebooting.
There are two types of storage :

  • Credential Encrypted Storage : The default storage for a device is Credential Encrypted Storage. As the name suggests, this type of storage is accessible only through user’s credentials like biometrics, pin code or any pattern thus making it more secure than Device Encrypted Storage.
  • Device Encrypted Storage : This type of storage is accessible only when the device is booted. Being less secure, it is advised to not store sensitive data in here. This type of storage is used during Direct Boot mode.

In order to access device encrypted storage through any component, that component needs to be registered as encryption aware. For doing so, set the attribute named android:directBootAware to true.
When the device is restarted, a broadcast message is sent by the system to the registered component with the action ACTION_LOCKED_BOOT_COMPLETED. The following is an example of registering a broadcast receiver as an encryption aware component :

<receiver android:name=".receiver.BootBroadcastReceiver"
   android:directBootAware="true">
   <intent-filter>
       <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
       <action android:name="android.intent.action.BOOT_COMPLETED" />
   </intent-filter>
</receiver>

 

As you can see in the above code snippet, ACTION_BOOT_COMPLETED is also registered. Now, this is for pre N devices.

Accessing Device Encrypted Storage

Uptil now, we had been using application context which refers to credential encrypted storage. So, how to get the context that refers to device encrypted storage ??
Don’t worry, its easy. We have a method called createDeviceProtectedStorageContext() which returns context to access device encrypted storage.

if (BuildCompat.isAtLeastN()) {
   final Context deviceContext = Context.createDeviceProtectedStorageContext();
   storageContext = deviceContext;
}

 

So, as you can see in the above code snippet, we can get the context to access device encrypted storage through Context. One more interesting thing you might have noticed is that verification of version SDK is done through a method called isAtleastN() which is provided by the class BuildCompat. This method checks whether the version SDK is N or more than that.

When to use appropriate storage type

When to use device encrypted storage and when to use credential encrypted storage is essential to know. As soon as the device restarts and user unlocks the device, private data of the user comes into the picture. Thus, at this point of time, we need to use credential encrypted storage. After the device is booted and before the user has unlocked the device, device encrypted storage must be used.

In order to get notified when the user unlocks the device, you need to register a broadcast receiver. If you need to show notifications as soon as user unlocked the device i.e. in foreground, then you need to listen for ACTION_USER_UNLOCKED message. However, if you need to notify the user through background process i.e. after the device is booted, then you need to listen for ACTION_BOOT_COMPLETED message.

If you need to check whether the user has unlocked the device, you can do so by calling UserManager.isUserUnlocked().

After you have decided when you should use which type of storage, let us take a look at how to migrate the data from one storage to another.

You need to know when you should migrate the data from one storage to another. You can make use of the following methods :

  • Context.moveSharedPreferencesFrom() : Migrate data from preferences between device encrypted storage and credential encrypted storage.
    Context.moveDatabaseFrom() : Migrate data from database between device encrypted storage and credential encrypted storage.

Let’s Code

lets_code

Alright then, let’s implement whatever we have gone through till now.
We shall take up a simple example of setting alarm. Now, as you all know, after setting the alarms, if the device is rebooted, the alarms won’t be preserved. You will have to set the alarms again after device is rebooted. So here, I have used local database to store the alarms.

First of all, let’s create a broadcast receiver which will help us to receive callbacks when the device is rebooted.

Creating a Broadcast Receiver

// Broadcast receiver to listen for the action ACTION_LOCKED_BOOT_COMPLETED and ACTION_BOOT_COMPLETED
public class BootBroadcastReceiver extends BroadcastReceiver {


   private static final String TAG = "BootBroadcastReceiver";


   @Override
   public void onReceive(Context context, Intent intent) {
       String action = intent.getAction();


       Log.i(TAG, "Action Received: " + action + ", user unlocked: " + UserManagerCompat
               .isUserUnlocked(context));


       if (BuildCompat.isAtLeastN()) {
           if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
               if (Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) {
                   setAlarms(context);
               }
           }
       } else {
           if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
               setAlarms(context);
           }
       }
   }


   private void setAlarms(Context context) {
       UtilAlarm util = new UtilAlarm(context);
       //DbAlarmStorage is a helper class managing Alarm table operations, checkout the example code for full code
       DbAlarmStorage dbAlarmStorage = new DbAlarmStorage(context);
       for (Alarm alarm : dbAlarmStorage.getAlarms()) {
           util.setAlarm(alarm);
       }
   }
}

 

As you can see in the above code snippet, we set the alarms as soon as we get the callback of ACTION_LOCKED_BOOT_COMPLETED and ACTION_BOOT_COMPLETED.

Registering the broadcast receiver

<receiver android:name=".receiver.BootBroadcastReceiver"
   android:exported="false"
   android:directBootAware="true">
   <!-- BOOT_COMPLETED action for pre-N devices -->
   <intent-filter>
       <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
       <action android:name="android.intent.action.BOOT_COMPLETED" />
   </intent-filter>
</receiver>

 

Next, you need to create a database with a table named tblAlarm which will contain fields like id, month, date, hour and minute. After creating it, let’s create a class to implement all the methods which will help us to perform operations for alarm.

public DbAlarmImpl(Context context) {
   DatabaseHelper databasehelper = DatabaseHelper.getInstance(context);
   database = databasehelper.getWritableDatabase();
}


public void saveAlarm(Alarm alarm) {
   database.insert(DatabaseHelper.TABLE_ALARM, null, getContentValues(alarm));
}


public void deleteAlarm(Alarm alarmToBeDeleted) {


   String rawQuery = "Delete from " + DatabaseHelper.TABLE_ALARM + " where " + DatabaseHelper.KEY_ALARM_ID +
           " = " + alarmToBeDeleted.getId();


   Cursor cursor = database.rawQuery(rawQuery, null);
   if (cursor.getCount() > 0) {
       Log.i("alarm", "deleted");
       cursor.close();
   }
}


public List<Alarm> getAllAlarms() {


   List<Alarm> alarmList = new ArrayList<>();


   String rawQuery = "Select * from " + DatabaseHelper.TABLE_ALARM;


   Cursor cursor = database.rawQuery(rawQuery, null);


   if (cursor.getCount() > 0) {
       cursor.moveToFirst();
       do {
           Alarm alarm = new Alarm();
           alarm.setId(cursor.getInt(cursor.getColumnIndex(DatabaseHelper.KEY_ALARM_ID)));
           alarm.setMinute(cursor.getInt(cursor.getColumnIndex(DatabaseHelper.KEY_MINUTE)));
           alarm.setHour(cursor.getInt(cursor.getColumnIndex(DatabaseHelper.KEY_HOUR)));
           alarm.setMonth(cursor.getInt(cursor.getColumnIndex(DatabaseHelper.KEY_MONTH)));
           alarm.setDate(cursor.getInt(cursor.getColumnIndex(DatabaseHelper.KEY_DATE)));
           alarmList.add(alarm);
       } while (cursor.moveToNext());
       cursor.close();
   } else {
       Log.d(LOG_TAG, "No alarm found");
   }
   return alarmList;
}

 

Now, we need to manage a lot of stuff like saving alarms, migrating the database from one type of storage to another and much more. So, for that, we shall create a class named DbAlarmStorage which will be as follows :

public class DbAlarmStorage {


   private static final String TAG = DbAlarmStorage.class.getSimpleName();
   private DbAlarmImpl dbAlarmImpl;


   public DbAlarmStorage(Context context) {


        // context which will hold the appropriate storage context
       Context storageContext = null;


       // if device is running Android N or newer version, move shared preferences to Device Encrypted Storage,
       // else keep them in Credential Encrypted Storage


    //  move database between the two types of storage 
   // after you run this demo, you will be able to find the logs related to the migration of database from one storage to 
  // another . This operation is carried out when you set the alarms in the beginning of the application and also after
  //  device is rebooted


       if (BuildCompat.isAtLeastN()) {
           final Context deviceContext;
           if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
               deviceContext = context.createDeviceProtectedStorageContext();
               if (!deviceContext.moveDatabaseFrom(context,
                       DatabaseHelper.DATABASE_NAME)) {
                   Log.e(TAG, "Failed to migrate database");
               }
               storageContext = deviceContext;
           }
       } else {
           storageContext = context;
       }
       if (storageContext != null) {
           dbAlarmImpl = new DbAlarmImpl(storageContext);
       }
   }


   // save alarm
   public Alarm saveAlarm(int month, int date, int hour, int minute) {
       Alarm alarm = new Alarm();
       alarm.setId(new Random().nextInt());
       alarm.setMinute(minute);
       alarm.setHour(hour);
       alarm.setMonth(month);
       alarm.setDate(date);

       dbAlarmImpl.saveAlarm(alarm);
       return alarm;
   }
  
   // get the list of alarms
   public Set<Alarm> getAlarms() {
       Set<Alarm> alarms = new HashSet<>();
       for (Alarm alarm : dbAlarmImpl.getAllAlarms()) {
           alarms.add(alarm);
       }
       return alarms;
   }

   public void deleteAlarm(Alarm alarm) {
       dbAlarmImpl.deleteAlarm(alarm);
   }
}

 

The following class will help to set an alarm and pass the required parameters to the service which will send the notification to the user as soon as the alarm goes off.

// Utils to perform operations on Alarm
public class UtilAlarm {


   private static final String TAG = "UtilAlarm";
   private final Context mContext;
   private final AlarmManager mAlarmManager;


   public UtilAlarm(Context context) {
       mContext = context;
       mAlarmManager = mContext.getSystemService(AlarmManager.class);
   }


   // schedule an alarm
   public void setAlarm(Alarm alarm) {
       Intent intent = new Intent(mContext, AlarmIntentService.class);


       intent.putExtra(AlarmIntentService.ID, alarm.getId());
       intent.putExtra(AlarmIntentService.MINUTE, alarm.getMinute());
       intent.putExtra(AlarmIntentService.HOUR, alarm.getHour());
       intent.putExtra(AlarmIntentService.DATE, alarm.getDate());
       intent.putExtra(AlarmIntentService.MONTH, alarm.getMonth());


       PendingIntent pendingIntent = PendingIntent
               .getService(mContext, alarm.getId(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
       Calendar alarmTime = Calendar.getInstance();
       alarmTime.set(Calendar.MONTH, alarm.getMonth());
       alarmTime.set(Calendar.DATE, alarm.getDate());
       alarmTime.set(Calendar.HOUR_OF_DAY, alarm.getHour());
       alarmTime.set(Calendar.MINUTE, alarm.getMinute());


       AlarmManager.AlarmClockInfo alarmClockInfo;
       alarmClockInfo = new AlarmManager.AlarmClockInfo(
               alarmTime.getTimeInMillis(),
               pendingIntent);
       mAlarmManager.setAlarmClock(alarmClockInfo, pendingIntent);


       Log.i(TAG, "Alarm set");
   }
}

 

After creating all the required methods and classed, let us create a service which will send a notification to the user when the alarm goes off.

Creating an Intent Service

 

public class AlarmIntentService extends IntentService {


   public AlarmIntentService() {
       super(AlarmIntentService.class.getName());
   }


   @Override
   protected void onHandleIntent(Intent intent) {
       Context context = getApplicationContext();


       Alarm alarm = new Alarm();
       Bundle bundle = intent.getExtras();
       alarm.setId(bundle.getInt(ID));
       alarm.setMinute(bundle.getInt(MINUTE));
       alarm.setHour(bundle.getInt(HOUR));
       alarm.setDate(bundle.getInt(DATE));
       alarm.setMonth(bundle.getInt(MONTH));


       NotificationManager notificationManager;
       notificationManager = getSystemService(NotificationManager.class);


       NotificationCompat.Builder builder =
               new NotificationCompat.Builder(context)
                       .setCategory(NotificationCompat.CATEGORY_ALARM)
                       .setSmallIcon(R.drawable.ic_notification)
                       .setSound(Settings.System.DEFAULT_ALARM_ALERT_URI)
                       .setPriority(Notification.PRIORITY_MAX)
                       .setContentTitle(context.getString(R.string.alarm));
       notificationManager.notify(alarm.getId(), builder.build());


       // delete the alarm once it goes off
       DbAlarmStorage dbAlarmStorage = new DbAlarmStorage(context);
       dbAlarmStorage.deleteAlarm(alarm);
   }
}

 

Now, we have reached the final stage.
You can take a TimePicker widget or TimePicker Dialog for the user to set an alarm.

The following is the layout for the main activity :

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/activity_main"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:gravity="center"
   tools:context="com.android.directbootdemo.MainActivity">


   <TimePicker
       android:id="@+id/timePicker"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content" />


   <android.support.v7.widget.AppCompatButton
       android:id="@+id/buttonAddAlarm"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_below="@+id/timePicker"
       android:layout_marginTop="20dp"
       android:text="@string/str_set_alarm" />


</RelativeLayout>

 

The following is the code for MainActivity :

public class MainActivity extends AppCompatActivity {


   @BindView(R.id.timePicker)
   TimePicker mTimePicker;


   @OnClick(R.id.buttonAddAlarm)
   void setAlarm() {


       DbAlarmStorage dbAlarmStorage = new DbAlarmStorage(this);
       Alarm alarm;


       // save alarm in database
       alarm = dbAlarmStorage.saveAlarm(Calendar.getInstance().get(Calendar.MONTH),           Calendar.getInstance().get(Calendar.DATE), mTimePicker.getHour(), mTimePicker.getMinute());
       UtilAlarm utilAlarm = new UtilAlarm(this);
       // set alarm
       utilAlarm.setAlarm(alarm);
       Toast.makeText(this, "Alarm set", Toast.LENGTH_SHORT).show();
   }


   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       ButterKnife.bind(this);
   }
}

 

Conclusion

So, this was a piece of Android Nougat for you 🙂 There is much more to know about.
I hope you have grasped some aspects worth knowing from this blog. I wish to deliver more and more about the unending innovations in Android. Meanwhile I will come up with next piece of android thing, show your support/love by sharing this article 🙂

To implement Direct Boot in your application, refer the following link :
Android N – Direct Boot example

Suraj Makhija

Software Engineer(Android) at LetsNurture Infotech, a Passionate Android Explorer and a CR7 Fan

Want to work with us? We're hiring!