Reminding the User - Creating a Feature‐Rich Application - Android Application Development For Dummies, 3rd Edition (2015)

Android Application Development For Dummies, 3rd Edition (2015)

Part III. Creating a Feature‐Rich Application

Chapter 14. Reminding the User

In This Chapter

arrow Understanding scheduled tasks

arrow Planning permissions

arrow Setting up alarms

arrow Using notifications

arrow Seeing how device reboots affect alarms

Many tasks need to happen daily, right? Wake up, take a shower, eat breakfast — we do all these things every day. These tasks make up the standard Monday‐through‐Friday morning routine for many people. You may have an internal clock and awaken every day on time, but most people have to set alarms to wake up on time. At work, employees have calendars that remind them of upcoming events they need to attend, such as meetings and important server upgrades. Reminders and alarms are part of most everyday routines, and people rely on them in one way or another.

Building your own scheduled task system from scratch would be a pain. Thankfully, Windows has scheduled tasks, Linux has cron, and Android has the AlarmManager class. Though Android is based on Linux, it doesn’t have access to cron; therefore, you have to set up scheduled actions via the Android AlarmManager.

These are the steps to reminding the user of something when the app isn’t running:

· Asking for permissions to wake up the device

· Registering a new alarm

· Creating a class to handle the alarm

· Re‐registering alarms when the phone reboots

· Creating a notification

Seeing Why You Need AlarmManager

A user adds a couple of tasks in the Tasks application (all due later today), puts his device away, and goes about his business. If he isn’t reminded about the tasks, he might forget about them; therefore, he needs a way to be reminded of what should happen — which is where theAlarmManager class comes into play.

The AlarmManager class allows users to schedule a time when the Tasks application should be run. When an alarm goes off, an intent is broadcast by the system. Your application then responds to that broadcast intent and performs an action, such as opening the application, notifying the user via a status bar notification (which you will write later in this chapter), or performing another type of action.

Asking the User for Permission

You wouldn’t let your next‐door neighbor store holiday decorations in your shed without permission, would you? Probably not. Android is no different. Performing some actions on a user’s Android device requires permission, as explained in the following sections.

You added the INTERNET permission to the Tasks app in Chapter 9. In this section, you’ll learn more about how Android permissions work.

Seeing how permissions affect the user experience

When a user installs an application from the Google Play Store, the application’s manifest file is inspected for required permissions. Anytime your application needs access to sensitive components (such as external storage, the Internet, or device information), the user is notified at install time and decides whether to continue the installation.

Don’t request unnecessary permissions for your app — security‐savvy users are likely to reject it. For example, the Silent Mode Toggle application (described in Part II) doesn’t need GPS locations, Internet access, or hardware‐related information. (But if you’d like to learn how to incorporate GPS location into your apps, visit the book’s online web extras at www.dummies.com/extras/androidappdevelopment.)

If your application doesn’t need a permission, yank it. The fewer permissions your application requests, the more likely the user is to install it.

Setting requested permissions in the AndroidManifest.xml file

When you need to request permissions, add them to the AndroidManifest.xml file in your project. You need to add the android.permission.RECEIVE_BOOT_COMPLETED permission to the Tasks application. It allows the application to know when the device reboots so that it can re‐register its alarms with the AlarmManager.

You edit the AndroidManifest.xml file to add the uses‐permission element to the manifest element. The XML permission request looks like this:

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

To view a full list of available permissions, view the Android permission documentation at http://d.android.com/reference/android/Manifest.permission.html.

If you don’t declare the permissions that your application needs, it won’t function as expected on either a device or an emulator, and any runtime exceptions that are thrown may crash your application. Always ensure that your permissions are present.

Waking Up a Process with AlarmManager

To wake up a process with AlarmManager, you have to set the alarm first. In the Tasks application, the best place to do it is right after you save a task in the save button’s save() call.

Creating the ReminderManager helper

Creating an alarm isn’t hard, but it does take a few lines of code. You will use this code in a couple of places, so it makes sense to have a method to do it for you. Put this method into a new class named ReminderManager.java in a new package namedcom/dummies/tasks/util:

