3. Going Further

Now that you have a basic app, it’s time to add some more features. To start, you’ll need to handle some more events from the user, create an ongoing notification, and list extra options in a menu. Along the way, you’ll learn the specifics of supporting multiple device configurations; explore event callbacks and multiple event filtering; create notifications, toasts, and dialogs to alert the user; and learn when and how to create menus.

Supporting Multiple Screen Sizes

Android is designed to operate on a wide range of hardware devices, but writing a separate interface for every device would be a nightmare. Luckily, the Android OS provides a number of abstractions that support this diverse set of hardware.

Resource Qualifiers

Android has many features that are inspired by the web (not surprising given that it was created by Google). Nowhere is this more apparent than in the design philosophy of Android views. In contrast to iOS devices, Android apps don’t know the screen resolution, size, or aspect ratio of the devices they run on, but you can use the View classes to stretch and shrink layouts to fill the available space, just as you can on the web.

You’ve already seen how to create Android layouts, and you’ve learned how to create a layout that stretches to fill the available space. This creates a flexible layout, but it’s usually not enough to make your app work on every device configuration. Often, you need to do more than just adjust the size of elements—you actually need to create a different layout to provide a useful interface. To make this easier, Android uses a series of layout qualifiers that define different device configurations. The layout qualifiers are appended to the resource folder names. Using these folders, you can create a layout for a specific set of device configurations; Android will automatically select the appropriate layout file for the user’s device.

For example, when a phone is held in portrait, the basic XML layout file for that device will be loaded and displayed. When a phone is rotated to landscape, a landscape-specific layout can be loaded, but only if it is available; if no landscape version is available, the standard portrait version will be loaded. Layout qualifiers exist for screen density, orientation, screen size, mobile country codes, region, platform version, primary navigation mode, and much more. Table 3.1 summarizes the important qualifiers.

Table 3.1 Common Layout Resource Qualifiers

image

Note that this is not an exhaustive list of qualifiers. Consult the Android documentation for all the available options.


image Tip

The number of layout options may seem overwhelming at first. Don’t worry. In general, you will only need to handle the orientation, screen size, and screen density qualifiers.


In the Hello World app you created in Chapter 1, the call to setContentView will load the XML layout file and display it to the user. To add a new landscape version of the layout, follow these steps:

1. In the res/ directory, create a new folder named layout-land/ and put a copy of the main.xml file into it.

2. Open the new file and change the string to "Hello Landscape".

Now when you start the app, you will see the standard layout in portrait but the new layout in landscape.

Resource Qualifier Precedence

Android selects the appropriate resources at runtime, based on the device configuration. Since multiple resource folders could match, Android establishes precedence for qualifiers to resolve conflicts. This precedence determines which resources are selected. Consult the Android documentation for the full list of qualifiers and their precedence.

When selecting resources, these are the general steps that Android takes to determine the proper folder:

1. Eliminate all folders that contradict the device configuration.

2. Select the next qualifier in precedence.

3. If any folders match this qualifier, eliminate all folders that do not match. If no folders match this qualifier, return to step 2.

4. Continue until only one resource folder remains.

The exception to these rules is screen pixel density. Android will scale any resources to fit the screen; therefore, all pixel densities are valid. Android will select the closest density that is available, preferring to scale down larger densities.


image Tip

When creating resource folders, you must list qualifiers in their precedence order. Your app will not compile otherwise.


An example will better illustrate how Android selects resources. Consider a device with the following configuration:

• Screen orientation: landscape

• Screen pixel density: hdpi

• Screen size: large

• Touchscreen type: finger

You have the following resource folders in your app:

/res/layout/
/res/layout-notouch/
/res/layout-land/
/res/layout-land-ldpi/
/res/layout-land-finger/
/res/layout-hdpi/

Android will run through its steps to select the best resource:

1. Eliminate the /res/layout-notouch/ folder because it conflicts with the touchscreen qualifier.

2. There are no folders with screen-size qualifiers, so skip to the next qualifier.

3. Orientation is the next highest precedence, so eliminate all folders that do not have the land qualifer. This leaves three folders: /res/layout-land/, /res/layout-land-ldpi/, and /res/layout-land-finger/.

