Invading the Home Screen - Professional Android 4 Application Development (2012)

Professional Android 4 Application Development (2012)

Chapter 14. Invading the Home Screen

What's in this Chapter?

Creating home screen Widgets

Creating collection-based home screen Widgets

Using Content Providers to populate Widgets

Surfacing search results to the Quick Search Box

Creating Live Wallpaper

Widgets, Live Wallpaper, and the Quick Search Box (QSB) let you populate a piece of the user's home screen, providing either a window to your application or a stand-alone source of information directly on the home screen. They're exciting innovations for users and developers:

· Users get instant access to interesting information without needing to open an application.

· Developers get an entry point to their applications directly from the home screen.

A useful home screen Widget or Live Wallpaper increases user engagement, decreasing the chance that an application will be uninstalled and increasing the likelihood of its being used. With such power comes responsibility. Widgets run constantly as subprocesses of the home screen, so you need to be particularly careful when creating them to ensure they remain responsive and don't drain system resources.

This chapter demonstrates how to create and use App Widgets and Live Wallpaper, detailing what they are, how to use them, and some techniques for incorporating interactivity into these application components. The chapter also describes how to surface search results from your application through the QSB.

Introducing Home Screen Widgets

Home screen Widgets, more properly AppWidgets, are visual application components that can be added to other applications. The most notable example is the default Android home screen, where users can add Widgets to their phone-top. This functionality is typically implemented by alternative home screen replacements, although any application can become an AppHost and support embedding third-party Widgets.

Widgets enable your application to populate a piece of interactive screen real estate, and embed an entry point, directly on the user's home screen. A good App Widget provides useful, concise, and timely information with a minimal resource cost.

Widgets can be either stand-alone applications (such as the native clock) or compact but highly visible components of larger applications—such as the Calendar and Media Player Apps Widgets.

Figure 14.1 shows five of the standard home screen Widgets available on Android devices: the Quick Search Box, Power Control, News & Weather, Media Player, and the Photo Gallery.

Figure 14.1

14.1

2.1

To add an App Widget to the home screen prior to Android 3.0, long-press a piece of empty space and select Widgets. You will be presented with a list of available Widgets to add to your home screen.

In Android 3.0 and above, App Widgets are added using the application launcher. Clicking the “Widgets” tab at the top of the launcher tray presents the list of available Widgets. Click and hold a Widget, and you will be able to position it onto your home screen.

After adding a Widget, you can move it by long-pressing it and dragging it around the screen. To resize (available in Android 3.1 and above), long-press and release. You'll see small indicators along the edges of the Widget that can be dragged to resize the Widget.

Remove Widgets by dragging them into the garbage can icon or “remove” label at the top or bottom of the screen.

Widgets embedded into an application are hosted within the parent application's process. They will wake the device based on their update rates to ensure each Widget is up to date when it's visible. As a developer, you need to take extra care when creating your Widgets to ensure that the update rate is as low as possible, and that the code executed within the update method is lightweight.

The following sections show how to create Widgets and describe some best practices for performing updates and adding interactivity.

Creating App Widgets

App Widgets are implemented as BroadcastReceivers. They use RemoteViews to define and update a view hierarchy hosted within another application process; most commonly that host process is the home screen.

To create a Widget for your application, you need to create three components:

1. An XML layout resource that defines the UI

2. An XML file that describes the meta data associated with the Widget

3. A Broadcast Receiver that defines and controls the Widget

You can create as many Widgets as you want for a single application, or you can have an application that consists of a single Widget. Each Widget can use the same size, layout, refresh rate, and update logic, or it can use different ones. In many cases it can be useful to offer multiple versions of your Widgets in different sizes.

Creating the Widget XML Layout Resource

The first step in creating your Widget is to design and implement its user interface (UI).

Construct your Widget's UI as you would other visual components in Android, as described in Chapter 4, “Building User Interfaces.” Best practice is to define your Widget layout using XML as an external layout resource, but it's also possible to lay out your UI programmatically within the Broadcast Receiver's onCreate method.

Widget Design Guidelines

Widgets are often displayed alongside other native and third-party Widgets, so it's important that yours conform to design standards—particularly because Widgets are most often used on the home screen.

There are UI design guidelines for controlling both a Widget's layout size and its visual styling. The former is enforced rigidly, whereas the latter is a guide only; both are summarized in the following sections. You can find additional detail on the Android Developers Widget Design Guidelines site, at http://developer.android.com/guide/practices/ui_guidelines/widget_design.html.

Widget Layout Sizes

The default Android home screen is divided into a grid of cells, varying in size and number depending on the device. It's best practice to specify a minimum height and width for your Widget that is required to ensure it is displayed in a good default state.

Where your minimum dimensions don't match the exact dimensions of the home screen cells, your Widget's size will be rounded up to fill the cells into which it extends.

To determine the approximate minimum height and width limits required to ensure your widget fits within a given number of cells, you can use the following formula:

Min height or width = 70dp * (cell count) – 30dp

Widget dimensions are specified in the Widget settings file, as described in the section “Defining Your Widget Settings.”

Widget Visual Styling

The visual styling of your Widget, your application's presence on the home screen, is very important. You should ensure that its style is consistent with that of your application, as well as with those of the other home screen components.

App Widgets fully support transparent backgrounds and allow the use of NinePatches and partially transparent Drawable resources. It's beyond the scope of this book to describe the Widget style promoted by Google in detail, but note the description available at the Widget UI guidelines link provided earlier.

Also note that an App Widget Template Pack is available for download from the sample page. It provides NinePatch background graphics, XML, and source Adobe Photoshop files for multiple screen densities, OS version widget styles, and widget colors. It also includes graphics that can be used within state-list Drawables to make your entire widget or parts of your widget interactive, as described later in this chapter in the “Using Remote Views to Add Widget Interactivity” section.

Supported Widget Views and Layouts

Because of security and performance considerations, there are several restrictions on the layouts and Views available for constructing a Widget UI.

The following Views are unavailable for App Widget layouts and will result in a null pointer error (NPE) if used:

· All custom Views

· Most descendant classes of allowed Views

· EditText

Currently, the layouts available are limited to the following:

· FrameLayout

· LinearLayout

· RelativeLayout

· GridLayout

The Views they contain are restricted to the following:

· AnalogClock

· Button

· Chronometer

· ImageButton

· ImageView

· ProgressBar

· TextView

· ViewFlipper

The Text Views, Image Views, and View Flippers are particularly useful. In the section “Changing Image Views Based on Selection Focus” you'll see how to use the Image View in conjunction with the SelectionStateDrawable resource to create interactive Widgets with little or no code.

Android 3.0 (API level 11) introduced Collection View Widgets, a new class of Widgets designed to display collections of data in the form of a list, grid, or stack. This Widget type is described in detail in the section “Introducing Collection View Widgets.”

Listing 14.1 shows an XML layout resource used to define the UI of an App Widget.

2.11

Listing 14.1: App Widget XML layout resource

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:padding="5dp">
  <ImageView
    android:id="@+id/widget_image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/icon"
  />
  <TextView
    android:id="@+id/widget_text"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:text="@string/widget_text"
  />
</LinearLayout>

code snippet PA4AD_Ch14_MyWidget/res/layout/my_widget_layout.xml

Defining Your Widget Settings

Widget definition resources are stored as XML in the res/xml folder of your project. The appwidget-provider tag enables you to describe the Widget meta data that defines attributes including the size, layout, and update rate for your Widget using the following attributes:

· initialLayout—The layout resource to use in constructing the Widget's UI.

· minWidth/minHeight—The minimum width and minimum height of the Widget, respectively, as described in the previous section.

· resizeMode—Android 3.1 (API level 12) introduced the concept of resizable Widgets. Setting the resize mode allows you to specify the direction in which the Widget can be resized, using a combination of horizontal and vertical, or disabling resizing by setting it to none.

· label—The title used by your Widget in the App Widget picker.

· updatePeriodMillis—The minimum period between Widget updates in milliseconds. Android will wake the device to update your Widget at this rate, so you should specify at least an hour. The App Widget Manager won't deliver updates more frequently than once every 30 minutes. Ideally your Widget shouldn't use this update technique more than once or twice daily. More details on this and other update techniques are provided later in this chapter.

· configure—You can optionally specify a fully qualified Activity to be launched when your Widget is added to the home screen. This Activity can be used to specify Widget settings and user preferences. Using a configuration Activity is described in the section “Creating and Using a Widget Configuration Activity.”

· icon—By default Android will use your application's icon when presenting your Widget within the App Widget picker. Specify a Drawable resource to use a different icon.

· previewImage—Android 3.0 (API level 11) introduced a new App Widget picker that displays a preview of Widgets rather than their icon. Specify a Drawable resource here that accurately depicts how your Widget will appear when added to the home screen.

Listing 14.2 shows the Widget resource file for a two-cell-by-two-cell Widget that updates once every hour and uses the layout resource defined in the previous section.

2.11

Listing 14.2: App Widget Provider definition

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:initialLayout="@layout/my_widget_layout"
  android:minWidth="110dp"
  android:minHeight="110dp"
  android:label="@string/widget_label"
  android:updatePeriodMillis="360000"
  android:resizeMode="horizontal|vertical"
  android:previewImage="@drawable/widget_preview"
/>

code snippet PA4AD_Ch14_MyWidget/res/xml/widget_provider_info.xml

Creating Your Widget Broadcast Receiver and Adding It to the Application Manifest

Widgets are implemented as Broadcast Receivers. Each Widget's Broadcast Receiver specifies Intent Filters to listen for broadcast Intents requesting Widget updates using the AppWidget.ACTION_APPWIDGET_UPDATE, DELETED, ENABLED, and DISABLED actions.

To create a Widget, extend the BroadcastReceiver class and implement a response to each of these broadcast Intents by overriding the onReceive method.

The AppWidgetProvider class encapsulates this Intent processing and provides you with event handlers for the update, delete, enable, and disable events.

Listing 14.3 shows a skeleton Widget implementation that extends AppWidgetProvider.

2.11

Listing 14.3: App Widget implementation

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.widget.RemoteViews;
import android.content.Context;
 
public class SkeletonAppWidget extends AppWidgetProvider {
  @Override
  public void onUpdate(Context context,
                       AppWidgetManager appWidgetManager,
                       int[] appWidgetIds) {
    // TODO Update the Widget UI.
  }
 
  @Override
  public void onDeleted(Context context, int[] appWidgetIds) {
    // TODO Handle deletion of the widget.
    super.onDeleted(context, appWidgetIds);
  }
 
  @Override
  public void onDisabled(Context context) {
    // TODO Widget has been disabled.
    super.onDisabled(context);
  }
 
  @Override
  public void onEnabled(Context context) {
    // TODO Widget has been enabled.
    super.onEnabled(context);
  }
}

code snippet PA4AD_Ch14_MyWidget/src/SkeletonAppWidget.java

Widgets must be added to the application manifest, using a receiver tag like other Broadcast Receivers. To specify a Broadcast Receiver as an App Widget, you need to add the following two tags to its manifest node, as shown in Listing 14.4.

· An Intent Filter for the android.appwidget.action.APPWIDGET_UPDATE action

· A reference to the appwidget-provider meta data XML resource, described in the previous section, that describes your Widget settings

