Lecture 9 UI Components II

This lecture discusses how to include additional user interface components in an Android application: namely Notifications31 and Settings Menus. As before, 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-s17/lecture09-notifications-settings.

9.1 Notifications

We have previously covered how to let the user know what’s going on by popping up a toast or even an AlertDialog, 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 Notifications. 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 utilize this documentation in order to add a feature that when we click on the appropriate menu button, a notification will appear that reports how many times we’ve selected that option.

  • To follow along this, open up the Notifications documentation.

  • Looking at the documentation we see an overview to start. There is also a link to the Notification Design Guide, which is a good place to go to figure out how to design effective notifications.

  • There is a lot of text about how to make a Notification… I personally prefer to work off of sample code, modifying it until I have something that does what I want, so I’m going to scroll down slowly until I find an example I can copy/paste in, or at least reference. Then we can scroll back up later to get more detail about how that code works.

  • Eventually you’ll find a subsection “Creating a Simple Notification”, which souds like a great place to start!

The first part of this Notification is using NotificationCompat.Builder (use the v4 support version). We have previously seen 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 properteis of the Notification

  • I don’t have a drawable resource to use for an icon, which makes me want to not include the icon specification. However, scrolling back up will reveal that a notification icon is required, so we will need to make one.

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

The next line makes an Intent. We’ve done that too… but why create an Intent for a Notificatoin? If we scroll up and look where Intent is referenced, we can find out about Notification Actions, which specify what happens when the user clicks on the Notification. Usually this opens the relevant application, and since Intents are messages to open Activities, it makes sense that clicking a Notification would send an Intent.

  • Notice that the Intent will actually be wrapped in a PendingIntent. Thus we will give the Notification a PendingIntent, which contains an “RSVP” (to open the Activity) that it can send to the system when someone clicks on it. (the Intent is “pending” delivery/activation by another service).

    In particular, we use a PendingIntent in order to make sure that the Activity who will be executing it (the “notification service” component) will have permission to send the contained Intent. The Intent the notification service sends is to wake up our Activity, run with our permissions. It is as if we had sent the Intent ourselves!

The example Notification is also using the a TaskStackBuilder to construct an “artificial” backstack. This is used to specify, for example, that if the user clicks on the Notification and jumps to a “detail” view (say), they can still hit the back button to return to the “master” view, as if they had navigated to the “detail” view following a normal application phone.

  • We build this backstack not just with methods, but by integrating with the “parent-child” relationship we’ve otherwise set up between Activities. In the Manifest, we had specified that SecondActivity's parent is MainActivity. This is what gave us the nice back button in the ActionBar. These sequence of parentActivityName attributes form a hierarchy that will be the “back navigation hierarchy.” We add the “endpoint” of the hierarchy to the builder using addParentStack(MyResultActivity.class), and then finally put the Intent we actually want to use “on top” of the stack with addNextIntent(resultIntent).

The resultIntent is not the PendingIntent… yet. While we could define a PendingIntent manually, the example uses the TaskStackBuilder#getPendingIntent() method to build an appropriate PendingIntent object.

  • Pass it an ID to refer to that request (like we’ve done when sending Intents for Results), and a flag PendingIntent.FLAG_CURRENT_UPDATE so that if we re-issue the PendingIntent it update instead of replace the pending Intent.
  • We can then assign that PendingIntent to the Notification builder (with setContentIntent()).

Finally, we can 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 (not the PendingIntent, but the Notification). Again, this will allow us to refer to and update that Notification.

This allows us to have working Notifications! We can click the button to launch a Notification, and then click on the Notification to be taken to our app, which has a working back stack!

We can also update this notification later, and it’s really straightforward: we simply re-issue 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!

You may notice that this notification doesn’t “pop up” in a way we might expect. This is because its priority isn’t high enough (it needs to be NotificationCompat.PRIORITY_HIGH or higher) and because it doesn’t use either sound or vibration (it needs to be really important to get a heads-up pop).

  • We 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.
  • Pattern is [delay, vibrate, sleep, vibrate, sleep, ...]
  • We can also assign a default sound with (e.g.,) builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
  • See the design guide for best practices on priority.

As always, there are a number of other pieces/details we can specify, but I leave those to you to look up in the documentation.

As a focus on development, this lecture references but does not discuss the UI Design guidelines: e.g., 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, and further HCI and Mobile Design guidelines apply here just as well. In general, this course will leave the UI design up to you. But major guidelines apply (e.g., make actions obvious, give feedback, avoid irreversible actions, etc.).

9.2 Settings

The second 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.

9.2.1 SharedPreferences

Shared Preferences32 are another way that we can persist data in application (besides putting it into a database via a ContentProvider, or using the file system as described in the next lecture). 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 in lists of key-value pairs as a basic XML tree in a plain-text file. Note that this is not a resource, rather a file that happens to be structured as XML.

  • This is 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.

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

We can get access to this SharedPreferences file using the .getSharedPreferences(String, int) method. The first parameter String is the name of the Preference 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.

9.2.2 Preference Settings

While SharedPreferences 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.

The “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 Preferences33 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 screen Settings window). 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 store the preference in the SharedPreferences file
    • android:title a user-visible name
    • 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! To do this, we’re going to use the PreferenceFragment class (a specialized Fragment for showing lists of Preference objects).

  • 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!

We’ll put this Fragment inside a plain Activity, which just loads 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 android.R.id.content 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! We never need to write to the file, just read from it (similar to any other SharedPreferences file).

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.