4. The next qualifier is pixel density. There is no exact match, so continue to the next qualifier. If no other qualifiers match, select the nearest pixel density.

5. The last qualifier is touchscreen type. In this case, finger means that the device has a capacitive touchscreen, so eliminate all folders that do not contain the finger qualifier.

6. The only remaining folder is /res/layout-land-finger/. Select the layouts in this folder.

Android performs this procedure for every resource your layouts require. Often, resources will be mixed from multiple locations. For example, the layout may be taken from the /res/layout-ldpi/ folder, but the drawable resources could be taken from the /res/layout-hdpi/ folder. Remember that Android selects each resource independently and will pick the best match. If you start getting strange problems with your layouts, check the precedence on your resource folders. Android may be loading different resources than you expect!


image Note

Android will select only screen size resource qualifiers that are smaller than or equal to the device configuration (excluding the pixel density qualifier). If you have only xlarge resources and the device has a small screen, then your app will crash when it runs.


Density-Independent Pixels

Using resource qualifiers and Android’s layout containers will let you create layouts that stretch and compress to fill available space. But sometimes you need to specify the exact dimensions of a view. If you’ve done GUI programming, you’re probably used to specifying exact sizes in pixels. Android supports this, but you should avoid using absolute pixel or dimension values when you create your app. The pixel density of devices varies greatly, and the same resource will appear as a different physical size on each device. Figure 3.1 shows an example of a button that has its height and width values specified in pixels. At each screen density, the relative size of the view is different.

image

Figure 3.1 A button with a fixed pixel size. At different screen resolutions, it appears as a physically different size.

To handle this, Android has several ways of declaring dimensions in a density-independent manner, summarized in Table 3.2.

Table 3.2 Android Dimension Units

image

Figure 3.2 shows the previous example, but with the height and width of the button specified in dp. The buttons appear much closer to the same size on the screen.

image

Figure 3.2 A button with density-independent pixels will appear as the same physical size, regardless of screen density.

In general, you should use dp for all units of measure (or sp for text sizes). Using these units will make your layouts appear consistent across device sizes and densities. This will ensure you get maximum device compatibility and will help you avoid layout headaches later.

9-Patch Graphics

Using the resource folders and density-independent pixels will get you most of the way to a flexible layout, and Android will scale your images appropriately in most situations. But often you will need to create image resources with rounded corners. These images won’t stretch properly and will appear distorted. To handle that case, Android supports a feature known as a 9-patch graphic. This is simply a PNG file with a 1-pixel border around it. The Draw 9-Patch tool (see Chapter 1) provides an easy way to create 9-patch graphics (Figure 3.3). These images can be stretched in the areas indicated by the shaded region (marked with black pixels in the border of the image). By using the Draw 9-Patch tool, you ensure that your image will stretch but will maintain the proper rounding on corners. All of the stock Android button resources use 9-patch graphics.

image

Figure 3.3 The Draw 9-Patch tool


Creating flexible layouts may seem arduous at first, but it will soon become second nature. Remember these points, and you should be able to build an app that is compatible with the majority of Android devices:

• Always use density-independent pixel values in your layout.

• Use wrap_content and match_parent whenever possible. This will make your layout much more flexible than layouts using hard-coded dimension values.

• Provide alternate image resources for each density to ensure that your images look appropriate on all screen densities.

• Use 9-patch graphics for any resources that can’t stretch without distortion.



image Tip

Remember that if you haven’t yet adapted your app to a particular screen configuration, you can specifically declare which configurations your app supports in the Android manifest. This will prevent your app from being installed on unsupported hardware.


Handling Notifications

Android is designed to run on portable devices that are carried everywhere and used in sporadic bursts. To ensure that users get the full benefit of these devices, Android supplies a robust set of notification techniques to ensure that users are immediately aware of any events. This section covers the notification options in order of increasing interruption to the user.

Toasts

A toast is the most basic and least intrusive notification. This is a simple message that is flashed on the screen for a short time (typically 5 to 10 seconds). It’s intended to give the user immediate feedback on some event that is relevant to their current situation. For example, if a social networking app is attempting to update a user’s status, it could use a toast to inform the user when the status is successfully updated. Figure 3.4 shows an example toast.