2.11

Listing 14.4: App Widget manifest node

<receiver android:name=".MyAppWidget" android:label="@string/widget_label">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <meta-data
    android:name="android.appwidget.provider"
    android:resource="@xml/widget_provider_info"
  />
</receiver>

code snippet PA4AD_Ch14_MyWidget/AndroidManifest.xml

Introducing the App Widget Manager and Remote Views

The AppWidgetManager class is used to update App Widgets and provide details related to them.

The RemoteViews class is used as a proxy to a View hierarchy hosted within another application's process. This lets you change a property, or run a method, on a View running within another application. For example, the UIs used by your App Widgets are hosted within their host process (typically the home screen). To modify those Views from the App Widget Provider running in your application's process, use Remote Views.

You can modify the appearance of the Views that form your App Widget UI by creating and modifying Remote Views and applying those changes using the App Widget Manager. Supported modifications include changing a Views visibility, text, or image values, and adding Click Listeners.

This section describes how to create new Remote Views and in particular how to use them within onUpdate method of an App Widget Provider. It also demonstrates how to use Remote Views to update your App Widget UI and add interactivity to your Widgets.

Creating and Manipulating Remote Views

To create a new Remote View object, pass the name of your application's package, and the layout resource you plan to manipulate, into the Remote Views constructor, as shown in Listing 14.5.

Listing 14.5: Creating Remote Views

RemoteViews views = new RemoteViews(context.getPackageName(),
                                    R.layout.my_widget_layout);

code Snippet PA4AD_Ch14_MyWidget/src/MyAppWidget.java

Remote Views represent a View hierarchy displayed in another process—in this instance, it will be used to define a set of changes to be applied to the UI of a running Widget.

2.1

The section “Applying Remote Views to Running App Widgets” describes how to use the App Widget Manager to apply the changes you made in this section to App Widgets. The modifications you apply here will not affect the running instances of your Widgets until you apply them.

Remote Views include a series of methods that provide access to many of the properties and methods available on native Views. The most versatile of these is a collection of set methods that let you specify a target method name to execute on a remotely hosted View. These methods support the passing of a single-value parameter, one for each primitive type, including Boolean, integer, byte, char, and float, as well as strings, bitmaps, Bundles, and URI parameters.

Listing 14.6 shows examples of some of the method signatures supported.

2.11

Listing 14.6: Using a Remote View to apply methods to Views within an App Widget

// Set the image level for an ImageView.
views.setInt(R.id.widget_image_view, "setImageLevel", 2);
// Show the cursor of a TextView.
views.setBoolean(R.id.widget_text_view, "setCursorVisible", true);
// Assign a bitmap to an ImageButton.
views.setBitmap(R.id.widget_image_button, "setImageBitmap", myBitmap);

code snippet PA4AD_Ch14_MyWidget/src/FullAppWidget.java

A number of methods specific to certain View classes are also available, including methods to modify Text Views, Image Views, Progress Bars, and Chronometers.

Listing 14.7 shows examples of some of these specialist methods:

Listing 14.7: Modifying View properties within an App Widget Remote View

// Update a Text View
views.setTextViewText(R.id.widget_text, "Updated Text");
views.setTextColor(R.id.widget_text, Color.BLUE);
// Update an Image View
views.setImageViewResource(R.id.widget_image, R.drawable.icon);
// Update a Progress Bar
views.setProgressBar(R.id.widget_progressbar, 100, 50, false);
// Update a Chronometer
views.setChronometer(R.id.widget_chronometer,
  SystemClock.elapsedRealtime(), null, true);

code snippet PA4AD_Ch14_MyWidget/src/FullAppWidget.java

You can set the visibility of any View hosted within a Remote Views layout by calling setViewVisibility, as shown here:

views.setViewVisibility(R.id.widget_text, View.INVISIBLE);

Remember that so far you have modified the Remote Views object that represents the View hierarchy within the App Widget, but you have not you applied it. For your changes to take effect, you must use the App Widget Manager to apply your updates.

Applying Remote Views to Running App Widgets

To apply the changes you make to the Remote Views to active Widgets, use the App Widget Manager's updateAppWidget method, passing in the identifiers of one or more Widgets to update and the Remote View to apply.

appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);

If you're updating your App Widget UI from within an App Widget Provider's update handler, the process is simple. The onUpdate handler receives the App Widget Manager and the array of active App Widget instance IDs as parameters, allowing you to follow the pattern shown in Listing 14.8.

2.11

Listing 14.8: Using a Remote View within the App Widget Provider's onUpdate Handler

@Override
public void onUpdate(Context context,
                     AppWidgetManager appWidgetManager,
                     int[] appWidgetIds) {
  // Iterate through each widget, creating a RemoteViews object and
  // applying the modified RemoteViews to each widget.
  final int N = appWidgetIds.length;
  for (int i = 0; i < N; i++) {
    int appWidgetId = appWidgetIds[i];
 
    // Create a Remote View
    RemoteViews views = new RemoteViews(context.getPackageName(),
                                        R.layout.my_widget_layout);
 
    // TODO Update the UI.
 
    // Notify the App Widget Manager to update the widget using
    // the modified remote view.
    appWidgetManager.updateAppWidget(appWidgetId, views);
  }
}

code snippet PA4AD_Ch14_MyWidget/src/MyAppWidget.java

2.1

It's best practice to iterate over the Widget ID array. This enables you to apply different UI values to each Widget based on its identifier and associated configuration settings.

You can also update your Widgets directly from a Service, Activity, or Broadcast Receiver. To do so, get a reference to the App Widget Manager by calling its static getInstance method, passing in the current context, as shown in Listing 14.9.

2.11

Listing 14.9: Accessing the App Widget Manager

// Get the App Widget Manager.
AppWidgetManager appWidgetManager 
  = AppWidgetManager.getInstance(context);

code snippet PA4AD_Ch14_MyWidget/src/MyReceiver.java

You can then use the getAppWidgetIds method on your App Widget Manager instance to find identifiers representing each currently running instance of the specified App Widget, as shown in this extension to Listing 14.9:

// Retrieve the identifiers for each instance of your chosen widget.
ComponentName thisWidget = new ComponentName(context, MyAppWidget.class);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);

To update the active Widgets, you can follow the same pattern described in Listing 14.8, as shown in Listing 14.10.

Listing 14.10: A standard pattern for updating Widget UI

final int N = appWidgetIds.length;
// Iterate through each widget, creating a RemoteViews object and
// applying the modified RemoteViews to each widget.
for (int i = 0; i < N; i++) {
  int appWidgetId = appWidgetIds[i];
  // Create a Remote View
  RemoteViews views = new RemoteViews(context.getPackageName(),
                                      R.layout.my_widget_layout);
 
  // TODO Update the widget UI using the views object.
 
  // Notify the App Widget Manager to update the widget using
  // the modified remote view.
  appWidgetManager.updateAppWidget(appWidgetId, views);
}

code snippet PA4AD_Ch14_MyWidget/src/MyReceiver.java

Using Remote Views to Add Widget Interactivity

App Widgets inherit the permissions of the processes within which they run, and most home screen apps run with full permissions, making the potential security risks significant. As a result of these security implications, Widget interactivity is carefully controlled.

Widget interaction is generally limited to the following:

· Adding a Click Listener to one or more Views

· Changing the UI based on selection changes

· Transitioning between Views within a Collection View Widget

2.1

There is no supported technique for entering text directly into an App Widget. If you need text input from your Widget, best practice is to add a Click Listener to the Widget that displays an Activity that accepts input.

Using a Click Listener

The simplest and most powerful way to add interactivity to your Widget is by adding a Click Listener to its Views. This is done using the setOnClickPendingIntent method on a Remote Views object.

Use this method to specify a Pending Intent that will be fired when the user clicks the specified View, as shown in Listing 14.11.

2.11

Listing 14.11: Adding a Click Listener to an App Widget

Intent intent = new Intent(context, MyActivity.class);
PendingIntent pendingIntent = 
  PendingIntent.getActivity(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.widget_text, pendingIntent);

code snippet PA4AD_Ch14_MyWidget/src/MyAppWidget.java

Pending Intents (described in more detail in Chapter 5, “Intents and Broadcast Receivers”) can contain Intents used to start Activities or Services, or that broadcast Intents.

Using this technique you can add Click Listeners to one or more of the Views used within your Widget, potentially providing support for multiple actions.

For example, the standard Media Player Widget assigns different broadcast Intents to several buttons, providing playback control through the play, pause, and next buttons.

2.1

When Pending Intents are broadcast, the Intents they wrap operate under the same permissions as the application that created them. In the case of Widgets, that means your application rather than the host process.

Changing Image Views Based on Selection Focus

Image Views are one of the most flexible Views available for your Widget UI, providing support for some basic user interactivity.

Using a SelectionStateDrawable resource (described in Chapter 3) you can create a Drawable resource that displays a different image based on the selection state of the View it is assigned to. By using a Selection State Drawable in your Widget design, you can create a dynamic UI that highlights the users' selection as they navigates though the Widget's controls and makes selections.

This is particularly important to ensure your Widget can be used with the trackball or D-pad in addition to a touch screen:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_window_focused="false"
        android:drawable="@drawable/widget_bg_normal"/>
  <item android:state_focused="true"
        android:drawable="@drawable/widget_bg_selected"/>
  <item android:state_pressed="true"
        android:drawable="@drawable/widget_bg_pressed"/>
  <item android:drawable="@drawable/widget_bg_normal"/>
</selector>

The referenced Drawable resources should be stored in low, medium, high, and extra high resolution in the application's res/drawable-[ldpi/mdpi/hdpi/xhdpi] folders, respectively. The selection state XML file should be placed in the res/drawable folder.

You can then use the Selection State Drawable directly as the source for an Image View or as the background image for any Widget View.

Refreshing Your Widgets

Widgets are most commonly displayed on the home screen, so it's important that they're always kept relevant and up to date. It's just as important to balance that relevance with your Widget's impact on system resources—particularly battery life.

The following sections describe several techniques for managing your Widget's refresh intervals.

Using the Minimum Update Rate

The simplest, but potentially most resource-intensive, technique is to set the minimum update rate for a Widget using the updatePeriodMillis attribute in the Widget's XML App Widget Provider Info definition. This is demonstrated in Listing 14.12, where the Widget is updated once every hour.

2.11

Listing 14.12: Setting the App Widget minimum update rate

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:initialLayout="@layout/my_widget_layout"
  android:minWidth="110dp"
  android:minHeight="110dp"
  android:label="@string/widget_label"
  android:resizeMode="horizontal|vertical"
  android:previewImage="@drawable/widget_preview"
  android:updatePeriodMillis="3600000"
/>

code snippet PA4AD_Ch14_MyWidget/res/xml/widget_provider_info.xml

Setting this value will cause the device to broadcast an Intent requesting an update of your Widget at the rate specified.

2.1

The host device will wake up to complete these updates, meaning they are completed even when the device is on standby. This has the potential to be a significant resource drain, so it's very important to consider the implications of your update rate. In most cases the system will not broadcast a minimum update broadcast more frequently than every 30 minutes.

This technique should be used to define the absolute minimum rate at which your Widget must be updated to remain useful. Generally, the update rate should be a minimum of an hour and ideally not more than once or twice a day.