/**
* A helper class that knows how to set reminders using the AlarmManager
*/
public class ReminderManager {

private ReminderManager() {} →6

public static void setReminder(Context context, long taskId, →8
String title, Calendar when) {

AlarmManager alarmManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE); →12

Intent i = new Intent(context, OnAlarmReceiver.class); →14
i.putExtra(TaskProvider.COLUMN_TASKID, taskId);
i.putExtra(TaskProvider.COLUMN_TITLE, title);

PendingIntent pi = PendingIntent.getBroadcast(context, 0, i, →18
PendingIntent.FLAG_ONE_SHOT);

alarmManager.setExact(AlarmManager.RTC_WAKEUP, →21
when.getTimeInMillis(), pi);
}
}

Here’s an explanation of what the previous code does:

6 The ReminderManager class should not be instantiated, so make the constructor private.

8 The setReminder method takes the task ID, the task’s title, and the date/time of the reminder and creates an alarm to wake up your OnAlarmReceiver (not yet written) at the specified time.

12 Asks the context for an AlarmManager by calling getSystemService(ALARM_SERVICE).

14 Creates an intent for the AlarmReceiver class, which you haven’t written yet. The intent tells the AlarmReceiver the ID and the title of the task that the AlarmReceiver needs to create a notification for.

18 Creates the PendingIntent that will wrap the intent from line 14. All intents used in the AlarmManager must be wrapped in a PendingIntent to “give permission” to the AlarmManager to call back into our application.

A pending intent is a wrapper around an intent and target action to perform with it. A pending intent can be handed to other applications so that they can perform the action you described on your behalf at a later time.

By giving a PendingIntent to another application (in this case, the Android OS which runs the AlarmManager), you are granting it the right to perform the operation you have specified as if the other application was yourself (with the same permissions and identity). As such, you should be careful about how you build the PendingIntent: Almost always, for example, the base intent you supply should have the component name explicitly set to one of your own components, to ensure it is ultimately sent there and nowhere else.

21 Sets the alarm using the pending intent and the date/time for the task. The AlarmManager can use one of the following:

· RTC (Real Time Clock): Specifies the exact time to wake up

· ELAPSED_REALTIME: Specifies the exact time to wake up relative to when the device booted

· INTERVAL: Specifies a periodic wake up

Additionally, most of the settings just described have WAKEUP and non‐WAKEUP options:

· WAKEUP: Specifying this means that the phone will wake up at exactly the time you specified and do whatever you say.

· non‐WAKEUP: Specifying this means that the device will wake up sometime around the time you specified.

Whenever possible you should choose non‐WAKEUP so that the device can group wakeup alarms together and minimize draining your battery. In this app, a WAKEUP is necessary because we don’t want to remind the user later than expected.

The next step is to call setReminder when a task is created or edited. To do this, open up TaskEditFragment and add the following to the bottom of your save() method:

// Create a reminder for this task
ReminderManager.setReminder( getActivity(),
taskId, title, taskDateAndTime);

This line of code instructs ReminderManager to set a new reminder for the task with a row ID of taskId at the particular date and time as defined by the taskDateAndTime variable.

If an alarm is already scheduled with a pending intent that contains the same signature, the previous alarm is canceled and the new one is set up.

Creating the notification in OnAlarmReceiver

Now that you have created an alarm, you need to specify what happens when the alarm fires.

The OnAlarmReceiver class, shown in Listing 14-1 , is responsible for handling the intent that’s fired when an alarm is raised. It’s a simple BroadcastReceiver called whenever an intent is broadcast that is addressed to it, like the one that was registered inTaskEditFragment in the previous section.

Create a new package named com.dummies.tasks.receiver, and add the OnAlarmReceiver class as follows:

Listing 141: The OnAlarmReceiver Class