image

Figure 3.4 A toast notification

To create a toast, use the static makeText method on the Toast class to create a Toast object. Give it a context, the text you want to display, and a duration. The duration can be either Toast.LENGTH_SHORT or Toast.LENGTH_LONG. Calling show() on the Toast object will display it to the user. The following code snippet creates a simple toast:

Context context = getApplicationContext();
CharSequence text = "Hello toast!";
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, text, duration);
toast.show();

Toasts include options for setting their position on the screen. Use the setGravity method to change the default display location. And as with most views in Android, you can override the default layout of a toast and create a custom toast.

Use toasts when you want to give quick feedback to the user but don’t expect them to take any action.


image Tip

It’s possible for toasts to be shown when your application is not in the foreground. For example, a toast generated by a background service could be displayed during any application. Carefully think through the situations that might generate a toast in your application, and choose appropriate text.


Status Bar Notifications

The primary notification method in Android is the notification tray. This tray can be pulled down from the top of the screen and contains all ongoing and immediately important notifications. A notification in the tray consists of an icon, title text, and message text. Tapping the notification will take the user to the app that generated the notification (more on this in a bit).

If you need to create a notification, it should generally go in the notification tray. It’s the easiest way to notify the user, it is unobtrusive, and users will expect it. Figure 3.5 shows some example notifications.

image

Figure 3.5 One-time and ongoing notifications in the Android notification tray

Notifications have a few basic parameters you can set:

• An icon to display in the status bar.

• Optional ticker text that will be displayed in the status bar when the notification is first shown.

• The title and message to display in the notification tray. This is also optional.

• A required PendingIntent to trigger when the user taps the notification.


image Tip

Take care not to generate an excessive number of notifications. This will clutter the user’s notification tray, and they will likely uninstall your app. Instead, collapse all notifications into a single summary notification (such as total number of messages received).


A PendingIntent is simply a holder for an intent and target action. It allows you to pass an Intent object to another application and invoke that Intent as if it were invoked by your application. In this way, the PendingIntent can be used to trigger actions in your app. The PendingIntent is used by the notification tray application to trigger an event in your app when the user taps your notification.

Here is an example notification:

int icon = R.drawable.icon;
CharSequence ticker = "Hello World!";
long now = System.currentTimeMillis();
Notification notification = new Notification(icon, ticker, now);

A notification is created with an icon, ticker text, and timestamp. This example creates a notification with the resource located at /res/drawable/icon, the ticker text set to the string "Hello World!", and the time set to the current time.

Once you have created a notification, use the NotificationManager to display that notification to the user:

NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Context context = getApplicationContext();
CharSequence message = "Hello World!";
Intent intent = new Intent(this, Example.class);
String title = "Hello World!";
String message = "This is a message.";
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
notification.setLatestEventInfo(context, title, message, pendingIntent);
nm.notify(ID, notification);


You can do more than just update the status bar with notifications—you have the option of playing a sound, flashing an LED, or vibrating the device when the notification is triggered. You do this by using the notification.defaults option. For example, to play the default notification sound, set the defaults option to:

notification.defaults |= Notification.DEFAULT_SOUND;

This will play the user-configured notification sound when your notification is displayed. You can also use a custom sound for the sound option:

notification.sound = Uri.parse("file:///sdcard/mysound.mp3");

There are custom options for LED flashing and vibrations as well. Check out the Notification class in the Android documentation for all options available. Keep in mind that not all devices will have LED indicators or vibration capability.


Use the setLatestEventInfo method to update the contents of the notification. Here, the PendingIntent is set to display the example activity when the notification is tapped. Call notify to display the notification in the tray. The device status bar will also display the notification briefly before returning to its usual state. You can update a notification that is already displayed by calling setLatestEventInfo and then notify again.


image Tip

Android 3.0 (Honeycomb) introduced the Notification.Builder class for creating notifications. This builder replaces the existing constructor for the Notification class and makes creating notifications easier. The notification ID for each notification should be globally unique in your app. You should list all these IDs in a common file to ensure their uniqueness, or strange behavior may result.


Dialogs