If your Widget requires more frequent updates, consider using one of the techniques described in the following sections to perform updates using either a more efficient scheduled model using Alarms or an event/Intent-driven model.

Using Intents

App Widgets are implemented as Broadcast Receivers, so you can trigger updates and UI refreshes by registering Intent Filters against them that listen for additional Broadcast Intents. This is a dynamic approach to refreshing your Widget that uses a more efficient event model rather than the potentially battery-draining method of specifying a short minimum refresh rate.

The XML snippet in Listing 14.13 assigns a new Intent Filter to the manifest entry of the Widget defined earlier.

2.11

Listing 14.13: Listening for Broadcast Intents within App Widgets

<receiver android:name=".MyAppWidget"
  android:label="@string/widget_label">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <intent-filter>
    <action android:name="com.paad.mywidget.FORCE_WIDGET_UPDATE" />
  </intent-filter>
  <meta-data
    android:name="android.appwidget.provider"
    android:resource="@xml/widget_provider_info"
  />
</receiver>

code snippet PA4AD_Ch14_MyWidget/AndroidManifest.xml

By updating the Widget's onReceive method handler, as shown in Listing 14.14, you can listen for this new Broadcast Intent and use it to update your Widget.

2.11

Listing 14.14: Updating App Widgets based on Broadcast Intents

public static String FORCE_WIDGET_UPDATE =
  "com.paad.mywidget.FORCE_WIDGET_UPDATE";
 
@Override
public void onReceive(Context context, Intent intent) {
  super.onReceive(context, intent);
 
  if (FORCE_WIDGET_UPDATE.equals(intent.getAction())) {
    // TODO Update widget
  }
}

code snippet PA4AD_Ch14_MyWidget/src/MyAppWidget.java

This approach is particularly useful for reacting to system, user, or application events—like a data refresh, or a user action such as clicking buttons on the Widget itself. You can also register for system event broadcasts such as changes to network connectivity, battery level, or screen brightness. By relying on existing events to trigger UI updates, you minimize the impact of Widget updates while maintaining a fresh UI.

You can also leverage this technique to trigger an update of your Widget at any time by broadcasting an Intent using the action specified in your Intent Filter, as shown in Listing 14.15.

Listing 14.15: Broadcasting an Intent to update an App Widget

sendBroadcast(new Intent(MyAppWidget.FORCE_WIDGET_UPDATE));

code snippet PA4AD_Ch14_MyWidget/src/MyActivity.java

Using Alarms

Alarms, covered in detail in Chapter 9, “Working in the Background,” provide a flexible way to schedule regular events within your application. Using Alarms, you can poll at regular intervals using the Intent-based update technique described in the previous section to trigger regular Widget updates.

Unlike the minimum refresh rate, Alarms can be configured to trigger only when the device is already awake, providing a more efficient alternative when regular updates are required.

Using Alarms to refresh your Widgets is similar to using the Intent-driven model described previously. Add a new Intent Filter to the manifest entry for your Widget, and override its onReceive method to identify the Intent that triggered it. Within your application, use the Alarm Manager to create an Alarm that fires an Intent with the registered action.

Like the minimum update rate, Alarms can be set to wake the device when they trigger—making it important to minimize their use to conserve battery life.

One alternative is to use either the RTC or ELAPSED_REALTIME modes when constructing your Alarm. These modes configure an Alarm to trigger at a set time or after a specified interval has elapsed, but only if the device is awake.

Listing 14.16 shows how to schedule a repeating Alarm that broadcasts an Intent used to force a Widget update.

2.11

Listing 14.16: Updating a Widget using a nonwaking repeating Alarm

PendingIntent pi = PendingIntent.getBroadcast(context, 0, 
  new Intent(MyAppWidget.FORCE_WIDGET_UPDATE), 0);
 
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME,
                          AlarmManager.INTERVAL_HOUR,
                          AlarmManager.INTERVAL_HOUR,
                          pi);

code snippet PA4AD_Ch14_MyWidget/src/MyActivity.java

Using this technique will ensure your Widget is updated regularly, without draining the battery unnecessarily by refreshing the UI when the screen is off.

A better approach is to use inexact repeating Alarms, as shown in this modification to Listing 14.16:

alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
                                 AlarmManager.INTERVAL_HOUR,
                                 AlarmManager.INTERVAL_HOUR,
                                 pi);

As described in Chapter 9, the inexact repeating Alarm will optimize the Alarm triggers by phase-shifting all the Alarms scheduled to occur at similar times. This ensures the device is awakened only once, rather than potentially several times within a few minutes.

Creating and Using a Widget Configuration Activity

In many cases it's useful for users to have the opportunity to configure a Widget before adding it to their home screen. Done properly, you can make it possible for users to add multiple instances of the same Widget to their home screen.

An App Widget configuration Activity is launched immediately when a Widget is added to the home screen. It can be any Activity within your application, provided it has an Intent Filter for the APPWIDGET_CONFIGURE action, as shown in Listing 14.17.

2.11

Listing 14.17: App Widget configuration Activity manifest entry

<activity android:name=".MyWidgetConfigurationActivity">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
  </intent-filter>
</activity>

code snippet PA4AD_Ch14_MyWidget/AndroidManifest.xml

To assign a configuration Activity to a Widget, you must add it to the Widget's App Widget Provider Info settings file using the configure tag. The activity must be specified by its fully qualified package name, as shown here:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:initialLayout="@layout/my_widget_layout"
  android:minWidth="146dp"
  android:minHeight="146dp"
  android:label="My App Widget"
  android:updatePeriodMillis="360000" 
  android:configure=
    "com.paad.PA4AD_Ch14_MyWidget.MyWidgetConfigurationActivity"
/>

The Intent that launches the configuration Activity will include an EXTRA_APPWIDGET_ID extra that provides the ID of the App Widget being configured.

Within the Activity, provide a UI to allow the user to complete the configuration and confirm. At this stage the Activity should result to RESULT_OK and return an Intent. The returned Intent must include an extra that describes the ID of the Widget being configured using the EXTRA_APPWIDGET_IDconstant. This skeleton pattern is shown in Listing 14.18.

2.11

Listing 14.18: Skeleton App Widget configuration Activity

private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
 
@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
 
  Intent intent = getIntent();
  Bundle extras = intent.getExtras();
  if (extras != null) {
    appWidgetId = extras.getInt(
      AppWidgetManager.EXTRA_APPWIDGET_ID, 
      AppWidgetManager.INVALID_APPWIDGET_ID);
  }
 
  // Set the result to canceled in case the user exits
  // the Activity without accepting the configuration
  // changes / settings.
  setResult(RESULT_CANCELED, null);
 
  // Configure the UI.
}
 
private void completedConfiguration() {
  // Save the configuration settings for the Widget ID
 
  // Notify the Widget Manager that the configuration has completed.
  Intent result = new Intent();
  result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
  setResult(RESULT_OK, result);
  finish();
}

code snippet PA4AD_Ch14_MyWidget/src/ MyWidgetConfigurationActivity.java

Creating an Earthquake Widget

The following instructions, which extend the Earthquake application from Chapter 13, “Maps, Geocoding, and Location-Based Services,” show you how to create a new home screen Widget to display details for the latest earthquake. The UI for this Widget is simple to the point of being inane—a side effect of keeping the example as concise as possible. It does not conform to the Widget style guidelines. When completed and added to the home screen, your Widget will appear, as shown in Figure 14.2.

Figure 14.2

14.2

Using a combination of the update techniques described previously, this Widget listens for Broadcast Intents that announce an update has been performed and sets the minimum update rate to ensure it is updated once per day regardless.

1. Start by creating the layout for the Widget UI as an XML resource. Save the quake_widget.xml file in the res/layout folder. Use a Linear Layout to configure Text Views that display the quake magnitude and location:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="#F111"
  android:padding="5dp">
  <TextView
    android:id="@+id/widget_magnitude"
    android:text="---"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:textSize="24sp"
    android:padding="3dp"
    android:gravity="center_vertical"
  />
  <TextView
    android:id="@+id/widget_details"
    android:text="Details Unknown"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:textSize="14sp"
    android:padding="3dp"
    android:gravity="center_vertical"
  />
</LinearLayout>

2. Create a stub for a new EarthquakeWidget class that extends AppWidgetProvider. You'll return to this class to update your Widget with the latest quake details.

package com.paad.earthquake;
 
import android.widget.RemoteViews;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
 
public class EarthquakeWidget extends AppWidgetProvider {
}

3. Create a new Widget definition file, quake_widget_info.xml, and place it in the res/xml folder. Set the minimum update rate to once a day and set the Widget dimensions to two cells wide and one cell high—110dp × 40dp. Use the Widget layout you created in step 1 for the initial layout.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:initialLayout="@layout/quake_widget"
  android:minWidth="110dp"
  android:minHeight="40dp"
  android:label="Earthquakes"
  android:updatePeriodMillis="8640000"
/>

4. Add your Widget to the application manifest, including a reference to the Widget definition resource you created in step 3, and registering an Intent Filter for the App Widget update action.

<receiver android:name=".EarthquakeWidget" android:label="Earthquake">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <meta-data
    android:name="android.appwidget.provider"
    android:resource="@xml/quake_widget_info"
  />
</receiver>

5. Your Widget is now configured and will be available to add to the home screen. You need to update the EarthquakeWidget class from step 2 to update the Widget to display the details of the latest earthquake.

5.1 Start by creating a method stub that takes an App Widget Manager and an array of Widget IDs as well as the context. Later you'll extend this stub to update the Widget appearance using Remote Views.

public void updateQuake(Context context, 
                        AppWidgetManager appWidgetManager,
                        int[] appWidgetIds) {
}

5.2 Create a second method stub that takes only the context, using that to obtain an instance of the AppWidgetManager. Then use the App Widget Manager to find the Widget IDs of the active Earthquake Widgets, passing both into the method you created in step 5.1.

public void updateQuake(Context context) {
  ComponentName thisWidget = new ComponentName(context,
                                               EarthquakeWidget.class);
  AppWidgetManager appWidgetManager = 
     AppWidgetManager.getInstance(context);
  int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
  updateQuake(context, appWidgetManager, appWidgetIds);
}

5.3 Within the updateQuake stub from step 5.1, use the Earthquake Content Provider created in Chapter 8 to retrieve the newest quake and extract its magnitude and location:

public void updateQuake(Context context,
                        AppWidgetManager appWidgetManager,
                        int[] appWidgetIds) {
 
  Cursor lastEarthquake;
  ContentResolver cr = context.getContentResolver();
  lastEarthquake = cr.query(EarthquakeProvider.CONTENT_URI,
                            null, null, null, null);
 
  String magnitude = "--";
  String details = "-- None --";
 
  if (lastEarthquake != null) {
    try {
      if (lastEarthquake.moveToFirst()) {
        int magColumn
          = lastEarthquake.getColumnIndexOrThrow(EarthquakeProvider.KEY_MAGNITUDE);
        int detailsColumn
          = lastEarthquake.getColumnIndexOrThrow(EarthquakeProvider.KEY_DETAILS);
 
        magnitude = lastEarthquake.getString(magColumn);
        details = lastEarthquake.getString(detailsColumn);
      }
    }
    finally {
      lastEarthquake.close();
    }
  }
}