/**
* This class is called when our reminder alarm fires,
* at which point we'll create a notification and show it to the user.
*/
public class OnAlarmReceiver extends BroadcastReceiver { →5
@Override
public void onReceive(Context context, Intent intent) { →7

// Important: Do not do any asynchronous operations in
// BroadcastReceive.onReceive! See the sidebar

NotificationManager mgr = (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE); →13

Intent taskEditIntent = →15
new Intent(context, TaskEditActivity.class);
long taskId = intent.getLongExtra(TaskProvider.COLUMN_TASKID, -1); →17
String title = intent.getStringExtra(TaskProvider.COLUMN_TITLE); →18
taskEditIntent.putExtra(TaskProvider.COLUMN_TASKID, taskId); →19

PendingIntent pi = PendingIntent.getActivity(context, 0, →21
taskEditIntent, PendingIntent.FLAG_ONE_SHOT);

// Build the Notification object using a Notification.Builder
Notification note = new Notification.Builder(context) →25
.setContentTitle(
context.getString(R.string.notify_new_task_title)) →27
.setContentText(title) →28
.setSmallIcon(android.R.drawable.stat_sys_warning) →29
.setContentIntent(pi) →30
.setAutoCancel(true) →31
.build(); →32

// Send the notification.
mgr.notify((int) taskId, note); →35
}
}

The numbered lines are explained in this list:

5 The OnAlarmReceiver is a BroadcastReceiver. This means that when the AlarmManager broadcasts the intent you created previously, the OnAlarmReceiver will wake up and receive the intent.

7 onReceive is called when the intent is received. The intent passed in will be the intent inside of your pending intent (not the pending intent itself).

13 Gets a NotificationManager from the context, which you’ll use to create a notification.

15 Creates the intent that opens the TaskEditActivity for the specified task id. You get the ID of the task from the OnAlarmReceiver’s broadcast intent. This intent is invoked when the user clicks on your notification. When that happens, theTaskEditActivity starts and users can view and edit their task.

17–18 Gets the task’s ID and title from the intent that you created in TaskEditFragment.

19 Adds the task’s ID to the edit intent because the receiver of the intent needs to know which task to edit.

21 Creates the PendingIntent that wraps the taskEditIntent. See the previous section to find out more about pending intents.

25–32 Now that you have the task’s ID, title, and an intent that starts the TaskEditActivity when invoked, it’s time to create the actual notification itself. Create a notification using the Notification.Builder.

27 Sets the title of the notification. Add the following string to your strings.xml:

<string name="notify_new_task_title">Task Reminder!</string>

28 Sets the content of the notification (displayed below the title) to the text of the task itself.

29 Gives the notification a simple icon. The stat_sys_warning is built into Android and works as a reasonable default.

30 Sets the intent for the notification to the pending intent that you created on line 21. This is the intent fired when the user clicks a notification.

31 Turns on “auto cancel.” This means that when a user clicks the notification, it is automatically dismissed. If you turn this off, the user must manually dismiss his notifications, or you must programmatically dismiss them for the user.

32 Builds the notification object and returns it.

35 Takes the notification object created on the previous line, and asks the NotificationManager to display it to the user.

Do not do any asynchronous operations (for example, using background threads) in BroadcastReceiver.onReceive!

The OS may kill your process immediately after onReceive returns, so if you attempt to do asynchronous operations in onReceive, they may get killed before they ever finish! The result is that sometimes things will appear to work, and sometimes they won’t.

Similarly, do not do any long‐running operations (such as network requests, disk or database reads or writes, and so on) in BroadcastReceiver.onReceive! onReceive is called from the UI thread, so if you do anything that may take more than a few hundred milliseconds, you can cause your app to appear to hang.

If you need to do asynchronous or long‐running operations, update OnAlarmReceiver to subclass android.support.v4.content.WakefulBroadcastReceiver and create a new service to do all your heavy lifting. Remember to call startWakefulServiceto start your service, and remember to call WakefulBroadcastReceiver.completeWakefulIntent from your service when you are done.

See http://d.android.com/reference/android/content/BroadcastReceiver.html and https://developer.android.com/reference/android/support/v4/content/WakefulBroadcastReceiver.html for more information.