Notifications are great for most events, because they don’t interrupt the user. However, sometimes you need to inform the user of something immediately—perhaps you need to alert the user to some failure in your app or confirm that they want to perform an action. To do this, you use a dialog.

Dialogs are small windows displayed over your application (Figure 3.6). They block all user input into your app and must be dismissed before the user can continue using your app. This makes them the most intrusive of all notification types. Android provides several types of dialogs, with different use cases: an AlertDialog with simple Accept and Cancel buttons, a ProgressDialog for displaying long-running progress, and date- and time-picker dialogs for accepting user input.

image

Figure 3.6 An alert dialog

To create a dialog, extend the DialogFragment class and implement either the onCreateView or onCreateDialog method. Use onCreateView to set the view hierarchy (what is displayed inside it) for the dialog; use onCreateDialog to create a completely custom dialog. A typical scenario is to override onCreateDialog and return an AlertDialog. An AlertDialog is a dialog with some text and one, two, or three buttons.


Dialogs have traditionally been managed as part of the activity life cycle. However, Android 3.0 introduced a new method of creating dialogs: the DialogFragment class. This class uses the new Fragments framework to create and manage the life cycle of dialogs. This is the primary method of creating a dialog for future versions of Android. The DialogFragment class is available to previous versions of Android through the compatibility library, a collection of classes that allows you to use the new fragments APIs on older versions of Android. You should use DialogFragments for all of your applications.


Add a confirm dialog to the TimeTracker app you created in Chapter 2 by following these steps:

1. Create a new class, ConfirmClearDialogFragment, that extends DialogFragment:

public class ConfirmClearDialogFragment extends DialogFragment {
}

2. Override the onCreateDialog method and return a new AlertDialog:

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    return new AlertDialog.Builder(getActivity())
    .setTitle(R.string.confirm_clear_all_title)
    .setMessage(R.string.confirm_clear_all_message)
    .setPositiveButton(R.string.ok, null)
    .setNegativeButton(R.string.cancel, null)
    .create();
}

Here, an AlertDialog.Builder class is used to create the AlertDialog that will be returned by the onCreateDialog method. Note that it uses the string resources defined in strings.xml to set the title, message, and button text of the dialog. In this example, the buttons are set to do nothing when clicked by setting the onClickListener to null. You will learn more about handling events using click listeners in the next section.

3. Add the following code to the onCreate method to create the dialog and display it to the user:

FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentByTag("dialog") == null) {
    ConfirmClearDialogFragment frag = ConfirmClearDialogFragment.newInstance(mTimeListAdapter);
    frag.show(fm, "dialog");
}

If you run the app now, the dialog should appear immediately. Dismiss it by pressing any button. Fragments let you decompose your application into reusable components, such as this dialog. You’ll learn more about fragments in Chapter 5.


image Note

Fragments, including the DialogFragment, require a public no-argument constructor. Failing to provide one will result in odd behavior in your app.


Handling Events

Like most GUI frameworks, Android uses an event-based model to handle user interaction. When the user taps the screen, a touch event is fired and the corresponding onTouch method of the tapped view is called. By extending the views in your UI, you can receive these events and use them to build gestures into your app; similar methods exist for handling focus and key change events.

These event callbacks form the basis of Android event handling. However, extending every view in your UI is not practical. Further, the low-level nature of the events requires work to implement simple interactions. For this reason, Android has a number of convenience methods for registering listeners on the existing View class. These listeners provide callback interfaces that will be called when common interactions, like tapping the screen, are triggered. To handle these common events, you register an event listener on a view. The listener will be called when the event occurs on that view. You generally want to register the listener on the specific view the user will interact with. For example, if you have a LinearLayout container with three buttons inside, you would register the listeners on the buttons rather than the container object. You have already seen an example of this with the onClick method in the TimeTracker app.


image Note

Android event callbacks are made by the main thread (also called the UI thread). It’s important that you not block this thread, or you will trigger an Application Not Responding (ANR) error. Make sure to perform any potentially long-running operations on a separate thread.


Screen Taps

The simplest type of event is a simple tap on the screen. To listen for this event, you register an onClickListener that has a single method: onClick. This method will be called every time a user taps that view on the screen. An onClickListener can be registered on any view. In fact, a button is just a view with a background that appears tappable and that responds to focus and press events by changing state.