5.4 Create a new RemoteViews object to set the text displayed by the Widget's Text View elements to show the magnitude and location of the last quake:

public void updateQuake(Context context,
                        AppWidgetManager appWidgetManager,
                        int[] appWidgetIds) {
 
  Cursor lastEarthquake;
  ContentResolver cr = context.getContentResolver();
  lastEarthquake = cr.query(EarthquakeProvider.CONTENT_URI,
                            null, null, null, null);
 
  String magnitude = "--";
  String details = "-- None --";
 
  if (lastEarthquake != null) {
    try {
      if (lastEarthquake.moveToFirst()) {
        int magColumn
          = lastEarthquake.getColumnIndexOrThrow(EarthquakeProvider.KEY_MAGNITUDE);
        int detailsColumn
          = lastEarthquake.getColumnIndexOrThrow(EarthquakeProvider.KEY_DETAILS);
 
        magnitude = lastEarthquake.getString(magColumn);
        details = lastEarthquake.getString(detailsColumn);
      }
    }
    finally {
      lastEarthquake.close();
    }
  }
 
  final int N = appWidgetIds.length;
  for (int i = 0; i < N; i++) {
    int appWidgetId = appWidgetIds[i];
    RemoteViews views = new RemoteViews(context.getPackageName(),
                                        R.layout.quake_widget);
    views.setTextViewText(R.id.widget_magnitude, magnitude);
    views.setTextViewText(R.id.widget_details, details);
    appWidgetManager.updateAppWidget(appWidgetId, views);
  }
 
}

6. Override the onUpdate handler to call updateQuake:

@Override
public void onUpdate(Context context,
                     AppWidgetManager appWidgetManager,
                     int[] appWidgetIds) {
 
  // Update the Widget UI with the latest Earthquake details.
  updateQuake(context, appWidgetManager, appWidgetIds);
}

Your Widget is now ready to be used and will update with new earthquake details when added to the home screen and once every 24 hours thereafter.

7. Now further enhance the Widget to update whenever the Earthquake Update Service you created in Chapter 9, has refreshed the earthquake database:

7.1 Start by updating the onHandleIntent handler in the EarthquakeUpdateService to broadcast an Intent when it has completed:

public static String QUAKES_REFRESHED = 
  "com.paad.earthquake.QUAKES_REFRESHED";
@Override
protected void onHandleIntent(Intent intent) {
  refreshEarthquakes();
  sendBroadcast(new Intent(QUAKES_REFRESHED));
}

7.2 Override the onReceive method in the EarthquakeWidget class by adding a check for the QUAKES_REFRESHED action you broadcast in step 7.1—calling updateQuakes when it's received. Be sure to call through to the superclass to ensure that the standard Widget event handlers are still triggered:

@Override
public void onReceive(Context context, Intent intent){
  super.onReceive(context, intent);
 
  if (EarthquakeUpdateService.QUAKES_REFRESHED.equals(intent.getAction()))
    updateQuake(context);
}

7.3 Add an Intent Filter for this Intent action to the Widget's manifest entry:

<receiver android:name=".EarthquakeWidget" android:label="Earthquake">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <intent-filter>
    <action android:name="com.paad.earthquake.QUAKES_REFRESHED" />
  </intent-filter>
  <meta-data
    android:name="android.appwidget.provider"
    android:resource="@xml/quake_widget_info"
  />
</receiver
> 

8. As a final step, add some interactivity to the Widget. Return to the onUpdate handler, and add a Click Listener to both the Text Views. Clicking the Widget should open the main Activity.

@Override
public void onUpdate(Context context,
                     AppWidgetManager appWidgetManager,
                     int[] appWidgetIds) {
 
  // Create a Pending Intent that will open the main Activity.
  Intent intent = new Intent(context, Earthquake.class);
  PendingIntent pendingIntent =
    PendingIntent.getActivity(context, 0, intent, 0);
 
  // Apply the On Click Listener to both Text Views.
  RemoteViews views = new RemoteViews(context.getPackageName(),
                                      R.layout.quake_widget);
 
  views.setOnClickPendingIntent(R.id.widget_magnitude, pendingIntent);
  views.setOnClickPendingIntent(R.id.widget_details, pendingIntent);
 
  // Notify the App Widget Manager to update the
  appWidgetManager.updateAppWidget(appWidgetIds, views);
 
  // Update the Widget UI with the latest Earthquake details.
  updateQuake(context, appWidgetManager, appWidgetIds);
} 

Your Widget will now update once per day and every time the Earthquake Update Service performs an update.

2.1

All code snippets in this example are part of the Chapter 14 Earthquake Part 1 project, available for download at www.wrox.com.

Introducing Collection View Widgets

Android 3.0 (API level 11) introduced Collection View Widgets, a new style of Widgets designed to display collections of data as lists, grids, or stacks, as shown in Figure 14.3.

Figure 14.3

14.3

As the name suggests, Collection View Widgets are designed to add support for collection-based Views specifically as follows:

· StackView—A flip-card style View that displays its child Views as a stack. The stack will automatically rotate through its collection, moving the topmost item to the back to reveal the one beneath it. Users can manually transition between items by swiping up or down to reveal the previous or next items, respectively.

· ListView—The traditional List View. Each item in the bound collection is displayed as a row on a vertical list.

· GridView—A two-dimensional scrolling grid where each item is displayed within a cell. You can control the number of columns, their width, and relevant spacing.

2.1

The introduction of these dynamic collection-based App Widgets has eliminated the need for the more limited Live Folders. As a result, Live Folders have been deprecated as of Android 3.0.

Each of these controls extends the Adapter View class. As a result, the UI used to display each item within it is defined using whatever layout you provide. Depending on the View used to display the collection, the specified layout will represent each row in a list, each card in a stack, or each cell in a grid.

The UI used to represent each item is restricted to the same Views and layouts supported by App Widgets:

· FrameLayout

· LinearLayout

· RelativeLayout

· AnalogClock

· Button

· Chronometer

· ImageButton

· ImageView

· ProgressBar

· TextView

· ViewFlipper

Collection View Widgets can be used to display any collection of data, but they're particularly useful for creating dynamic Widgets that surface data held within your application's Content Providers.

Collection View Widgets are implemented in much the same way as regular App Widgets—using App Widget Provider Info files to configure the Widget settings, BroadcastReceivers to define their behavior, and RemoteViews to modify the Widgets at run time.

In addition, collection-based App Widgets require the following components:

· An additional layout resource that defines the UI for each item displayed within the Widget.

· A RemoteViewsFactory that acts as a de facto Adapter for your Widget by supplying the Views that will be displayed within your collection View. It creates the Remote Views using the item layout definition and populates its elements using the underlying data you want to display.

· A RemoteViewsService that instantiates and manages the Remote Views Factory.

With these components complete, you can use the Remote Views Factory to create and update each of the Views that will represent the items in your collection. You can automate this process by creating a Remote View and using the setRemoteAdapter method to assign the Remote Views Service to it. When the Remote View is applied to the collection Widget, the Remote Views Service will create and update each item, as necessary. This process is described in the section “Populating Collection View Widgets Using a Remote Views Service.”

Creating Collection View Widget Layouts

Collection View Widgets require two layout definitions—one that includes the Stack, List, or Grid View, and another that describes the layout to be used by each item within the stack, list, or grid.

As with regular App Widgets, it's best practice to define your layouts as external XML layout resources, as shown in Listing 14.19.

2.11

Listing 14.19: Defining the Widget layout for a Stack Widget

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:padding="5dp">
  <StackView
    android:id="@+id/widget_stack_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
  />
</FrameLayout> 

code snippet PA4AD_Ch14_MyWidget/res/layout/my_stack_widget_layout.xml

Listing 14.20 shows an example layout resource used to describe the UI of each card displayed by the Stack View Widget.

2.11

Listing 14.20: Defining the layout for each item displayed by the Stack View Widget

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="#FF555555"
  android:padding="5dp">
  <TextView
    android:id="@+id/widget_text"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:gravity="center_horizontal"
    android:text="@string/widget_text"
  />
  <TextView
    android:id="@+id/widget_title_text"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_above="@id/widget_text"
    android:textSize="30sp"
    android:gravity="center"
   android:text="---"
  />
</RelativeLayout>

code snippet PA4AD_Ch14_MyWidget/res/layout/my_stack_widget_item_layout.xml

The Widget layout is used within the App Widget Provider Info resource as it would be for any App Widget. The item layout is used by a Remote Views Factory to create the Views used to represent each item in the underlying collection.

Creating the Remote Views Service

The Remote Views Service is used as a wrapper that instantiates and manages a Remote Views Factory, which, in turn, is used to supply each of the Views displayed within the Collection View Widget.

To create a Remote Views Service, extend the RemoteViewsService class and override the onGetViewFactory handler to return a new instance of a Remote Views Factory, as shown in Listing 14.21.

2.11

Listing 14.21: Creating a Remote Views Service

import java.util.ArrayList;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
 
public class MyRemoteViewsService extends RemoteViewsService {
  
  @Override
  public RemoteViewsFactory onGetViewFactory(Intent intent) {
    return new MyRemoteViewsFactory(getApplicationContext(), intent);
  }
 
}

code snippet PA4AD_Ch14_MyWidget/src/MyRemoteViewsService.java

As with any Service, you'll need to add your Remote Views Service to your application manifest using a service tag. To prevent other applications from accessing your Widgets, you must specify the android.permission.BIND_REMOTEVIEWS permission, as shown in Listing 14.22.

Listing 14.22: Adding a Remote Views Service to the manifest

<service android:name=".MyRemoteViewsService"
         android:permission="android.permission.BIND_REMOTEVIEWS">
</service>

code snippet PA4AD_Ch14_MyWidget/AndroidManifest.xml

Creating a Remote Views Factory

The RemoteViewsFactory acts as a thin wrapper around the Adapter class. It is where you create and populate the Views that will be displayed in the Collection View Widget—effectively binding them to the underlying data collection.

To implement your Remote Views Factory, extend the RemoteViewsFactory class. This is normally done within the enclosing Remote Views Service class.

Your implementation should mirror that of a custom Adapter that will populate the Stack, List, or Grid View. Listing 14.23 shows a simple implementation of a Remote Views Factory that uses a static Array List to populate its Views. Note that the Remote Views Factory doesn't need to know what kind of Collection View Widget will be used to display each item.

2.11

Listing 14.23: Creating a Remote Views Factory

class MyRemoteViewsFactory implements RemoteViewsFactory {
  
  private ArrayList<String> myWidgetText = new ArrayList<String>();
  private Context context;
  private Intent intent;
  private int widgetId;
  