Now, register the OnAlarmReceiver in your AndroidManifest.xml by adding the following line inside your application element:

<receiver android:name=".receiver.OnAlarmReceiver"
android:exported="false"/>

At this point, you should be able to run your app, create a new task, and watch it immediately pop up a reminder notification in your status bar, as in Figure 14-1.

image

Figure 141: An ­important reminder.

The NotificationManager has a lot of other options for getting the user’s attention. You can augment a notification using one — or more — of these options:

· Vibration: The device vibrates briefly when a notification is received — useful when the device is in the user’s pocket.

· Sound: An alarm sounds when the notification is received. A ringtone or a prerecorded tone that you install along with your application is useful when the user has cranked up the notification sound level.

· Light: The LED light on the device flashes at a given interval in the color you specify. (Many devices contain an LED that you can program.) If the LED supports only a single color, such as white, it flashes in that color and ignores your color specification. If the user has set the volume level to silent, the light provides an excellent cue that something needs attention.

· Expandable preview: The user can expand a notification by using the pinch‐and‐zoom gesture. The expandable notification is a helpful way to show users an expanded preview of the notification content, such as a message preview for an email application.

· Action buttons: A user has always been able to tap a notification to launch the app that created it. However, you can add as many as three additional buttons to a Jelly Bean app to make it perform whatever ­operations you want. One outstanding example in the Tasks app is having the Snooze button temporarily dismiss the notification and bring it back later.

Go to http://d.android.com/guide/topics/ui/notifiers/notifications.html for more information about creating more advanced notifications.

Updating a Notification

At some point, you might need to update the view of your notification, such as when your code runs in the background, to see whether tasks have been reviewed. This code checks to see whether any notifications are overdue. Suppose that after the two‐hour mark passes, you want to change the icon of the notification to a red exclamation point and quickly flash the LED in red. Thankfully, updating the notification is a fairly simple process.

If you call the notify() method again with an ID that’s already active on the status bar, the notification is updated on the status bar. Therefore, to update the notification, you simply create a new Notification object with the same ID and text (but with a different red icon) and then call notify() again to update the notification.

Clearing a Notification

Users constitute an unpredictable group — whether they’re first‐time users or advanced power users, they can be located anywhere in the world and use their devices in their own, special ways. At some point, a user may see a notification and decide to open the app using the app launcher instead. If this happens while a notification is active, the notification persists. Even if the user looks at the task at hand, the notification still persists on the status bar. Your application should be able to simply recognize the state of the application and take the appropriate measures to cancel any existing notifications for the task. However, if the user opens your app and reviews a different task that has no active notification, your app shouldn’t clear the notification.

Clear only the notification that the user is reviewing.

The NotificationManager makes it simple to cancel an existing notification by using the cancel() method. This method accepts one parameter — the ID of the notification. You may recall using the ID of the task as the ID of the note. The ID of the task is unique to the Tasks application. By doing this, you can easily open a task and cancel any existing notification by calling the cancel() method with the ID of the task.

At some point, you might also need to clear all previously shown notifications. To do this, simply call the cancelAll() method on the NotificationManager.

Rebooting Devices

You probably forget things from time to time. It’s only human. The Android AlarmManager is no different. The AlarmManager doesn’t persist alarms; therefore, when the device reboots, you must set up the alarms again.

If you don’t set up your alarms again, they simply don’t fire because, to Android, they don’t exist.

Creating a boot receiver

The RECEIVE_BOOT_COMPLETED permission allows your application to receive a broadcast notification from Android when the device is done booting and is eligible to be interactive with the user. Because the Android system can broadcast a message when this event is complete, you need to add another BroadcastReceiver to your project. This BroadcastReceiver is responsible for handling the boot notification from Android. When the broadcast is received, the receiver needs to retrieve the tasks from the TaskProvider and loop through each task and schedule an alarm for it, to ensure that your alarms don’t get lost in the reboot.

Add a new BroadcastReceiver to your application. For the Tasks application, the BroadcastReceiver has the name OnBootReceiver. You also need to add the following lines of code to the application element in the AndroidManifest.xml file:

<receiver android:name=".receiver.OnBootReceiver" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>

This snippet informs Android that OnBootReceiver should receive boot notifications for the BOOT_COMPLETED action. In layman’s terms, it lets OnBootReceiver know when the device is done booting up.

The full implementation of OnBootReceiver is shown in Listing 14-2 . Add this class to your com/dummies/tasks/receiver directory:

Listing 142: The OnBootReceiver Class

public class OnBootReceiver extends BroadcastReceiver { →1

@Override
public void onReceive(Context context, Intent intent) { →4

Cursor cursor = context.getContentResolver().query(
TaskProvider.CONTENT_URI, null, null, null, null); →7

// If our db is empty, don't do anything
if (cursor == null)
return;

try {
cursor.moveToFirst(); →14

int taskIdColumnIndex = cursor
.getColumnIndex(TaskProvider.COLUMN_TASKID); →17
int dateTimeColumnIndex = cursor
.getColumnIndex(TaskProvider.COLUMN_DATE_TIME);
int titleColumnIndex = cursor
.getColumnIndex(TaskProvider.COLUMN_TITLE); →21

while (!cursor.isAfterLast()) { →23

long taskId = cursor.getLong(taskIdColumnIndex); →25
long dateTime = cursor.getLong(dateTimeColumnIndex); →26
String title = cursor.getString(titleColumnIndex); →27

Calendar cal = Calendar.getInstance();
cal.setTime(new Date(dateTime)); →30

ReminderManager.setReminder(context, taskId, →32
title, cal);

cursor.moveToNext(); →35
}

} finally {
cursor.close(); →39
}
}
}

The numbered lines are detailed in this list:

1 The definition of the OnBootReceiver.

4 The onReceive() method that’s called when the receiver receives an intent to perform an action.

7 Obtains a cursor with all the reminders from the TaskProvider via the ContentResolver. It’s similar to the calls used to update and delete reminders in the TaskEditFragment and TaskListFragment.

14 Moves to the first record in the Cursor.

17–21 Each row in the cursor contains several columns of data. These lines get the index for the task, date/time, and title.

You want to find the ID of the row as well as the date and time so that you can schedule the reminder. You also need the title to display to the user. To get this information, you need to find the index of the columns that contain this information.

23 Sets up a while loop that checks to see whether the cursor has moved past the last record. If it equals false, the app moves to line 25. If this value is true, no more records are available to use in the cursor.

25–27 The ID, title, and dateTime are retrieved from the cursor for this row using the column indices from lines 17–21.

30 After the date is retrieved from the cursor, the Calendar variable needs to be updated with the correct time. This line sets the local Calendar object to the time of the task in the row.

32 Schedules a new reminder with the row ID from the database at the time defined by the recently built Calendar variable.

35 Moves to the next record in the cursor. If no more records exist in the cursor, the call to isAfterLast() on line 23 returns true, which means that the while loop exits. Otherwise, the next row is processed.

39 Closes the cursor because it’s no longer needed. BroadcastReceivers generally don’t use loaders, so you need to close the cursor.

When you previously worked with the Cursor object in Chapter 13, you didn’t have to close the cursor. This is because the Loader object was managing the cursor.

If you were to start the application, create a few reminders, and then reboot the device, you would see that the reminders persisted.

Checking the boot receiver

If you’re unsure whether OnBootReceiver is working, you can place log statements into the while loop, like this:

Log.d("OnBootReceiver", "Adding alarm from boot.");
Log.d("OnBootReceiver", "Row Id - " + rowId);

This snippet prints messages to the system log. You can then shut down the emulator (or device) and start it again. Watch the messages stream in logcat, and look for OnBootReceiver messages. If you have two tasks in your database, you should see two sets of messages informing you of the system adding an alarm during boot.

Interested in adding even more features to your feature‐rich app? Visit the book’s online web extras at www.dummies.com/extras/androidappdevelopment to learn how to incorporate GPS location information into your Tasks app.