Users can now add, reorder, dismiss reminders. Now how do we remind them of those reminders? With notifications!

Design

There are guidelines for when and how notifications should be used. There are many details about the structure of notifications:

  • header area
  • content area
  • action area that contains buttons

Notifications can have an expanded view, and if an app has notifications from different sources (like an email app), it can show a summarized view of all the notifications.
Our app will do something simple: remind the user to check out their reminders. When they tap the notification, we'll open the app with the list of reminders.

Triggering

For the notification to be the most effective, we want to show it when the user is home or at work. One way to know is to check if the phone is on Wifi. This way we don't even need to request permission for GPS, or ask the user where they live or work.
The documentation on background operations lists the different mechanisms at our disposal. In our case, Scheduled Jobs are the best. We want the job to trigger when on a Wifi network (unmetered network).
To implement it, I heavily relied on this post by a Developer Advocate.

Create the Service

Android Studio is great: when I created my JobService, it suggested to declare the service in the manifest, just like the post prescribed. The only thing I didn't understand from the post was what android:exported was for. I did not use it in my manifest and it worked fine.

Screen-Shot-2017-10-01-at-3.41.59-PM

The service ended up looking like this:

class ReminderService : JobService() {
    private val TAG = ReminderService::class.java.simpleName

    override fun onStopJob(parameters: JobParameters?): Boolean {
        // If it fails, don't reschedule the job.
        return false
    }

    override fun onStartJob(parameters: JobParameters?): Boolean {
        Log.i(TAG, "OnStartJob")
        // TODO: implement sendNotification()
        sendNotification()
        jobFinished(parameters, true)
        return false
    }

As specified in the documentation, onStartJob should return false because the method has finished its work (sendNotification() is synchronous) and call jobFinished when it is done.

Schedule the job

I configured the job to run ReminderService once an hour only when on Wifi. Also I made it persisted, meaning it will persist even after a reboot.

val jobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val jobInfo = JobInfo.Builder(SHOW_NOTIFICATION_JOB_ID,
        ComponentName(this, ReminderService::class.java))
        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
        .setPeriodic(3600000)
        .setPersisted(true)
        .build()
val result = jobScheduler.schedule(jobInfo)
if (result == JobScheduler.RESULT_SUCCESS) {
    ...

Making the job persistent required me to to add the RECEIVE_BOOT_COMPLETED permission. Here's the final version of the manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.wafrat.rappel">

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <application ...>
        ...
        <service android:name=".ReminderService"
            android:permission="android.permission.BIND_JOB_SERVICE" />
    </application>
</manifest>

Notifications

The documentation on notifications explains that starting with Android O (API level 26), you should create a Notification Channel with the NotificationManager, then post Notifications to it.

On API level < 26

Since I also target Nexus 6, which runs Nougat (API level 23), I could not use the NotificationManager. I had to use the deprecated NotificationCompat.Builder.

Code snippet TBD

I used a PendingIntent to show the activity for the list of reminders when the user taps the notification. The code worked, but the icon was not right. I will have to look up how to use drawables in a future post.

On API level ≥ 26

After a few days, I was wonderin why I was receiving no notification on my Pixel, even though I did on my Nexus 6. Was the scheduler more aggressive and not trigger every hour? Then why was it not triggering ever? It turns out Android O won't show notifications without first setting up a Notification Channel.
When implementing the call according to documentation on notifications,Android Studio showed a lint error about how my minimum target is level 23, but I am using level 26 classes. It also explains how to fix it. In my case, I want to explicitly tell the compiler that this code will only run on level 26 and above using the @TargetApi annotation.
My OnJobStart now has to check which to run:

override fun onStartJob(parameters: JobParameters?): Boolean {
    Log.i(TAG, "OnStartJob")
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        createNotificationChannel()
    }
    sendNotification()
    jobFinished(parameters, true)
    return false
}

And I can make my O specific implementation like so:

@TargetApi(26)
private fun createNotificationChannel() {
    ...
}

Stack or no stack?

In the API Guide on Notifications, they show a code snippet that builds a stack builder with only one activity, explaining that this is so navigating backwards from the Activity leds back to the Home screen.
However on the Training page on Notifications, they say there is no need to make a stack.

// Because clicking the notification opens a new ("special") activity, there's
// no need to create an artificial back stack.
val resultIntent = Intent(this, ScrollingActivity::class.java)
val resultPendingIntent = PendingIntent.getActivity(
        this,
        0,
        resultIntent,
        PendingIntent.FLAG_UPDATE_CURRENT
);
builder.setContentIntent(resultPendingIntent);

I've tried both and they both work, so I went for the version without a stack. When I need fancier behavior, I will read more here.

Wrap up

The next day, I received a notification on my watch as I got to work! I had just joined the company Wifi while still on the street. In the future I might change it to trigger 15-20 minutes after getting to Wifi, if they are still on Wifi.