In the previous section, you saw an example of an AlertDialog with buttons. In that example, the button event listeners had been set to null, disabling them. You can add custom button-click actions to the dialog by creating implementations of the OnClickInterface:

AlertDialog.Builder(getActivity())
    .setTitle(R.string.confirm_clear_all_title)
    .setMessage(R.string.confirm_clear_all_message)
    .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            dialog.dismiss();
            mAdapter.clear();
        }
    })
    .setNegativeButton(R.string.cancel, null)
    .create();

This code creates a confirmation dialog. The positive button uses a click listener that will dismiss the dialog and clear the list adapter. The negative button again just clears the dialog.


image Tip

To avoid creating anonymous classes for click handling, you can implement the click interfaces in your activity and simply pass in this when registering a click listener.


Long Presses

A long press is triggered when the user taps and holds on the screen. It is used to create an alternate action for a view. This can be used to create a context-specific menu, trigger an alternate action, or drag an icon on the screen. You can think of the long press as analogous to the right-click on a traditional desktop application.

Long presses are handled by registering an onLongPressListener. Other than the name, the setup of a long-press listener is exactly the same as a standard click listener. Here is a simple example of a long press listener that displays a toast message:

View view = findViewById(R.id.my_view);
view.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {
        Toast.makeText(TimeTrackerActivity.this, "Long pressed!",  Toast.LENGTH_LONG).show();
        return false;
    }
});

The most common usage of long pressing in a UI is for creating a context menu. For those cases, you don’t create a long press listener but instead create a context-menu listener. You’ll learn more about context menus in the next section.


image Note

Android will propagate events up the view hierarchy. Returning true from an event handler will stop the propagation, as the event has been reported as handled. Make sure you want to stop handling events when you return true.


Focus Events and Key Events

The majority of Android devices have a touchscreen interface. However, Android is also designed to work on devices that use a keyboard-style input. In this case, the touch/click events don’t apply. To handle those cases, Android uses focus events and key events. Figure 3.7 shows an example of the different states of a button.

image

Figure 3.7 Different states of a button: default (left), focused (middle), pressed (right)

The focus event is triggered when a view on the screen gains focus. This happens when a user navigates to it using a trackball or the arrow keys on a keyboard. It is called again when the view loses focus; this is typically used on devices that have a trackball. When the user actually presses an action button on your view, the key event will be called. You can intercept this event using an OnKeyListener, which will trigger the onKey event when the user presses a button. You can also directly override the onKeyUp, onKeyDown, or onKeyPress methods of the View class, providing a lower level of event handling.

While these events may seem unnecessary in an age of touchscreen devices, there are important uses for focus and key events. If you’re designing apps for the Google TV platform, you will want to use these events to handle navigation in your app, because the user will likely be using a remote control. Also, properly handling focus and key events is key to adding accessibility features to your application. Users with disabilities that require screen readers or other alternate input methods will appreciate an app that is designed to work without a touch interface.

Creating Menus

All Android devices before version 3.0 include a menu button. This menu button creates an activity-specific menu that you can use to provide extra functionality in your app (Figure 3.8). This frees you to design your UI with only the most important actions and to hide optional functionality. On Android versions 3.0 and later, the menu button is generally part of the application UI; it appears as a button in the action bar. You will learn more about the action bar in Chapter 6.

image

Figure 3.8 A menu on Android 2.3


image Tip

You should take care not to provide too much functionality via the options menu. Common user actions should be available with a single touch in your UI. Users may not even know that an action is available if it is buried in a menu.


Menu Layout

Like all other Android layouts, menus can be defined using XML or Java code. It’s generally a better idea to use XML, because you can quickly create the menu options and their order without any boilerplate code.

Add a menu to the TimeTracker app to clear the current list of times:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:id="@+id/clear_all"
        android:title="@string/clear_all"/>
</menu>

The basic structure of the menu layout is quite simple: A top-level menu element contains the item elements; each item element defines a single menu option. The android:id attribute is required for each item and is how you will reference the options in your code. The android:title attribute provides the string resource name that will appear in your app. Although a title is not required, you should always provide one; otherwise, your menu option will appear as a blank space.