  public MyRemoteViewsFactory(Context context, Intent intent) {
    // Optional constructor implementation. 
    // Useful for getting references to the 
    // Context of the calling widget
    this.context = context;
    this.intent = intent;
    
    widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
      AppWidgetManager.INVALID_APPWIDGET_ID);
  }
 
  // Set up any connections / cursors to your data source. 
  // Heavy lifting, like downloading data should be 
  // deferred to onDataSetChanged()or getViewAt(). 
  // Taking more than 20 seconds in this call will result
  // in an ANR.
  public void onCreate() {
    myWidgetText.add("The");
    myWidgetText.add("quick");
    myWidgetText.add("brown");
    myWidgetText.add("fox");
    myWidgetText.add("jumps");
    myWidgetText.add("over");
    myWidgetText.add("the");
    myWidgetText.add("lazy");
    myWidgetText.add("droid");
  }
 
  // Called when the underlying data collection being displayed is 
  // modified. You can use the AppWidgetManager's 
  // notifyAppWidgetViewDataChanged method to trigger this handler.
  public void onDataSetChanged() {
    // TODO Processing when underlying data has changed.
  }
  
  // Return the number of items in the collection being displayed.
  public int getCount() {
    return myWidgetText.size();
  }
 
  // Return true if the unique IDs provided by each item are stable -- 
  // that is, they don't change at run time.
  public boolean hasStableIds() {
    return false;
  }
 
  // Return the unique ID associated with the item at a given index.
  public long getItemId(int index) {
    return index;
  }
 
  // The number of different view definitions. Usually 1.
  public int getViewTypeCount() {
    return 1;
  }
 
  // Optionally specify a "loading" view to display. Return null to 
  // use the default.
  public RemoteViews getLoadingView() {
    return null;
  }
 
  // Create and populate the View to display at the given index.
  public RemoteViews getViewAt(int index) {
    // Create a view to display at the required index.
    RemoteViews rv = new RemoteViews(context.getPackageName(),
      R.layout.my_stack_widget_item_layout);
    
     // Populate the view from the underlying data.
    rv.setTextViewText(R.id.widget_title_text,
                       myWidgetText.get(index));
    rv.setTextViewText(R.id.widget_text, "View Number: " + 
                       String.valueOf(index));
    
    // Create an item-specific fill-in Intent that will populate 
    // the Pending Intent template created in the App Widget Provider.
    Intent fillInIntent = new Intent();
    fillInIntent.putExtra(Intent.EXTRA_TEXT, myWidgetText.get(index));
    rv.setOnClickFillInIntent(R.id.widget_title_text, fillInIntent);
    
    return rv;
  }
 
  // Close connections, cursors, or any other persistent state you
  // created in onCreate.
  public void onDestroy() {
    myWidgetText.clear();
  }
}

code snippet PA4AD_Ch14_MyWidget/src/MyRemoteViewsService.java

Populating Collection View Widgets Using a Remote Views Service

With your Remote Views Factory complete, all that remains is to bind the List, Grid, or Stack View within your App Widget Layout to the Remote Views Service. This is done using a Remote View, typically within the onUpdate handler of your App Widget implementation.

Create a new Remote View instance as you would when updating the UI of a standard App Widget. Use the setRemoteAdapter method to bind your Remote Views Service to the particular List, Grid, or Stack View within the Widget layout.

The Remote View Service is specified using an Intent of the following form:

Intent intent = new Intent(context, MyRemoteViewsService.class);

This Intent is received by the onGetViewFactory handler within the Remote Views Service, enabling you to pass additional parameters into the Service and the Factory it contains.

You also specify the ID of the Widget you are binding to, allowing you to specify a different Service for different Widget instances.

The setEmptyView method provides a means of specifying a View that should be displayed if (and only if) the underlying data collection is empty.

After completing the binding process, use the App Widget Manager's updateAppWidget method to apply the binding to the specified Widget. Listing 14.24 shows the standard pattern for binding a Widget to a Remote Views Service.

2.11

Listing 14.24: Binding a Remove Views Service to a Widget

@Override
public void onUpdate(Context context,
                     AppWidgetManager appWidgetManager,
                     int[] appWidgetIds) {
  // Iterate through each widget, creating a RemoteViews object and
  // applying the modified RemoteViews to each widget.
  final int N = appWidgetIds.length;
  for (int i = 0; i < N; i++) {
    int appWidgetId = appWidgetIds[i];
 
    // Create a Remote View.
    RemoteViews views = new RemoteViews(context.getPackageName(),
      R.layout.my_stack_widget_layout);
    
    // Bind this widget to a Remote Views Service.
    Intent intent = new Intent(context, MyRemoteViewsService.class);
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
    views.setRemoteAdapter(appWidgetId, R.id.widget_stack_view,
                           intent);
  
    // Specify a View within the Widget layout hierarchy to display 
    // when the bound collection is empty.
    views.setEmptyView(R.id.widget_stack_view, R.id.widget_empty_text);
 
    // TODO Customize this Widgets UI based on configuration 
    // settings etc.
 
    // Notify the App Widget Manager to update the widget using
    // the modified remote view.
    appWidgetManager.updateAppWidget(appWidgetId, views);
  }
}

code snippet PA4AD_Ch14_MyWidget/src/MyStackWidget.java

Adding Interactivity to the Items Within a Collection View Widget

For efficiency reasons, it's not possible to assign a unique onClickPendingIntent to each item displayed as part of a Collection View Widget. Instead, use the setPendingIntentTemplate to assign a template Intent to your Widget, as shown in Listing 14.25.

2.11

Listing 14.25: Adding a Click Listener to individual items within a Collection View Widget using a Pending Intent

Intent templateIntent = new Intent(Intent.ACTION_VIEW);      
templateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
PendingIntent templatePendingIntent = PendingIntent.getActivity(
  context, 0, templateIntent, PendingIntent.FLAG_UPDATE_CURRENT);
      
views.setPendingIntentTemplate(R.id.widget_stack_view, 
                               templatePendingIntent);

code snippet PA4AD_Ch14_MyWidget/src/MyStackWidget.java

This Pending Intent can then be “filled-in” within the getViewAt handler of your Remote Views Service implementation using the setOnClickFillInIntent method of your Remote Views object, as shown in Listing 14.26.

Listing 14.26: Filling in a Pending Intent template for each item displayed in your Collection View Widget

// Create the item-specific fill-in Intent that will populate 
// the Pending Intent template created in the App Widget Provider.
Intent fillInIntent = new Intent();
fillInIntent.putExtra(Intent.EXTRA_TEXT, myWidgetText.get(index));
rv.setOnClickFillInIntent(R.id.widget_title_text, fillInIntent);

code snippet PA4AD_Ch14_MyWidget/src/MyRemoteViewsService.java

The fill-in Intent is applied to the template Intent using the Intent.fillIn method. It copies the contents of the fill-in Intent into the template Intent, replacing any undefined fields with those defined by the fill-in Intent. Fields with existing data will not be overridden.

The resulting Pending Intent will be broadcast when a user clicks that particular item from within your Widget.

Binding Collection View Widgets to Content Providers

One of the most powerful uses of Collection View Widgets is to surface data from your Content Providers to the home screen. Listing 14.27 shows the skeleton code for a Remote Views Factory implementation that binds to a Content Provider—in this case, one that displays the thumbnails of images stored on the external media store.

2.11

Listing 14.27: Creating a Content Provider–backed Remote Views Factory

class MyRemoteViewsFactory implements RemoteViewsFactory {
  
  private Context context;
  private ContentResolver cr;
  private Cursor c;
  
  public MyRemoteViewsFactory(Context context) {
    // Get a reference to the application context and
    // its Content Resolver.
    this.context = context;
    cr = context.getContentResolver();
  }
 
  public void onCreate() {
    // Execute the query that returns a Cursor over the data
    // to be displayed. Any secondary lookups or decoding should
    // be completed in the onDataSetChanged handler.
    c = cr.query(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, 
                 null, null, null, null);
  }
 
  public void onDataSetChanged() {
    // Any secondary lookups, processing, or decoding can be done
    // here synchronously. The Widget will be updated only after
    // this method has completed.
  }
  
  public int getCount() {
    // Return the number of items in the Cursor.
    if (c != null)
      return c.getCount();
    else
      return 0;
  }
 
  public long getItemId(int index) {
    // Return the unique ID associated with a particular item.
    if (c != null)
      return c.getInt(
        c.getColumnIndex(MediaStore.Images.Thumbnails._ID));
    else
      return index;
  }
 
  public RemoteViews getViewAt(int index) {
    // Move the Cursor to the requested row position.
    c.moveToPosition(index);
 
    // Extract the data from the required columns.
    int idIdx = c.getColumnIndex(MediaStore.Images.Thumbnails._ID);
    String id = c.getString(idIdx);
    Uri uri = Uri.withAppendedPath(
        MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, ""
        + id);
    
    // Create a new Remote Views object using the appropriate 
    // item layout
    RemoteViews rv = new RemoteViews(context.getPackageName(),
      R.layout.my_media_widget_item_layout);
    
    // Assign the values extracted from the Cursor to the Remote Views. 
    rv.setImageViewUri(R.id.widget_media_thumbnail, uri);
 
    // Assign the item-specific fill-in Intent that will populate 
    // the Pending Intent template specified in the App Widget
    // Provider. In this instance the template Intent specifies 
    // an ACTION_VIEW action.
    Intent fillInIntent = new Intent();
    fillInIntent.setData(uri);
    rv.setOnClickFillInIntent(R.id.widget_media_thumbnail, 
                              fillInIntent);
    
    return rv;
  }
 
  public int getViewTypeCount() {
    // The number of different view definitions to use.
    // For Content Providers, this will almost always be 1.
    return 1;
  }
 
  public boolean hasStableIds() {
    // Content Provider IDs should be unique and permanent.
    return true;
  }
 
  public void onDestroy() {
    // Close the result Cursor.
    c.close();
  }
 
  public RemoteViews getLoadingView() {
    // Use the default loading view.
    return null;
  }
} 

code snippet PA4AD_Ch14_MyWidget/src/MyMediaRemoteViewsService.java

This more flexible alternative for exposing Content Provider data on the home screen is a replacement for Live Folders, which have now been deprecated.

Refreshing Your Collection View Widgets

The App Widget Manager includes the notifyAppWidgetViewDataChanged method, which allows you to specify a Widget ID (or array of IDs) to update, along with the resource identifier for the collection View within that Widget whose underlying data source has changed:

appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds,
   R.id.widget_stack_view);

This will cause the onDataSetChanged handler within the associated Remote Views Factory to be executed, followed by the meta-data calls, including getCount, before each of the Views is re-created.

Alternatively, the techniques used to update App Widgets—altering the minimum refresh rate, using Intents, and setting Alarms—can also be used to update Collection View Widgets; however, they will cause the entire Widget to be re-created, meaning that refreshing the collection-based Views based on changes to the underlying data is more efficient.

Creating an Earthquake Collection View Widget

In this example you add a second Widget to the Earthquake application. This one will use a ListView-based Collection View Widget to display a list of the recent earthquakes.

1. Start by creating a layout for the Collection View Widget UI as an XML resource. Save the quake_collection_widget.xml file in the res/layout folder. Use a Frame Layout that includes the List View for displaying the earthquakes and a Text View to display when the collection is empty:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:padding="5dp">
  <ListView
    android:id="@+id/widget_list_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
  />
  <TextView
    android:id="@+id/widget_empty_text"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:text="No Earthquakes!"
  />
</FrameLayout>

2. Create a new EarthquakeListWidget class that extends AppWidgetProvider. You'll return to this class to bind your Widget to the Remote Views Service that will provide the Views that display each earthquake.

package com.paad.earthquake;
 
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
 
public class EarthquakeListWidget extends AppWidgetProvider {
}

