Lecture 8 Notifications & Settings

This lecture discusses some additional user interface components common to Android applications: Notifications, Settings Menus. and Dialogs This lecture aims to provide exposure rather than complete coverage to these concepts; for more options and examples, see the official Android documentation.

This lecture references code found at https://github.com/info448/lecture08-notifications-settings.

8.1 Dialogs

We have previously provided feedback to users via simple pop-ups such as Toasts or Snackbars. However, sometimes you would like to show a more complex “pop-up” View—perhaps one that requires additional interaction.

A Dialog28 is a “pop-up” modal (a view which doesn’t fill the screen) that either asks the user to make a decision or provides some additional information. At it’s most basic, Dialogs are similar to the window.alert() function and its variants used in JavaScript.

There is a base Dialog class, but almost always we use a pre-defined subclass instead (similar to how we’ve use AppCompatActivity). AlertDialog29 is the most common version: a simple message with buttons you can respond with (confirm, cancel, etc).

We don’t actually instantiate an AlertDialog directly (in fact, its constructors are protected so inaccessible to us). Instead we use a helper factory class called an AlertDialog.Builder. There are a number of steps to use a builder to create a Dialog:

  1. Instantiate a new builder for this particular dialog. The constructor takes in a Context under which to create the Dialog. Note that once the builder is initialized, you can create and recreate the same dialog with a single method call—that’s the benefits of using a factory.

  2. Call “setter” methods on the builder in order to specify the title, message, etc. for the dialog that will appear. This can be hard-coded text or a reference to an XML String resource (as a user-facing String, the later is more appropriate for published applications). Each setter method will return a reference to the builder, making it easy to chain them.

  3. Use appropriate setter methods to specify callbacks (via a DialogInterface.OnClickListener) for individual buttons. Note that the “positive” button normally has the text "OK", but this can be customized.

  4. Finally, actually instantiate the AlertDialog with the builder.create() method, using the show() method to make the dialog appear on the screen!

//java
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Alert!")
       .setMessage("Danger Will Robinson!");
builder.setPositiveButton("I see it!", new DialogInterface.OnClickListener() {
  public void onClick(DialogInterface dialog, int id) {
    // User clicked OK button
  }
});

AlertDialog dialog = builder.create();
dialog.show();
//kotlin
val builder = AlertDialog.Builder(this)
builder.apply {
    setTitle("Alert!")
    setMessage("Danger Will Robinson!")
    setPositiveButton("I see it!") { dialog, id -> 
        Log.v(TAG, "You clicked okay! Good times :)") 
    }
}

An important part of learning to develop Android applications is being able to read the API to discover effective options. For example, can you read the AlertDialog.Builder API and determine how to add a “cancel” button to the alert?

While AlertDialog is the most common Dialog, Android supports other subclasses as well. For example, DatePickerDialog and TimePickerDialog provide pre-defined user interfaces for picking a date or a time respectively. See the Pickers guide for details about how to utilize these.

DialogFragments

The process described above will create and show a Dialog, but that dialog has a few problems in how it interacts with the rest of the Android framework—namely with the lifecycle of the Activity in which it is embedded.

For example, if the device changes configurations (e.g., is rotated from portrait to landscape) then the Activity is destroyed and re-created (it’s onCreate() method will be called again). But if this happens while a Dialog is being shown, then a android.view.WindowLeaked error will occur and the Dialog is lost!

To avoid these problems, we need to have a way of giving that Dialog its own lifecycle which can interact with the the Activity’s lifecycle… sort of like making it a modular piece of an Activity… that’s right, we need to make it a Fragment! Specifically, we will use a subclass of Fragment called DialogFragment, which is a Fragment that displays as a modal dialog floating above the Activity (no extra work needed).

Just like with the previous Fragment examples, we’ll need to create our own subclass of DialogFragment. It’s often easiest to make this a nested class if the Dialog won’t be doing a lot of work (e.g., shows a simple confirmation).

Rather than specifying a Fragment layout through onCreateView(), we can instead override the onCreateDialog() callback to specify a Dialog object that will provide the view hierarchy for the Fragment. This Dialog can be created with the AlertDialog.Builder class as before!

//java
public static class HelloDialogFragment extends DialogFragment {

    public static HelloDialogFragment newInstance() {
        Bundle args = new Bundle();
        HelloDialogFragment fragment = new HelloDialogFragment();
        fragment.setArguments(args);
        return fragment;
    }

    public Dialog onCreateDialog(Bundle savedInstanceState) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        //...
        AlertDialog dialog = builder.create();
        return dialog;
    }
}
//kotlin
class HelloDialogFragment : DialogFragment() {
    companion object {
        fun newInstance(): HelloDialogFragment {
            val args = Bundle()
            val fragment = HelloDialogFragment()
            fragment.arguments = args
            return fragment
        }
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return activity?.let { //confirm Activity isn't null
            val builder = AlertDialog.Builder(it)
            builder.apply {
                //...
            }
            builder.create()
        } ?: throw IllegalStateException("Activity cannot be null")
    }
}