You can optionally assign to your menu items an icon that will be displayed alongside the text. Icons can help the user quickly understand the available options in your menu. Menus can be nested inside items, creating submenus. Figure 3.9 shows an example of a menu leading to a submenu.

image

Figure 3.9 A submenu is opened when the user taps the Submenu menu option.

There are many more options available for menu items. You should explore the range of options available and take advantage of menus in your application.

Menu Callbacks

To provide an options menu in your activity, you need to override the callback methods onCreateOptionsMenu and onOptionsItemSelected. The onCreateOptionsMenu callback is called when the user presses the menu button; this is where you create the menu by using the layout resource file. To do this, inflate the layout file by using the MenuInflater class. The following code snippet provides an example:

@Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu, menu);
        return true;
    }

Once the menu has been created, you override onOptionsItemSelected to handle the menu selection. This method is called with the menu item that the user selected. You then select the appropriate action based on the selected menu item.


The process of converting a layout XML file into a hierarchy of views is called inflating. This is typically done using the LayoutInflater class, although the MenuInflater is used for inflating menu layouts. Inflating a view is optimized by the Android resource compiler and requires a compiled XML file. You can’t inflate a generic XML file, only those contained in the R.java file.

You can inflate a view by using View.inflate method or by calling inflate on the LayoutInflator system service. You can retrieve a reference to the LayoutInflator by calling getSystemService and passing it the Context.LAYOUT_INFLATER_SERVICE string.

You only need to inflate views that are added to your layout at runtime. Calling setContentView will inflate the views in your layout for you. When using findViewById to retrieve a view, the result is already inflated.


Add an option to clear all tasks to the TimeTracker application:

1. Override the onOptionsItemSelected method:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch(item.getItemId()) {
    case R.id.clear_all:
        return true;
    default:
        return super.onOptionsItemSelected(item);
    }
}

2. Move the dialog creation code from the onCreate method. The result is a dialog confirming that the user wants to clear all the tasks:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch(item.getItemId()) {
    case R.id.clear_all:
        FragmentManager fm = getSupportFragmentManager();
        if (fm.findFragmentByTag("dialog") == null) {
            ConfirmClearDialogFragment frag = ConfirmClearDialogFragment.newInstance(mTimeListAdapter);
            frag.show(fm, "dialog");
        }
        return true;
    default:
        return super.onOptionsItemSelected(item);
    }
}

The method returns true when it has finished creating the dialog to indicate that the event has been handled and should not be propagated to any other handlers.

Context Menus

A context menu is like a right-click menu in a standard desktop computing environment. Context menus work the same way as options menus but are triggered when the user long-presses a view. To create a context menu, you set the context menu listener for a view and implement the onCreateContextMenu method. In this method, you inflate the context menu to display to the user. Finally, you implement the onContextItemSelected method to handle user selections. You register the context menu listener using either View.setOnCreateContextMenuListener or Activity.registerForContextMenu. Figure 3.10 shows an example context menu. Here is the code to create that menu:

TextView tv = (TextView) findViewById(R.id.text);
tv.setOnCreateContextMenuListener(this);
// Alternatively, use Activity.registerForContextMenu(tv);
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
        ContextMenuInfo menuInfo) {
    super.onCreateContextMenu(menu, v, menuInfo);
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.context_menu, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch(item.getItemId()) {
    case R.id.item1:
        return true;
    case R.id.item2:
        return true;
    default:
        return super.onOptionsItemSelected(item);
    }
}

image

Figure 3.10 Context menus are opened when the user long-presses a view.

Implementing the Time Tracker

Now that you’ve added a notification, a menu, and a dialog to the TimeTracker app, you should have something that looks like Figure 3.11. It still runs only while the app is in the foreground, though. To fix this, you’ll need to create a service to handle running the timer and updating the notification. A service is how you perform background tasks on Android. Its life cycle is similar to that of the activity, but it does not have a UI component. Anytime you need to execute code when the user is not actively using your app, you should create a service.

image

Figure 3.11 TimeTracker app with notification and confirm dialog


image Note