3. Create a new Widget definition file, quake_list_widget_info.xml, in the res/xml folder. Set the minimum update rate to once a day, set the Widget dimensions to two cells wide and one cell high (110dp × 40dp), and make it vertically resizable. Use the Widget layout you created in step 1 for the initial layout.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:initialLayout="@layout/quake_collection_widget"
  android:minWidth="110dp"
  android:minHeight="40dp"
  android:label="Earthquakes"
  android:updatePeriodMillis="8640000"
  android:resizeMode="vertical"
/>

4. Add your Widget to the application manifest, including a reference to the Widget definition resource you created in step 3. It should also include an Intent Filter for the App Widget update action.

<receiver android:name=".EarthquakeListWidget" android:label="Earthquake List">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <meta-data
    android:name="android.appwidget.provider"
    android:resource="@xml/quake_list_widget_info"
  />
</receiver>

5. Create a new EarthquakeRemoteViewsService class that extends RemoteViewsService. It should include an internal EarthquakeRemoteViewsFactory class that extends RemoteViewsFactory, which should be returned from the Earthquake Remote Views Service's onGetViewFactory handler:

package com.paad.earthquake;
 
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
 
public class EarthquakeRemoteViewsService extends RemoteViewsService {
  
  @Override
  public RemoteViewsFactory onGetViewFactory(Intent intent) {
    return new EarthquakeRemoteViewsFactory(getApplicationContext());
  }
  
  class EarthquakeRemoteViewsFactory implements RemoteViewsFactory {
    
    private Context context;
 
    public EarthquakeRemoteViewsFactory(Context context) {
      this.context = context;
    }
 
    public void onCreate() {
    }
  
    public void onDataSetChanged() {
    }    
 
    public int getCount() {
      return 0;
    }
  
    public long getItemId(int index) {
      return index;
    }
  
    public RemoteViews getViewAt(int index) {
      return null;
    }
 
    public int getViewTypeCount() {
      return 1;
    }
  
    public boolean hasStableIds() {
      return true;
    }
 
    public RemoteViews getLoadingView() {
      return null;
    }
 
    public void onDestroy() {
    }
  }
}

6. Add a new variable to store the Service Context, and create a new Constructor that accepts the Context and stores it in this property:

private Context context;
 
public EarthquakeRemoteViewsFactory(Context context) {
  this.context = context;
}

7. Create a new executeCursor method to query the Earthquake Provider for the current Earthquake list. Update the onCreate handler to execute that method and store the result a new class property:

private Cursor c;
private Cursor executeQuery() {
  String[] projection = new String[] {
      EarthquakeProvider.KEY_ID,
      EarthquakeProvider.KEY_MAGNITUDE,
      EarthquakeProvider.KEY_DETAILS
    };
    
  Context appContext = getApplicationContext();
  SharedPreferences prefs = 
    PreferenceManager.getDefaultSharedPreferences(appContext);
  int minimumMagnitude = 
    Integer.parseInt(prefs.getString(PreferencesActivity.PREF_MIN_MAG, "3"));
  
  String where = EarthquakeProvider.KEY_MAGNITUDE + " > " + minimumMagnitude;
   
  return context.getContentResolver().query(EarthquakeProvider.CONTENT_URI, 
      projection, where, null, null);
}
public void onCreate() {
  c = executeQuery();
}

8. Update the onDataSetChanged and onDestroy handlers to requery and destroy the Cursor, respectively:

public void onDataSetChanged() {
  c = executeQuery();
}
 
public void onDestroy() {
  c.close();
}

9. The Earthquake Remote Views Factory supplies the Views that represent each Earthquake in the List View. Populate each of the method stubs to use the data from the earthquake Cursor to populate the View that represents each item in the list.

9.1 Start by updating the getCount and getItemId methods to return the number of records in the Cursor and the unique identifier associated with each record, respectively:

public int getCount() {
  if (c != null)
    return c.getCount();
  else
    return 0;
}
 
public long getItemId(int index) {
  if (c != null)
    return c.getLong(c.getColumnIndex(EarthquakeProvider.KEY_ID));
  else
    return index;
}

9.2 Then update the getViewAt method. This is where the Views used to represent each Earthquake in the List View are created and populated. Create a new Remote Views object using the layout definition you created for the previous Earthquake App Widget example, and populate it with data from the current earthquake. Also create and assign a fill-in Intent that will add the current earthquake's URI to the template Intent you'll define within the Widget provider.

public RemoteViews getViewAt(int index) {
  // Move the Cursor to the required index.
  c.moveToPosition(index);
  
  // Extract the values for the current cursor row.
  int idIdx = c.getColumnIndex(EarthquakeProvider.KEY_ID);
  int magnitudeIdx = c.getColumnIndex(EarthquakeProvider.KEY_MAGNITUDE);
  int detailsIdx = c.getColumnIndex(EarthquakeProvider.KEY_DETAILS);
 
  String id = c.getString(idIdx); 
  String magnitude = c.getString(magnitudeIdx); 
  String details = c.getString(detailsIdx); 
 
  // Create a new Remote Views object and use it to populate the 
  // layout used to represent each earthquake in the list.
  RemoteViews rv = new RemoteViews(context.getPackageName(), 
                                   R.layout.quake_widget);
 
  rv.setTextViewText(R.id.widget_magnitude, magnitude);
  rv.setTextViewText(R.id.widget_details, details);
      
  // Create the fill-in Intent that adds the URI for the current item
  // to the template Intent.
  Intent fillInIntent = new Intent();
  Uri uri = Uri.withAppendedPath(EarthquakeProvider.CONTENT_URI, id);
  fillInIntent.setData(uri);
 
  rv.setOnClickFillInIntent(R.id.widget_magnitude, fillInIntent);
  rv.setOnClickFillInIntent(R.id.widget_details, fillInIntent);
 
  return rv;
}

10. Add the Earthquake Remote Views Service to your application manifest, including a requirement for the BIND_REMOTEVIEWS permission:

<service android:name=".EarthquakeRemoteViewsService"
  android:permission="android.permission.BIND_REMOTEVIEWS">
</service>

11. Return to the Earthquake List Widget class and override the onUpdate method. Iterate over each of the active Widgets, attaching the Earthquake Remote Views Service you created in step 5. Take this opportunity to create and assign a template Pending Intent to each item that will start a new Activity to view the URI filled in by the fill-in Intent you created in step 9.2.

@Override
public void onUpdate(Context context,
                     AppWidgetManager appWidgetManager,
                     int[] appWidgetIds) {
  
  // Iterate over the array of active widgets.
  final int N = appWidgetIds.length;
  for (int i = 0; i < N; i++) {
    int appWidgetId = appWidgetIds[i];
    
    // Set up the intent that starts the Earthquake 
    // Remote Views Service, which will supply the views
    // shown in the List View.
    Intent intent = new Intent(context, EarthquakeRemoteViewsService.class);
    // Add the app widget ID to the intent extras.
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
 
    // Instantiate the RemoteViews object for the App Widget layout.
    RemoteViews views = new RemoteViews(context.getPackageName(),
      R.layout.quake_collection_widget);
    
    // Set up the RemoteViews object to use a RemoteViews adapter. 
    views.setRemoteAdapter(R.id.widget_list_view, intent);
    
    // The empty view is displayed when the collection has no items. 
    views.setEmptyView(R.id.widget_list_view, R.id.widget_empty_text);
  
    // Create a Pending Intent template to provide interactivity to 
    // each item displayed within the collection View.
    Intent templateIntent = new Intent(context, Earthquake.class);
    templateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
    PendingIntent templatePendingIntent = 
      PendingIntent.getActivity(context, 0, templateIntent,
                                PendingIntent.FLAG_UPDATE_CURRENT);
    
    views.setPendingIntentTemplate(R.id.widget_list_view, 
                                   templatePendingIntent);
    
    // Notify the App Widget Manager to update the widget using
    // the modified remote view.
    appWidgetManager.updateAppWidget(appWidgetId, views);
  }
}

12. As a final step, enhance the Widget to update whenever the Earthquake Update Service you created in Chapter 9 has refreshed the earthquake database. Do this by updating the onHandleIntent handler in the EarthquakeUpdateService to call the App Widget Manager'snotifyAppWidgetViewDataChanged method when it has completed:

@Override
protected void onHandleIntent(Intent intent) {
  refreshEarthquakes();
  sendBroadcast(new Intent(QUAKES_REFRESHED));
  
  Context context = getApplicationContext();
  AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
  ComponentName earthquakeWidget = 
    new ComponentName(context, EarthquakeListWidget.class);
  int[] appWidgetIds = appWidgetManager.getAppWidgetIds(earthquakeWidget);
 
  appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds,
    R.id.widget_list_view);
}

2.1

All code snippets in this example are part of the Chapter 14 Earthquake Part 2 project, available for download at www.wrox.com.

Figure 14.4 shows the Earthquake Collection View Widget added to the home screen.

Figure 14.4

14.4

Introducing Live Folders

In Android 3.0 Live Folders were deprecated in favor of the richer, more customizable Collection View Widgets described in the previous section.

Live Folders provided a similar functionality for earlier versions of Android—a means by which your application can expose data from its Content Providers directly on to the home screen.

Although Collection View Widgets are the supported alternative for devices running Android 3.0 or above, you can still use Live Folders to provide dynamic home screen shortcuts to information stored in your application for devices running earlier version of Android.

When added, a Live Folder is represented on the home screen as a shortcut icon. Selecting the icon will open the Live Folder, as shown in Figure 14.5. This figure shows a Live Folder open on an Android home screen—in this case, the starred contacts list.

Figure 14.5

14.5

2.1

To add a Live Folder to the home screen on Android devices prior to Android 3.0, long-press a piece of empty space and select Folders. You will be presented with a list of available Live Folders; click one to select and add it. After it is added, click to open the Live Folder, and long-press to move the shortcut. Live Folders aren't available on devices running Android 3.0 or higher.

Creating Live Folders

Live Folders are a combination of a Content Provider and an Activity. To create a new Live Folder, you need to define the following:

· An Activity responsible for creating and configuring the Live Folder by generating and returning a specially formatted Intent

· A Content Provider that provides the items to be displayed using the required column names

Unlike Collection View Widgets, each Live Folder item can display up to only three pieces of information: an icon, a title, and a description.

The Live Folder Content Provider

Any Content Provider can provide the data displayed within a Live Folder. Live Folders use a standard set of column names:

· LiveFolders._ID—A unique identifier used to indicate which item was selected if a user clicks the Live Folder list.

· LiveFolders.NAME—The primary text, displayed in a large font. This is the only required column.

· LiveFolders.DESCRIPTION—A longer descriptive field in a smaller font, displayed beneath the name column.

· LiveFolders.ICON_BITMAP—An image bitmap to be displayed at the left of each item. Alternatively, you can use a combination of LiveFolders.ICON_PACKAGE and LiveFolder.ICON_RESOURCE to specify a Drawable resource to use from a particular package.

Rather than renaming the columns within your Content Provider to suit the requirements of Live Folders, you should apply a projection that maps your existing column names to those required by a Live Folder, as shown in Listing 14.28.

2.11

Listing 14.28: Creating a projection to support a Live Folder