Finally, we can actually show this DialogFragment by instantiating it (remember to use a newInstance() factory method!) and then calling the show() method on it to make it show as a Dialog. The show() method takes in a FragmentManager used to manage this transaction. By using a DialogFragment, it is possible to change the device configuration (rotate the phone) and the Dialog is retained.

Here’s the other neat trick: a DialogFragment is just a Fragment. That means we can use it anywhere we normally used Fragments… including embedding them into layouts! For example if you made the DetailsFragment subclass DialogFragment instead of Fragment, it would be able to be used in the exact same as before. It’s still a Fragment, just with extra features—one of which is a show() method that will show it as a Dialog!

  • Use setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog) to make the Fragment look a little more like a dialog.

The truth is that Dialogs are not very commonly used in Android (compare to other UI systems). Apps are more likely to just dynamically change the Fragment or Activity being shown, rather than interrupt the user flow by creating a pop-up modal. And 80% of the Dialogs that are used are AlertDialogs. Nevertheless, it is worth being familiar with this process and the patterns it draws upon!

8.2 Notifications

We can let the user know what is going on with the app by popping up a Toast or Dialog, but often we want to notify the user of something outside of the normal Activity UI (e.g., when the app isn’t running, or without getting in the way of other interactions). To do this, we can use Notifications30. These are specialized views that show up in the notification area (the icons at the top of the operating system display) and in the system’s notification drawer, which the user can get to at any point—even when outside the app—by swiping down on the screen.

Android’s documentation for UI components is overall quite thorough and usable (after all, Google wants to make sure that developers can build effective apps, thereby making the platform worthwhile). And because there are so many different UI elements and they change all the time, in order to do real-world Android development you need to be able to read, synthesize, and apply this documentation. As such, this lecture will demonstrate how to utilize that documentation and apply it to create notifications. We will follow through the documentation to add a feature that when we click on the “notify” button, a notification will appear that reports how many times we’ve clicked that button.

  • To follow along this, open up the Notifications documentation at https://developer.android.com/training/notify-user/build-notification.

  • Looking at the documentation we see a link to the Notifications Overview to start. That page also links to the Notification Design Guide, which is a good place to go to figure out how to design effective notifications.

  • I personally prefer to work off of sample code, modifying it until I have something that does what I want. So I suggest scrolling down slowly until you find an example you can copy/paste in, or at least reference. Then you can scroll back up later to get more detail about how that code works. The table of contents is also useful for this.

    Eventually you’ll find a subsection “Create a basic notification”, which sounds like a great place to start!

The first part of creating this Notification is using NotificationCompat.Builder (use the v4 support version; the v7 is now deprecated). We saw this kind of Builder class with AlertBuilder, and the same concept applies here: it is a class used to construct the Notification for us. We call setters to specify the properties of the Notification.

  • Starting in API 26 (Oreo), we also are required to specify a notification channel (called a “notification category” in the user interface). These are used to help users manage notifications by grouping them together and allowing users to control the settings of that group—such as whether they are shown, what sounds they play, etc.

    Notification channels are identified by a String ID that is unique within the package, so we’ll usually define this as a constant. We can make a single channel for our whole app for now. We’ll actually set up the channel later, but all notifications need to be associated with one.

    • Note that you need to do this if you target API 26 or later; otherwise the method signatures change a bit.
  • Going through those settings: I don’t have a drawable resource to use for an icon, which makes me want to not include the icon specification. Looking up a bit you’ll notice that notification icon is required, so we will need to make one.

    We can produce an new Image Asset for the notification icon (File > New > Image Asset), just as we did previously with launcher icons and Material icons. Specify the “type” as Notification, give it an appropriate name, and pick a clipart of your choosing.

  • We set the “title” and the content for the notification.

  • The “priority” is how important the notification is (e.g., whether it should interrupt the user or not). It’s used for API 25 and lower; API 26 (Oreo) builds this into the Notification Channel’s “importance” instead. But we set it here for backwards compatibility.