The service life cycle callbacks are run by the Android main thread. Just as with activities, you should avoid performing long-running operations in those methods. Instead, start a thread or use message handlers with background workers to perform the actual background work.


1. Create a new TimerService class that extends Service. Move all the Handler code from the TimeTrackerActivity to the TimerService, and add some convenience methods for stopping and resetting the timer:

public class TimerService extends Service {
    private static final String TAG = "TimerService";
    public static int TIMER_NOTIFICATION = 0;
    private NotificationManager mNM = null;
    private Notification mNotification = null;
    private long mStart = 0;
    private long mTime = 0;
    public class LocalBinder extends Binder {
        TimerService getService() {
            return TimerService.this;
        }
    }
    private final IBinder mBinder = new LocalBinder();
    private Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            long current = System.currentTimeMillis();
            mTime += current - mStart;
            mStart = current;
            updateTime(mTime);
            mHandler.sendEmptyMessageDelayed(0, 250);
        };
    };
    @Override
    public void onCreate() {
        Log.i(TAG, "onCreate");
        mNM = (NotificationManager)getSystemService (NOTIFICATION_SERVICE);
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Show notification when we start the timer
        showNotification();
        mStart = System.currentTimeMillis();
        // Only a single message type, 0
        mHandler.removeMessages(0);
        mHandler.sendEmptyMessage(0);
        // Keep restarting until we stop the service
        return START_STICKY;
    }
    @Override
    public void onDestroy() {
        // Cancel the ongoing notification.
        mNM.cancel(TIMER_NOTIFICATION);
        mHandler.removeMessages(0);
    }
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
    public void stop() {
        mHandler.removeMessages(0);
        stopSelf();
        mNM.cancel(TIMER_NOTIFICATION);
    }
    public boolean isStopped() {
        return !mHandler.hasMessages(0);
    }
    public void reset() {
        stop();
        timerStopped(mTime);
        mTime = 0;
    }
}

You still need to update the activity, though, so the service will need to notify the activity of the current time via the updateTime method.

2. Create the updateTime method, and inside it create a broadcast intent to send the current time:

private void updateTime(long time) {
    Intent intent = new Intent(TimeTrackerActivity. ACTION_TIME_UPDATE);
    intent.putExtra("time", time);
    sendBroadcast(intent);
}

3. Create a timerStopped method to notify the activity that the timer has finished:

private void timerStopped(long time) {
    // Broadcast timer stopped
    Intent intent = new Intent(TimeTrackerActivity. ACTION_TIMER_FINISHED);
    intent.putExtra("time", time);
    sendBroadcast(intent);
}

4. In the TimeTrackerActivity onCreate method, create an IntentFilter to be called when the intent is broadcast:

IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_TIME_UPDATE);
filter.addAction(ACTION_TIMER_FINISHED);
registerReceiver(mTimeReceiver, filter);

Now when the service updates the time, the activity will be notified and can update its counter. This will also come in handy later when you create a widget.

5. Add the notification code to the service, and call it when the timer is updated:

private Notification mNotification;
private void updateNotification(long time) {
    String title = getResources().getString (R.string.running_timer_notification_title);
    String message = DateUtils.formatElapsedTime(time/1000);
    Context context = getApplicationContext();
    Intent intent = new Intent(context, TimeTrackerActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
    mNotification.setLatestEventInfo(context, title, message, pendingIntent);
mNM.notify(TIMER_NOTIFICATION, mNotification);
}

You should now be able to run the timer in the background (Figure 3.12).

image

Figure 3.12 The ongoing timer notification is even present on the home screen.

Wrapping Up

This chapter introduced basic Android UI concepts for supporting multiple device configurations, notifications, and options menus. Along the way, you learned that

• Android uses a combination of folder naming conventions, image scaling, and density-independent dimensions to create flexible layouts for different device configurations.

• Touch, focus, and key events are available, but you’ll probably want to use an event listener to handle common user actions such as tapping on the screen.

• Notifications are the primary method of notifying your users, but dialogs and toasts can be used when you need more or less urgency.

• Menus allow you to add functionality to your app without cluttering the layout, but you should take care not to hide essential actions from the user.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.58.199