private static final HashMap<String, String> LIVE_FOLDER_PROJECTION;
static {
  // Create the projection map.
  LIVE_FOLDER_PROJECTION = new HashMap<String, String>();
 
  // Map existing column names to those required by a Live Folder.
  LIVE_FOLDER_PROJECTION.put(LiveFolders._ID,
                           KEY_ID + " AS " +
                           LiveFolders._ID);
  LIVE_FOLDER_PROJECTION.put(LiveFolders.NAME,
                           KEY_NAME + " AS " +
                           LiveFolders.NAME);
  LIVE_FOLDER_PROJECTION.put(LiveFolders.DESCRIPTION,
                           KEY_DESCRIPTION + " AS " +
                           LiveFolders.DESCRIPTION);
  LIVE_FOLDER_PROJECTION.put(LiveFolders.ICON_BITMAP,
                           KEY_IMAGE + " AS " +
                           LiveFolders.ICON_BITMAP);
}

code snippet PA4AD_Ch14_MyLiveFolder/src/MyContentProvider.java

Only the ID and name columns are required; the bitmap and description columns can be used or left unmapped, as required.

The projection typically will be applied within the query method of your Content Provider when the query request URI matches the pattern you specify for Live Folder request, as shown in Listing 14.29.

2.11

Listing 14.29: Applying a projection to support a Live Folder

public static Uri LIVE_FOLDER_URI 
  = Uri.parse("com.paad.provider.MyLiveFolder");
 
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {
 
  SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
  
  switch (URI_MATCHER.match(uri)) {
    case LIVE_FOLDER:
      qb.setTables(MYTABLE);
      qb.setProjectionMap(LIVE_FOLDER_PROJECTION);
      break;
    default:
      throw new IllegalArgumentException("Unknown URI " + uri);
  }
 
  Cursor c = qb.query(null, projection, selection, selectionArgs, 
                      null, null, null);
 
  c.setNotificationUri(getContext().getContentResolver(), uri);
 
  return c;
}

code snippet PA4AD_Ch14_MyLiveFolder/src/MyContentProvider.java

The Live Folder Activity

The Live Folder is defined using an Intent returned as the result of an Activity (typically from within the onCreate handler).

Use the Intent's setData method to specify the URI of the Content Provider supplying the data (with the appropriate projection applied), as described in the previous section.

You can configure the Intent further by using a series of extras as follows:

· LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE—Specifies the display mode to use. This can be either LiveFolders.DISPLAY_MODE_LIST or LiveFolders.DISPLAY_MODE_GRID to display your Live Folder as a list or grid, respectively.

· LiveFolders.EXTRA_LIVE_FOLDER_ICON—Provides a Drawable resource that will be used as the home screen icon that represents the Live Folder when it hasn't been opened.

· LiveFolders.EXTRA_LIVE_FOLDER_NAME—Provides a descriptive name to use with the icon described above to represent the Live Folder on the home screen when it hasn't been opened.

Listing 14.30 shows the overridden onCreate method of an Activity used to create a Live Folder. After the Live Folder definition Intent is constructed, it is set as the Activity result using setResult, and the Activity is closed with a call to finish.

2.11

Listing 14.30: Creating a Live Folder from within an Activity

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
 
  // Check to confirm this Activity was launched as part
  // of a request to add a new Live Folder to the home screen
  String action = getIntent().getAction();
  if (LiveFolders.ACTION_CREATE_LIVE_FOLDER.equals(action)) {
    Intent intent = new Intent(); 
 
    // Set the URI of the Content Provider that will supply the
    // data to display. The appropriate projection must already
    // be applied to the returned data.
    intent.setData(MyContentProvider.LIVE_FOLDER_URI);
 
    // Set the display mode to a list.
    intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE,
                    LiveFolders.DISPLAY_MODE_LIST);
 
    // Indicate the icon to be used to represent the Live Folder 
    // shortcut on the home screen.
    intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_ICON,
                    Intent.ShortcutIconResource.fromContext(this,
                      R.drawable.icon));
 
    // Provide the name to be used to represent the Live Folder on 
    // the home screen. 
    intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME, "Earthquakes");
 
    // Return the Live Folder Intent as a result.
    setResult(RESULT_OK, intent);
  }
  else
    setResult(RESULT_CANCELED);
  finish();
}

code snippet PA4AD_Ch14_MyLiveFolder/src/MyLiveFolder.java

You can also provide support for selecting items in the Live Folder.

By adding a LiveFolders.EXTRA_LIVE_FOLDER_BASE_INTENT extra to the returned Intent, you can specify a base Intent to fire when a Live Folder item is selected. When this value is set, selecting a Live Folder item will result in startActivity being called with the specified base Intent used as the Intent parameter.

Best practice (as shown in Listing 14.31) is to set the data parameter of this Intent to the base URI of the Content Provider that's supplying the Live Folder's data. In such cases the Live Folder will automatically append the value stored in the selected item's _id column to the Intent's data value.

2.11

Listing 14.31: Adding a base Intent for Live Folder item selection

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
 
  // Check to confirm this Activity was launched as part
  // of a request to add a new Live Folder to the home screen
  String action = getIntent().getAction();
  if (LiveFolders.ACTION_CREATE_LIVE_FOLDER.equals(action)) {
    Intent intent = new Intent(); 
 
    // Set the URI of the Content Provider that will supply the
    // data to display. The appropriate projection must already
    // be applied to the returned data.
    intent.setData(LiveFolderProvider.CONTENT_URI);
 
    // Set the display mode to a list.
    intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE,
                    LiveFolders.DISPLAY_MODE_LIST);
 
    // Indicate the icon to be used to represent the Live Folder 
    // shortcut on the home screen.
    intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_ICON,
                    Intent.ShortcutIconResource.fromContext(context,
                      R.drawable.icon));
 
    // Provide the name to be used to represent the Live Folder on 
    // the home screen. 
    intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME, 
                    "My Live Folder");
 
    // Specify a base Intent that will request the responding Activity
    // View the selected item.
    intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_BASE_INTENT,
                    new Intent(Intent.ACTION_VIEW,
                               MyContentProvider.CONTENT_URI));
 
    // Return the Live Folder Intent as a result.
    setResult(RESULT_OK, intent);
  }
  else
    setResult(RESULT_CANCELED);
  finish();
}

code snippet PA4AD_Ch14_MyLiveFolder/src/MyLiveFolder.java

In order for the system to identify an Activity as a Live Folder, you must include an Intent Filter for the CREATE_LIVE_FOLDER action when adding the Live Folder Activity to your application manifest, as shown in Listing 14.32.

2.11

Listing 14.32: Adding the Live Folder Activity to the manifest

<activity android:name=".MyLiveFolder"
          android:label="My Live Folder">
  <intent-filter>
    <action android:name="android.intent.action.CREATE_LIVE_FOLDER"/>
  </intent-filter>
</activity>

code snippet PA4AD_Ch14_MyLiveFolder/AndroidManifest.xml

Creating an Earthquake Live Folder

In the following example you extend the Earthquake application again—this time to include a Live Folder that displays the magnitude and location of each quake. The resulting Live Folder is very similar to the Collection View Widget you created earlier in this chapter, making it a useful alternative for devices running Android releases prior to Android 3.0.

1. Start by modifying the EarthquakeProvider class. Create a new static URI definition that will be used to return the Live Folder items:

public static final Uri LIVE_FOLDER_URI = 
  Uri.parse("content://com.paad.provider.earthquake/live_folder");

2. Modify the uriMatcher object, and getType method to check for this new URI request:

private static final int QUAKES = 1;
private static final int QUAKE_ID = 2;
private static final int SEARCH = 3;
private static final int LIVE_FOLDER = 4;
 
private static final UriMatcher uriMatcher;
 
//Allocate the UriMatcher object, where a URI ending in ‘earthquakes' will
//correspond to a request for all earthquakes, and ‘earthquakes' with a
//trailing ‘/[rowID]’ will represent a single earthquake row.
static {
  uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
  uriMatcher.addURI("com.paad.earthquakeprovider", "earthquakes", QUAKES);
  uriMatcher.addURI("com.paad.earthquakeprovider", "earthquakes/#", QUAKE_ID);
  uriMatcher.addURI("com.paad.provider.Earthquake", "live_folder", LIVE_FOLDER);
  uriMatcher.addURI("com.paad.earthquakeprovider",
    SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH);
  uriMatcher.addURI("com.paad.earthquakeprovider",
    SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH);
  uriMatcher.addURI("com.paad.earthquakeprovider",
    SearchManager.SUGGEST_URI_PATH_SHORTCUT, SEARCH);
  uriMatcher.addURI("com.paad.earthquakeprovider",
    SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", SEARCH);
}
 
@Override
public String getType(Uri uri) {
  switch (uriMatcher.match(uri)) {
    case QUAKES|LIVE_FOLDER: return "vnd.android.cursor.dir/vnd.paad.earthquake";
    case QUAKE_ID: return "vnd.android.cursor.item/vnd.paad.earthquake";
    case SEARCH  : return SearchManager.SUGGEST_MIME_TYPE;
    default: throw new IllegalArgumentException("Unsupported URI: " + uri);
  }
}

3. Create a new hash map that defines a projection suitable for a Live Folder. It should return the magnitude and location details as the description and name columns, respectively:

static final HashMap<String, String> LIVE_FOLDER_PROJECTION;
static {
  LIVE_FOLDER_PROJECTION = new HashMap<String, String>();
  LIVE_FOLDER_PROJECTION.put(LiveFolders._ID,
                             KEY_ID + " AS " + LiveFolders._ID);
  LIVE_FOLDER_PROJECTION.put(LiveFolders.NAME,
                             KEY_DETAILS + " AS " + LiveFolders.NAME);
  LIVE_FOLDER_PROJECTION.put(LiveFolders.DESCRIPTION,
                             KEY_DATE + " AS " + LiveFolders.DESCRIPTION);
}

4. Update the query method to apply the projection map from step 3 to the returned earthquake query for Live Folder requests:

@Override
public Cursor query(Uri uri,
                    String[] projection,
                    String selection,
                    String[] selectionArgs,
                    String sort) {
 
  SQLiteDatabase database = dbHelper.getWritableDatabase();
 
  SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
 
  qb.setTables(EarthquakeDatabaseHelper.EARTHQUAKE_TABLE);
 
  // If this is a row query, limit the result set to the passed in row.
  switch (uriMatcher.match(uri)) {
    case QUAKE_ID: qb.appendWhere(KEY_ID + "=" + uri.getPathSegments().get(1));
                   break;
    case SEARCH  : qb.appendWhere(KEY_SUMMARY + " LIKE \"%" +
                     uri.getPathSegments().get(1) + "%\"");
                     qb.setProjectionMap(SEARCH_PROJECTION_MAP);
                   break;
    case LIVE_FOLDER : qb.setProjectionMap(LIVE_FOLDER_PROJECTION);
                       break;
    default      : break;
  }
 
  [ ... existing query method ... ]
}

5. Create a new EarthquakeLiveFolders class that contains a static EarthquakeLiveFolder Activity:

package com.paad.earthquake;
 
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.provider.LiveFolders;
 
public class EarthquakeLiveFolders extends Activity {
  public static class EarthquakeLiveFolder extends Activity { 
  }
}

6. Add a new method that builds the Intent used to create the Live Folder. It should use the query URI you created in step 1, set the display mode to list, and define the icon and title string to use. Also set the base Intent to the individual item query from the Earthquake Provider:

private static Intent createLiveFolderIntent(Context context) {
  Intent intent = new Intent();
  intent.setData(EarthquakeProvider.LIVE_FOLDER_URI);
  intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_BASE_INTENT,
                  new Intent(Intent.ACTION_VIEW,
                             EarthquakeProvider.CONTENT_URI));
  intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE,
                  LiveFolders.DISPLAY_MODE_LIST);
  intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_ICON,
                  Intent.ShortcutIconResource.fromContext(context,
                                                          R.drawable.ic_launcher));
  intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME, "Earthquakes");
  return intent;
}