The next step is to actually construct the Notification Channel. While we’ve given it a name, we need to actually make the object to manage that channel’s settings.

  • Note that the code to run channels is only available for API 26 (Oreo). Since they’re not in the support library, we have to use an if statement to make sure we don’t try to run the code on older devices! We do this by comparing against the Build.VERSION.SDK_INT:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        // Create the NotificationChannel
    }
  • The channel is instantiated as a new NotificationChannel object, and is passed an ID (so it knows what it’s name is), a display label, and an importance.

    For importance, IMPORTANCE_HIGH will cause it to pop up and make a sound. See the documentation for other options. Also check out the design guide for best practices on specifying Notification priority.

    • For earlier versions of Android, you set the priority. In order for a notification to visually intrude upon the user, its priority must be high enough (it needs to be NotificationCompat.PRIORITY_HIGH or higher) and because it needs to use either sound or vibration (it needs to be really important to get a heads-up pop).

      You can make the Notification vibrate by using the setVibrate() method, passing it an array of times (in milliseconds) at which to turn vibration on and off. The pattern is [delay, vibrate, sleep, vibrate, sleep, ...] You can also assign a default sound with (e.g.) builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);

  • We use the NotificationManager (not the support library one!) to “register” the channel, so that notifications can be added to it.

    val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    notificationManager.createNotificationChannel(channel)

Skipping ahead, we can actually show the notification that we’ve created so far. We use the NotificationManager (similar to the FragmentManager, SmsManager, etc.) to fetch the notification service (the “application” that handles all the notifications for the OS). We tell this manager to actually issue the built Notification object.

  • We also pass the notify() method an ID number to refer to the particular Notification. This will allow us to refer to and update that Notification later. We can update this notification later by just issuing a Notification with the same ID number, and it will “replace” the previous one!

  • For example, we can have our text be based on some instance variable, and have the Notification track the number of clicks!

8.2.1 Tap Actions

We’ve managed to show notifications, but we also want them to do something, such as when you click on it—specifying the Notification Action. At the very least, clicking on the notification should open up the relevant application. And since Intents are messages to open Activities, it makes sense that clicking a Notification would send an Intent. Moreover, remember that notifications exist outside of the app that creates them; so we need to use Intents to send messages (we can’t just specify a callback function).

But beyond just making the Intent to send, we’re actually going to create what is called a PendingIntent. The details are not super readable, but it’s basically a wrapper around an Intent that we give to another class. Then when that other component wants to send the Intent, it can “unwrap” it from the PendingIntent and run the command as if it were us (in terms of the “source”, permissions, etc). The Intent is one that we sent, but is “pending” delivery/activation by another service. So when we select the notification, the Notification Drawer service can open up that PendingIntent packet, pull out the Intent we want to send, and then mail it off. And this Intent that the Notification Drawer sends is to wake up our Activity, and it is sent with our permissions (rather than the Drawer’s permissions), as if we had sent it ourselves!

  • This is like when you ask a professor for a letter of recommendation, and you give them the stamped envelope (which you should always do!). When they get the packet, then can then send their letter using your envelop and stamp!

  • We create a PendingIntent using the PendingIntent.getActivity() method—this takes in a Context, an ID code (similar to the request codes we use when sending Intents for Results; this allows us to know which PendingIntent a command came from), the Intent to be launched, and a flag PendingIntent.FLAG_CURRENT_UPDATE so that if we re-issue the PendingIntent it update the existing one instead of replacing it with a new pending action (e.g., for if we update our notification’s action).

  • Note that you can create an “artificial backstack” (e.g., if the Notification is supposed to drop the user into the middle of your app’s workflow) by using the TaskStackBuilder class. This builder will let you specify a “history” of intents, and use that to produce the proper PendingIntent.

  • Be sure and tell the NotificationCompat.Builder about the content intent we just defined!

Now we can click on the Notification and have it do something! Note that, as always, there are a number of other pieces/details/features we can specify, but I leave those to you to look up in the documentation.

As the course focuses on development, this lecture references but does not discuss the UI Design guidelines. For example: what kind of text should you put in your Notification? When should you choose to use a notification? Android has lots of guidance on these questions in their design documentation. Major HCI and Mobile Design guidelines apply here as well (e.g., make actions obvious, give feedback, avoid irreversible actions, etc.).

8.3 Settings

The last topic of this lecture is to support letting the user decide whether clicking the button should create notifications or not. For example, maybe sometimes the user just want to see Toasts! The cleanest way to support this kind of user preference is to create some Settings using Preferences.

SharedPreferences

Shared Preferences31 are one way that we can persist data in an application across application launches—that is, the data is stored on disk rather than just in memory, so will not be lost if the application is destroyed. SharedPreferences store key-value pairs of primitives (Strings, ints, etc), similar to what we’ve been putting in Bundles. This data will be stored across application sessions: if I save some data to the Preferences and close the app, it will be there when I come back.

  • Preferences are stored in an XML File in the file system. Basically we save lists of key-value pairs as a basic XML tree in a plain-text file (similar to the values resource files we’ve created). Note that this is not a resource, rather it is a file that happens to also be structured as XML.

  • SharedPreferences are not great for intricate or extensive structured data (since it only stores key-value pairs, and only primitives at that). Use other options for more complex data persistence, such as putting the data into a database or using the file system—both of which are discussed in later lectures.