7. Override the onCreate method of the EarthquakeLiveFolder class to return the Intent defined in step 6:

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
 
  String action = getIntent().getAction();
  if (LiveFolders.ACTION_CREATE_LIVE_FOLDER.equals(action))
    setResult(RESULT_OK, createLiveFolderIntent(this));
  else
    setResult(RESULT_CANCELED);
  finish();
}

8. Add the EarthquakeLiveFolder Activity to the application manifest, including an Intent Filter for the action android.intent.action.CREATE_LIVE_FOLDER:

<activity android:name=".EarthquakeLiveFolders$EarthquakeLiveFolder"
          android:label="All Earthquakes">
  <intent-filter>
    <action android:name="android.intent.action.CREATE_LIVE_FOLDER"/>
  </intent-filter>
</activity>

Figure 14.6 shows the earthquake Live Folder open on the home screen.

Figure 14.6

14.6

2.1

All code snippets in this example are part of the Chapter 14 Earthquake Part 3 project, available for download at www.wrox.com.

Surfacing Application Search Results Using the Quick Search Box

The QSB (shown in Figure 14.7) is positioned prominently on the home screen. The user can launch it at any time by clicking it or pressing the hardware search key, where one is available.

Figure 14.7

14.7

Android 1.6 (API level 4) introduced the ability to serve your application search results through the universal QSB. By surfacing search results from your application through this mechanism, you provide users with an additional access point to your application through live search results.

Surfacing Search Results to the Quick Search Box

To serve your search results to the QSB, you must first implement search functionality within your application, as described in Chapter 8, “Databases and Content Providers.”

To make your results available globally, modify the searchable.xml file that describes the application search meta data and add two new attributes:

· searchSettingsDescription—Used to describe your search results in the Settings menu. This is what the users will see when browsing to include application results in their searches.

· includeInGlobalSearch—Set this to true to surface these results to the QSB.

<searchable xmlns:android="http://schemas.android.com/apk/res/android"
  android:label="@string/search_label"
 
  android:searchSuggestAuthority="com.paad.provider.mysearch"
  android:searchSuggestIntentAction="android.intent.action.VIEW"
  android:searchSettingsDescription="@string/search_description"
  android:includeInGlobalSearch="true">
</searchable>

To avoid the possibility of misuse, adding new search providers requires users to opt-in, so your search results will not be automatically surfaced directly to the QSB.

For users to add your application's search results to their QSB search, they must opt-in using the system settings, as shown in Figure 14.8. From the QSB Activity, they must select Menu Í <MenuArrow>Settings Í <MenuArrow>Searchable Items and tick the check boxes alongside each Provider they want to enable.

2.1

Because search result surfacing in the QSB is strictly opt-in, you should consider notifying your users that this additional functionality is available.

Figure 14.8

14.8

Adding the Earthquake Example Search Results to the Quick Search Box

To surface search results from the Earthquake project to the QSB, edit the searchable.xml file in your res/xml resources folder. Add a new attribute, setting includeInGlobalSearch to true:

<searchable xmlns:android="http://schemas.android.com/apk/res/android"
  android:label="@string/app_name"
  android:searchSettingsDescription="@string/search_description"
  android:searchSuggestAuthority="com.paad.earthquakeprovider"
  android:searchSuggestIntentAction="android.intent.action.VIEW"
  android:searchSuggestIntentData=
    "content://com.paad.earthquakeprovider/earthquakes"
  android:includeInGlobalSearch="true">
</searchable>

2.1

All code snippets in this example are part of the Chapter 14 Earthquake Part 4 project, available for download at www.wrox.com.

Creating Live Wallpaper

Live Wallpaper was introduced in Android 2.1 (API level 7) as a way to create dynamic, interactive home screen backgrounds. They offer an exciting alternative for displaying information to your users directly on their home screen.

Live Wallpapers use a Surface View to render a dynamic display that can be interacted with in real time. Your Live Wallpaper can listen for, and reach to, screen touch events—letting users engage directly with the background of their home screen.

To create a new Live Wallpaper, you need the following three components:

· An XML resource that describes the metadata associated with the Live Wallpaper—specifically its author, description, and a thumbnail used to represent it from the Live Wallpaper picker.

· A Wallpaper Service implementation that will wrap, instantiate, and manage your Wallpaper Engine.

· A Wallpaper Service Engine implementation (returned through the Wallpaper Service) that defines the UI and interactive behavior of your Live Wallpaper. The Wallpaper Service Engine represents where the bulk of your Live Wallpaper implementation will live.

Creating a Live Wallpaper Definition Resource

The Live Wallpaper resource definition is an XML file stored in the res/xml folder. Its resource identifier is its filename without the XML extension. Use attributes within a wallpaper tag to define the author name, description, and thumbnail to display in the Live Wallpaper gallery.

Listing 14.33 shows a sample Live Wallpaper resource definition.

2.11

Listing 14.33: Sample Live Wallpaper resource definition

<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
  android:author="@string/author"
  android:description="@string/description"
  android:thumbnail="@drawable/wallpapericon"
/>

code snippet PA4AD_Ch14_LiveWallpaper/res/xml/mylivewallpaper.xml

Note that you must use references to existing string resources for the author and description attribute values. String literals are not valid.

You can also use the settingsActivity tag to specify an Activity that should be launched to configure the Live Wallpaper's settings, much like the configuration Activity used to configure Widget settings:

<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
  android:author="@string/author"
  android:description="@string/description"
  android:thumbnail="@drawable/wallpapericon"
  android:settingsActivity="com.paad.mylivewallpaper.WallpaperSettings"
/>

This Activity will be launched immediately before the Live Wallpaper is added to the home screen, allowing the user to configure the Wallpaper.

Creating a Wallpaper Service

Extend the WallpaperService class to create a wrapper Service that instantiates and manages the Wallpaper Service Engine class.

All the drawing and interaction for Live Wallpaper is handled in the Wallpaper Service Engine class described in the next section. Override the onCreateEngine handler to return a new instance of your custom Wallpaper Service Engine, as shown in Listing 14.34.

2.11

Listing 14.34: Creating a Wallpaper Service

import android.service.wallpaper.WallpaperService;
import android.service.wallpaper.WallpaperService.Engine;
 
public class MyWallpaperService extends WallpaperService {
  @Override
  public Engine onCreateEngine() {
    return new MyWallpaperServiceEngine();
  }
}

code snippet PA4AD_Ch14_LiveWallpaper/src/MyWallpaperService.java

After creating the Wallpaper Service, add it to your application manifest using a service tag.

A Wallpaper Service must include an Intent Filter to listen for the android.service.wallpaper.WallpaperService action and a meta-data node that specifies android.service.wallpaper as the name attribute and associates it with the resource file described in the previous section using a resourceattribute.

An application that includes a Wallpaper Service must also require the android.permission.BIND_WALLPAPER permission. Listing 14.35 shows how to add the Wallpaper Service from Listing 14.34 to the manifest.

Listing 14.35: Adding a Wallpaper Service to the manifest

<application 
  android:icon="@drawable/icon" 
  android:label="@string/app_name"
  android.permission="android.permission.BIND_WALLPAPER">
 
  <service android:name=".MyWallpaperService">
    <intent-filter>
      <action android:name=
        "android.service.wallpaper.WallpaperService" />
    </intent-filter>
    <meta-data
      android:name="android.service.wallpaper"
      android:resource="@xml/mylivewallpaper"
    />
  </service>
</application>

code snippet PA4AD_Ch14_LiveWallpaper/AndroidManifest.xml

Creating a Wallpaper Service Engine

The WallpaperService.Engine class is where you define the behavior of the Live Wallpaper. The Wallpaper Service Engine includes the Surface View onto which you will draw your Live Wallpaper and handlers notifying you of touch events and home screen offset changes and is where you should implement your redraw loop.

The Surface View, introduced in Chapter 11, is a specialized drawing canvas that supports updates from background threads, making it ideal for creating smooth, dynamic, and interactive graphics.

To implement your own Wallpaper Service engine, extend the WallpaperService.Engine class within an enclosing Wallpaper Service class, as shown in the skeleton code in Listing 14.36.

2.11

Listing 14.36: Wallpaper Service Engine skeleton code

public class MyWallpaperServiceEngine extends WallpaperService.Engine {
 
  private static final int FPS = 30;
  private final Handler handler = new Handler();
 
  @Override
  public void onCreate(SurfaceHolder surfaceHolder) {
    super.onCreate(surfaceHolder);
    // TODO Handle initialization.
  }
 
  @Override
  public void onOffsetsChanged(float xOffset, float yOffset,
                               float xOffsetStep, float yOffsetStep,
                               int xPixelOffset, int yPixelOffset) {
    super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep,
                           xPixelOffset, yPixelOffset);
    // Triggered whenever the user swipes between multiple 
    // home-screen panels. 
  }
 
  @Override
  public void onTouchEvent(MotionEvent event) {
    super.onTouchEvent(event);
    // Triggered when the Live Wallpaper receives a touch event
  }
 
  @Override
  public void onSurfaceCreated(SurfaceHolder holder) {
    super.onSurfaceCreated(holder);
    // TODO Surface has been created, begin the update loop that will
    // update the Live Wallpaper.
    drawFrame();
  }
 
  private void drawFrame() {
    final SurfaceHolder holder = getSurfaceHolder();
 
    Canvas canvas = null;
    try {
      canvas = holder.lockCanvas();
      if (canvas != null) {
        // Draw on the Canvas!
      }
    } finally {
      if (canvas != null) 
        holder.unlockCanvasAndPost(canvas);
    }
 
    // Schedule the next frame
    handler.removeCallbacks(drawSurface);
    handler.postDelayed(drawSurface, 1000 / FPS);
  }
 
  // Runnable used to allow you to schedule frame draws.
  private final Runnable drawSurface = new Runnable() {
    public void run() {
      drawFrame();
    }
  };
 
}

code snippet PA4AD_Ch14_LiveWallpaper/src/ MyWallpaperSkeletonService.java

You must wait for the Surface to complete its initialization—indicated by the onSurfaceCreated handler being called—before you can begin drawing on it.

After the Surface has been created, you can begin the drawing loop that updates the Live Wallpaper's UI. In Listing 14.36 this is done by scheduling a new frame to be drawn at the completion of the drawing of the previous frame. The rate of redraws in this example is determined by the desired frame rate.

You can use the onTouchEvent and the onOffsetsChanged handlers to add interactivity to your Live Wallpapers.