Even though they are called “Preferences”, they not just for “user preferences”! We can persist any small bits of primitive data in a SharedPreferences file.

We can get access to a SharedPreferences file using the .getSharedPreferences(String, int) method. The first parameter String is the name of the SharedPreference file we want to access (we can have multiple XML files; just use getPreferences() to use a single default). The second parameter int is a flag about whether other apps should have access to that file. MODE_PRIVATE (0) is the default, MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE are the other options.

We can edit this XML file by calling .edit() on the SharedPreferences object to get a SharedPreferences.Editor, which is a Bundle-esque object we can put values into.

  • We need to call .commit() on the editor to save our changes to the file system!

Finally, we can just call get() methods on the SharedPreferences object in order to fetch data out of it! The second parameter of these methods is a default value for if a preference doesn’t exist yet, making it easy to avoid null errors.

For practice, try saving the notification count in the Activity’s onStop() function, and retrieving it in onCreate(). This will allow you to persist the count even when the Activity is destroyed.

Preference Settings

While a SharedPreferences file acts a generic data store, it is called Shared Preferences because it’s most commonly used for “user preferences”—e.g., the “Settings” for an app.

  • Yes, the term “preferences” can be used to mean either “shared preferences” or “user preferences” (or both if they are the same thing!) depending on the context.

A “User Preference Menu” is a user-facing element, so we’ll want to define it as an XML resource. But we’re not going to try and create our own layout and interaction: instead we’re just going to define the list of Preferences32 themselves as a resource!

  • We can create a new resource using Android Studio’s New Resource wizard. The “type” for this is actually just XML (generic), though our “root element” will be a <PreferenceScreen> (thanks intelligent defaults!). By convention, the preferences resource is named preferences.xml.

Inside the <PreferenceScreen>, we add more elements: one to represent each preference we want to let the user adjust (or each “line” of the Settings screen). We can define different types of Preference objects, such as <CheckBoxPreference>, <EditTextPreference>, <SwitchPreference>, or <ListPreference> (for a dialog of radio buttons). There are a couple of other options as well; see the Preference base class.

  • These elements should include the following XML attributes (among others):

    • android:key the key to use when storing the preference in the SharedPreferences file
    • android:title a user-visible name for the Preference
    • android:defaultvalue a default value for the preference (use true or false for checkboxes).
    • More options cam be found in the the Preference documentation.
  • We can further divide these Preferences to organize them: we can place them inside a PreferenceCategory tag (with its own title and key) in order to group them together.

  • Finally we can specify that our Preferences have multiple screens by nesting PreferenceScreen elements. This produces “subscreens” (like submenus): when we click on the item it will take us to the next screen.

Note that a cleaner (but more labor-intensive) way to do this if you have lots of settings is to use preference-headers which allows for better multi-pane layouts… but since we’re not making any apps with that many settings this process is left as exercise for the reader.

Once we have the Preferences all defined in XML, we just need to show them in our application! Since the Settings will be their own screen, we’ll need to make another Activity (though it won’t need a layout!). To actually render the Preferences XML, we’ll use the PreferenceFragment class (a specialized Fragment for showing lists of Preference objects); it’s usually easies to create this as a nested class in the Activity.

  • For the Fragment, we don’t need to specify an onCreateView() method, instead we’re just going to load that Preference resource in the onCreate() method using addPreferencesFromResource(R.xml.preferences). This will cause the PreferenceFragment to create the appropriate layout!

  • For the Activity, we just need to have it load that Fragment via a FragmentTransaction:

    getFragmentManager().beginTransaction()
                    .replace(android.R.id.content, new SettingsFragment())
                    .commit();
    • The Activity doesn’t even need to load a layout: just specify a transaction! But if we want to include other stuff (e.g., an ActionBar), we’d need to structure the Activity and its layout in more detail.

    • Note that the container for our Fragment is android.R.id.content. This refers to the “root element” of the current View—basically what setContentView() is normally inflating into.

  • There is a PreferenceActivity class as well, but the official recommendation is: do not use it. Many of its methods are deprecated, and since we’re using Fragments via the support library, we should stick with the Fragment process.

Finally, how do we interact with these settings? Here’s the trick: a preferences XML resource is automatically associated with a SharedPreferences file! And in fact, every time we adjust a setting in the PreferenceFragment, the values in that file are edited as well! In effect, the PreferenceFragment automatically edits the SharedPreferences based on the user interaction. We never need to write to the file ourselves, just read from it (and we read from it in the exact same way we read any other SharedPreferences file, as described above).

The preference XML corresponds to the “default” SharedPreferences file, which we’ll access via:

SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
  • And then we have this object we can fetch data from with getString(), getBoolean(), etc.

This will allow us to check the preferences before we show a notification!

That’s the basics of using Settings. For more details see the documentation, as well as the design guide for best practices on how to organize your Settings.