Chapter 10. Invading the Phone-Top

WHAT'S IN THIS CHAPTER?

  • Creating home screen Widgets

  • Implementing Live Folders

  • Adding search to your applications

  • Surfacing search results to the Quick Search Box

  • Creating Live Wallpaper

Widgets, Live Folders, Live Wallpaper, and the Quick Search Box let you own 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 an exciting innovation for users and developers, providing the following:

  • 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 widget, Live Folder, or dynamic wallpaper decreases the chance that an application will be uninstalled, and increases the likelihood of its being used.

With such power comes responsibility. Widgets run constantly as subprocesses of the home-screen process. You need to be particularly careful when creating widgets to ensure they remain responsive and don't drain system resources.

This chapter demonstrates how to create and use App Widgets, Live Folders, and Live Wallpaper detailing what they are, how to use them, and some techniques for incorporating interactivity into these application components.

It also describes how to integrate the Android search framework into your application and surface search results to the Quick Search Box.

INTRODUCING HOME-SCREEN WIDGETS

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, though any application you create can become an AppHost and support third-party widgets if you desire.

Widgets enable your application to own a piece of interactive screen real estate, and 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 widgets.

Figure 10-1 shows four of the standard home-screen widgets available on Android devices: the search box, power control, news and weather, and media player.

FIGURE 10-1

Figure 10-1. FIGURE 10-1

Note

To add a widget to the home screen, long-press a piece of empty space and select Widgets. You will be presented with a list of available widgets. Once you've added one you can move it by long-pressing it and dragging it around the screen. Remove widgets by dragging them into the garbage can icon at the bottom of the screen.

Widgets embedded into the home screen are hosted within the home screen's process. They will wake the device based on their update rates to ensure each widget is up to date when it is 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 interaction.

CREATING APP WIDGETS

App Widgets are implemented as IntentReceivers. They use RemoteViews to update a view hierarchy hosted within another application process; in most cases that host process is the home screen.

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

  1. A layout resource that defines the UI for the widget

  2. An XML definition file that describes the metadata associated with the widget

  3. An Intent Receiver that defines and controls the widget

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

Creating the Widget Layout

The first step in creating your widget is to design and implement its user interface.

Construct your widget's UI as you would other visual components in Android, as described in Chapter 4. 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 Intent Receiver's onCreate method.

Widget Design Guidelines

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

There are widget UI design guidelines for controlling both layout size and visual styling. The former is rigidly enforced while the latter is a guide only; both are summarized in the following sections. Additional detail can also be found 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 four-by-four grid of cells, each a minimum of 74 Ă— 74 device-independent pixels (dp). To select the height and width of your widget, start by calculating the number of cells you wish to use. The total pixels required will be 74 times the cell count minus two pixels for padding, as shown in the following formula:

Minimum size in dp = (Cell count * 74dp) - 2dp

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 all the cells.

Widget dimensions are specified in the widget settings file, as described later in this chapter.

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.

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 given earlier. Google's description includes the image resources used to create the native Android widgets shipped with Google Experience devices.

App Widgets fully support transparent backgrounds and allow the use of NinePatches and partially transparent PNG-drawable resources.

Supported Widget Views and Layouts

Because of security and performance considerations there are several restrictions on the layouts and Views available to you when you're constructing your widget UI.

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

  • All custom Views

  • Descendents of the allowed Views

  • EditText

Currently, the layouts available are limited to:

  • FrameLayout

  • LinearLayout

  • RelativeLayout

The Views they contain are restricted to:

  • AnalogClock

  • Button

  • Chronometer

  • ImageButton

  • ImageView

  • ProgressBar

  • TextView

The Text Views and Image Views are particularly useful. Later in this chapter you'll see how to use the Image View in conjunction with the SelectionStateDrawable resource to create interactive widgets with little or no code.

Listing 10-1 shows a sample layout resource used to define the UI of an App Widget.

Example 10-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="fill_parent"
  android:layout_height="fill_parent"
  android:padding="10sp">
<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="Text Goes Here"
  />
</LinearLayout>

Defining Your Widget Settings

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

  • initialLayout The layout resource to use in constructing the widget's user interface.

  • minWidth / minHeight Respectively, the minimum width and minimum height of the widget, as described in the previous section.

  • label The title used by your widget in the 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. 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 later in this chapter.

Listing 10-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.

Example 10-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="146dp"
  android:minHeight="146dp"
  android:label="My App Widget"
  android:updatePeriodMillis="3600000"
/>

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

Widgets are implemented as Intent Receivers with Intent Filters that catch broadcast Intents, which request widget updates using the AppWidget.ACTION_APPWIDGET_UPDATE, DELETED, ENABLED, and DISABLED actions. You can create your widget by extending the IntentReceiver class directly and interpreting those broadcast Intents by overriding the onReceive method.

The AppWidgetProvider class provides a simplified alternative by encapsulating the Intent processing and presenting you with event handlers for the update, delete, enable, and disable events.

Listing 10-3 shows a simple widget implementation that extends AppWidgetProvider and overrides the onUpdate method:

Example 10-3. App Widget implementation

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;

public class MyAppWidget extends AppWidgetProvider {
  @Override
  public void onUpdate(Context context,
                       AppWidgetManager appWidgetManager,
                       int[] appWidgetIds) {
    // TODO Update the Widget UI.
  }
}

Widgets are added to the application manifest much like other Intent Receivers. However, to specify an Intent Receiver as an App Widget you need to add two additional tags to its manifest node (Listing 10-4).

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

  • A reference to the metadata XML resource that describes your widget

Example 10-4. App Widget manifest node

<receiver android:name=".MyAppWidget" android:label="My App Widget">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <meta-data
    android:name="android.appwidget.provider"
    android:resource="@xml/my_app_widget_info"
  />
</receiver>

Introducing Remote Views and the App Widget Manager

The RemoteViews class is used to describe and manipulate a View hierarchy that's hosted within another application process. This lets you change a property, or run a method, on a View running as part of another application.

For example, the Views within App Widgets are hosted within a separate process (generally the home screen), so Remote Views can be used to modify the widget UI from the Intent Receiver running within your application.

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

Using Remote Views with the App Widget Manager, you can modify the appearance of the Views supported by the App Widget framework. Among other things, you can change the visibility, text, or image values, and add click listeners.

This section describes how to create new Remote Views from within and without the onUpdate method of an App Widget Provider. It also demonstrates how to use Remote Views to update widget UI and add interactivity to your widgets.

Creating Remote Views and Using the App Widget Manager to Apply Them

To create a new Remote Views object you must pass the name of the calling application package, and the layout resource you plan to manipulate, into the constructor, as shown in Listing 10-5. Later in this section you'll learn how to use this Remote Views object to update the Views and layout of your widget.

Example 10-5. Using Remote Views

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

To use Remote Views on widgets, call the static getInstance method to return an instance of the App Widget Manager and use it to find identifiers for each instance of a particular widget class, as in this continuation of Listing 10-5:

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

When you've finished making changes to a Remote Views object, apply those modifications to one or more widgets by calling the updateAppWidget method on the App Widget Manager, passing in either an individual widget ID or an array of identifiers:

appWidgetManager.updateAppWidget(appWidgetIds, views);

The standard pattern used to update widget UI is to iterate over the widget ID array as shown in Listing 10-6. This enables you to apply different UI values to each widget based on its configuration settings or UI requirements.

Example 10-6. 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 Remove 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);
}

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

The App Widget Provider simplifies your widget interactions by passing the App Widget Manager, and an array of matching App Widget IDs, as parameters into the onUpdate handler.

You can then follow the same pattern as shown above, without the need to obtain a reference to the App Widget Manager or find the identifier values for the affected widgets first as shown in Listing 10-7.

Example 10-7. Using a Remote View within the App Widget Provider's onUpdate Handler

@Override
public void onUpdate(Context context,
                     AppWidgetManager appWidgetManager,
                     int[] appWidgetIds) {
  final int N = appWidgetIds.length;
  for (int i = 0; i < N; i++) {
    int appWidgetId = appWidgetIds[i];
    // Create a Remove 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);
  }
}

Using Remote Views to Modify UI

Remote Views expose a variety of methods designed to provide access to the properties and methods available on Views in order for you to change their appearance.

The most versatile of these is a series of methods that lets you execute a target method name on a remotely hosted View. These methods support the passing of single-value parameters. Supported method signatures include a parameter for each primitive type, including Boolean, integer, and float, plus strings, bitmaps, and URI parameters.

Listing 10-8 shows examples of some of the method signatures supported.

Example 10-8. Using a Remote View to modify App Widget UI

// 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);

Remote Views also include a set of View-specific methods to set values applicable to a particular View class, including Text Views, Image Views, Progress Bars, and Chronometers.

Listing 10-9 shows examples of some of these specialist methods:

Example 10-9. Modifying View properties within an App Widget Remote View

// Update a Text View
views.setTextViewText(R.id.widget_text_view, "Updated Text");
views.setTextColor(R.id.widget_text_view, Color.BLUE);
// Update an Image View
views.setImageViewBitmap(R.id.widget_image_view, myBitmap);
// Update a Progress Bar
views.setProgressBar(R.id.widget_progressbar, 100, 50, false);
// Update a Chronometer
views.setChronometer(.id.widget_chronometer,
SystemClock.elapsedRealtime(), null, true);

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

views.setViewVisibility(R.id.widget_text_view, View.VISIBLE);

As described in the previous section, once you've made changes to a Remote Views object you must use the App Widget Manager to apply those changes to a particular widget, as shown here:

appWidgetManager.updateAppWidget(appWidgetId, views);

Making Your Widgets Interactive

You can also add interactivity to your widgets using Remote Views, but reactions to user input are tightly restricted.

Because they run within the home-screen process, the widgets themselves inherit its permissions. As a result of these security implications widget interactivity is carefully controlled.

Widget interaction is generally limited to two possibilities:

  • Adding a click listener to one or more views within the layout

  • Changing the UI based on selection changes

It's notable that 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 that displays an Activity to accept the user data when a portion of the widget is clicked.

Note

One popular alternative is to use Image Views designed to look like Edit Text controls. By means of Selection State Drawables they can appear to gain focus. When the Image View is clicked, a partially transparent Activity is launched to accept the user input.

Using a Click Listener

The most powerful technique for adding interactivity to your widget is through the use of the setOnClickPendingIntent method on a Remote Views object.

This lets you specify a Pending Intent that will be fired when the user clicks on the specified widget View. Pending Intents (described in more detail in Chapter 5) can contain Intents used to start Activities or Services or broadcast Intents.

Listing 10-10 demonstrates a broadcast Intent assigned to a Text View element within a widget layout:

Example 10-10. Adding a Click Listener to an App Widget

Intent intent = new Intent("com.paad.ACTION_WIDGET_CLICK");
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
views.setOnClickPendingIntent(R.id.my_text_view, pendingIntent);

Using this technique you can add click handlers to one or more of the Views used within your widget, which means you can add 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.

Changing Image Views Based on Selection Focus

Image Views are one of the most flexible types of View available for your widget UI, providing support for some basic user interactivity within your widgets.

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 user selection as he or she navigates though the widget's controls.

The XML snippet in Listing 10-11 shows a sample Selection State Drawable resource.

Example 10-11. A Selection State Drawable resource for an App Widget

<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 Drawable resources referenced should be stored, along with the selection state xml file, in the application's 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 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 in the XML definition file, as shown in Listing 10-12, where the widget is updated once every hour:

Example 10-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="146dp"
  android:minHeight="146dp"
  android:label="My App Widget"
  android:updatePeriodMillis="3600000"
/>

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

Note

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.

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

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

Listening for Intents

As widgets are implemented as Intent Receivers you can trigger updates and UI refreshes by registering Intent Filters for additional actions.

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 10-13 assigns a new Intent Filter to the manifest entry of the widget defined earlier:

Example 10-13. Listening for Intent broacasts within App Widgets

<receiver android:name=".MyAppWidget" android:label="My App Widget">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <intent-filter>
    <action android:name="com.paad.chapter9.FORCE_WIDGET_UPDATE" />
  </intent-filter>
  <meta-data
    android:name="android.appwidget.provider"
    android:resource="@xml/my_app_widget_info"
  />
</receiver>

By updating the widget's onReceive method handler as shown in Listing 10-14, you can listen for this new Intent and use it to update your widget.

Example 10-14. Updating App Widgets based on broadcast Intents

public static String FORCE_WIDGET_UPDATE =
"com.paad.chapter9.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 UI.
  }
}

To trigger an update of your widget at any point in your application, you can broadcast an Intent using this action:

context.sendBroadcast(new Intent(FORCE_WIDGET_UPDATE));

This technique 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.

Using Alarms

Alarms provide a middle-ground alternative to the polling and Intent-based techniques described so far.

Alarms, covered in detail in Chapter 9, provide a flexible way to schedule regular events within your application. Using alarms you can poll at regular intervals, using an Intent to trigger your updates.

Using Alarms to refresh your widgets is similar to using the Intent-driven model described earlier. 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.

Alarms have an advantage over the minimum refresh rate, thanks to their flexibility.

Like the widgets' refresh rate, Alarms also have the ability to wake the device when they fire — making it equally important to take care to minimize battery use.

Alternatively, by using the RTC or ELAPSED_REALTIME modes when constructing your alarm, you can configure it to trigger after a minimum interval has elapsed, but only after the device has awakened:

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

Using this technique will ensure your widget is up to date when visible, without draining the battery unnecessarily to update the widget when the screen is off.

If your widget does need to be updated even when the device is on standby, you can optimize this process with the inexact repeating option, shown here:

String alarmService = Context.ALARM_SERVICE;
AlarmManager alarmManager = (AlarmManager)getSystemService(alarmService);

Intent intent = new Intent(MyAppWidget.FORCE_WIDGET_UPDATE);
PendingIntent pi = PendingIntent.getBroadcast(this,
                                              0,
                                              intent,
                                              0);

alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                                 AlarmManager.INTERVAL_HALF_DAY,
                                 AlarmManager.INTERVAL_HALF_DAY,
                                 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 only awakened once, rather than several times within a few minutes.

Creating and Using a Widget Configuration Activity

In some cases an App Widget will be significantly more useful if the user is given the opportunity to customize the data it displays and how the data is displayed. This is particularly important given that multiple instances of the same widget can be added to the home screen.

An App Widget configuration Activity is an Activity that 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 here:

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

It must also return a result Intent that includes an extra that describes the App Widget ID of the widget it is configuring using the EXTRA_APPWIDGET_ID constant. This extra is included in the Intent that launches the Activity.

Intent result = new Intent();
result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
setResult(RESULT_OK, result);
finish();

To assign a completed configuration Activity to a widget you must add it to the widget 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="3600000"
  android:configure="com.paad.chapter9.MyWidgetConfigurationActivity"
/>

CREATING AN EARTHQUAKE WIDGET

The following instructions show you how to create a new home-screen widget to display details for the latest earthquake detected. The UI for this widget is simple to the point of being inane; this is a side effect of keeping the example as concise as possible. Note that it does not conform to the widget style guidelines.

Once completed and added to the home screen, your widget will appear as in Figure 10-2.

FIGURE 10-2

Figure 10-2. FIGURE 10-2

Using a combination of the update techniques described above, 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.

The following code extends the Earthquake application last seen in Chapter 8:

  1. Start by creating the layout for the widget UI as an XML resource. Use a Linear Layout to configure Text Views that display the quake magnitude and location:

    <LinearLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="horizontal"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      android:background="#F111"
      android:padding="5sp">
      <TextView
        android:id="@+id/widget_magnitude"
        android:layout_width="wrap_content"
    android:layout_height="fill_parent"
        android:textSize="24sp"
        android:padding="3dp"
      />
      <TextView
        android:id="@+id/widget_details"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:textSize="14sp"
        android:padding="3dp"
      />
    </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.appwidget.AppWidgetManager;
    import android.appwidget.AppWidgetProvider;
    import android.content.ComponentName;
    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 24 hours and set the widget dimensions to two cells wide and one cell high—146dp×74dp. 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="146dp"
      android:minHeight="74dp"
      android:label="Last Earthquake"
      android:updatePeriodMillis="86400000"
    />
  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="Last Earthquake">
      <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
      </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/earthquake_widget_info"
      />
    </receiver>

    Your widget is now configured and will be available to add to the home screen. You now need to update the EarthquakeWidget class from Step 2 to update the widget to display the details of the latest quake.

  5. Start by creating two new updateQuake methods within the Earthquake Widget class:

    • 5.1. The first should take an App Widget Manager and an array of widget IDs as well as the context. Later you'll extend this second stub to update the widget appearance using Remote Views.

      public void updateQuake(Context context,
                              AppWidgetManager appWidgetManager,
                              int[] appWidgetIds) {
      }
    • 5.2. The second method stub should take 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 6 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()) {
      magnitude =
      lastEarthquake.getString(EarthquakeProvider.MAGNITUDE_COLUMN);
              details =
      lastEarthquake.getString(EarthquakeProvider.DETAILS_COLUMN);
            }
          }
          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()) {
              magnitude =
      lastEarthquake.getString(EarthquakeProvider.MAGNITUDE_COLUMN);
              details =
      lastEarthquake.getString(EarthquakeProvider.DETAILS_COLUMN);
            }
          }
          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) {
      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 enhance the widget to update whenever the Earthquake Service you created in Chapter 8 has refreshed the earthquake database:

    • 7.1. Start by updating the doRefreshEarthquakes method in the EarthquakeService to broadcast an Intent when it has completed.

      public static String QUAKES_REFRESHED =
        "com.paad.earthquake.QUAKES_REFRESHED";
      public void doRefreshEarthquakes() {
        [ ... Existing doRefreshEarthquakes code ... ]
        sendBroadcast(new Intent(QUAKES_REFRESHED));
      }
    • 7.2. Override the onReceive method in the EarthquakeWidget class, but 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);
      }
    • 7.3. Add a check for the QUAKES_REFRESHED action you broadcast in Step 7.1, and call updateQuakes when it's received:

      @Override
      public void onReceive(Context context, Intent intent){
        super.onReceive(context, intent);
      
        if (intent.getAction().equals(EarthquakeService.QUAKES_REFRESHED))
          updateQuake(context);
      }
    • 7.4. Finally, add an Intent Filter for this Intent action to the widget's manifest entry:

      <receiver android:name="EarthquakeWidget" android:label="Last 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/earthquake_widget_info"
        />
      </receiver>

Your widget will now update once per day, and every time the Earthquake Service performs a lookup.

To enhance the Earthquake Widget, consider how you could use Layered Drawables within an Image View to indicate the magnitude of the earthquake being shown. Figure 10-3 shows one possibility.

FIGURE 10-3

Figure 10-3. FIGURE 10-3

INTRODUCING LIVE FOLDERS

Live Folders are a unique and powerful means by which your applications can expose data from their Content Providers directly on the home screen. They provide dynamic shortcuts to information stored in your application.

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 10-4. This figure shows a Live Folder open on an Android home screen, in this case the starred contacts list.

Note

To add a Live Folder to the home screen, 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. Once it is added, click to open the Live Folder, and long-press to move the shortcut.

Creating Live Folders

Live Folders are a combination of two things: a Content Provider that returns the data required to populate a Live Folder in a standard format, and an Activity that returns an Intent used to generate the Live Folder.

To create a new Live Folder you need to define:

  • 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 correct column names

Each Live Folder item can display up to three pieces of information: an icon, a title, and a description.

Live Folder Content Providers

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.IMAGE An image displayed at the left of each item.

FIGURE 10-4

Figure 10-4. FIGURE 10-4

When displayed, a Live Folder will use these column names to extract data from your Content Provider for display.

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

Example 10-15. Creating a projection to support a Live Folder

final HashMap<String, String> liveFolderProjection =
  new HashMap<String, String>();
liveFolderProjection.put(LiveFolders._ID,
                         KEY_ID + " AS " +
                         LiveFolders._ID);
liveFolderProjection.put(LiveFolders.NAME,
                         KEY_NAME_COLUMN + " AS " +
                         LiveFolders.NAME);
liveFolderProjection.put(LiveFolders.DESCRIPTION,
                         KEY_DESCRIPTION_COLUMN + " AS " +
                         LiveFolders.DESCRIPTION);
liveFolderProjection.put(LiveFolders.IMAGE,
                         KEY_IMAGE_COLUMN + " AS " +
                         LiveFolders.IMAGE);

SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(MY_TABLES);
qb.setProjectionMap(LIVE_FOLDER_PROJECTION);

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

Live Folder Activity

The Live Folder itself is created with an Intent returned as a result from an Activity. The Intent's data property indicates the URI of the Content Provider supplying the data (with the appropriate projection applied), while a series of extras are used to configure settings such as the display mode, icon, and folder name.

Listing 10-16 shows the overridden onCreate method of an Activity used to create a Live Folder.

The Live Folder definition Intent is constructed and set as the Activity result, before the Activity is closed with a call to finish.

Example 10-16. Live Folder creation Activity

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  String action = getIntent().getAction();
  if (LiveFolders.ACTION_CREATE_LIVE_FOLDER.equals(action)) {
    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.icon));
    intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME, "Earthquakes");
    setResult(RESULT_OK, createLiveFolderIntent(this));
  }
  else
    setResult(RESULT_CANCELED);
  finish();
}

As well as the standard configuration values you can add a LiveFolders.EXTRA_LIVE_FOLDER_BASE_INTENT extra to specify a base Intent to fire when a Live Folder item is selected.

When an item is chosen the Live Folder will call showActivity, passing in an Intent that has the data parameter set to the Live Folder's base URI with the selected item's _id value appended.

When adding your Live Folder Activity to the application manifest you need to include an Intent Filter for the CREATE_LIVE_FOLDER action, as shown in Listing 10-17.

Example 10-17. Adding the Live Folder Intent Filter

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

Creating an Earthquake Live Folder

In the following example you'll extend the Earthquake application again, this time to include a Live Folder that displays the magnitude and location of each quake.

  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 LIVE_FOLDER = 3;
    
    static {
     uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     uriMatcher.addURI("com.paad.provider.Earthquake", "earthquakes", QUAKES);
     uriMatcher.addURI("com.paad.provider.Earthquake", "earthquakes/#", QUAKE_ID);
     uriMatcher.addURI("com.paad.provider.Earthquake", "live_folder", LIVE_FOLDER);
    }
    
    @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";
        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 4 to the returned earthquake query for Live Folder requests.

    @Override
    public Cursor query(Uri uri,
                        String[] projection,
                        String selection,
                        String[] selectionArgs,
                        String sort) {
    SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
      qb.setTables(EARTHQUAKE_TABLE);
    
      switch (uriMatcher.match(uri)) {
        case QUAKE_ID :
          qb.appendWhere(KEY_ID + "=" + uri.getPathSegments().get(1));
          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 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.icon));
      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 10-5 shows the earthquake Live Folder open on the home screen.

FIGURE 10-5

Figure 10-5. FIGURE 10-5

You could expand this example by using the Earthquake Map Activity to display a specific quake when it's selected from the list.

Start by adding an Intent Filter to the Earthquake Map Activity that listens for View actions on earthquake Content Provider data described by the Intent created in Step 6. Then improve the Activity to retrieve the location of the selected quake and center the map to that point.

ADDING SEARCH TO YOUR APPLICATIONS AND THE QUICK SEARCH BOX

With applications featuring large back-end databases and storing large volumes of data, the ability to search for information within an application is an increasingly important feature.

Android includes a framework to simplify searching within your Content Providers and surfacing the results using a consistent framework. This section explains how to add search functionality to your application using this search framework.

Adding Search to Your Application

Most Android devices feature a hardware search key. Using this framework you can expose your application-specific search functionality whenever a user presses the search button. The search box will dynamically display search results as the user types a query.

Creating a Search Activity

To enable application search, you must create an Activity that will be used to initiate and display the search.

The first step is to create a new searchable metadata XML resource in the res/xml folder. This file, shown in Listing 10-18, specifies the authority of the Content Provider you will be performing the search on, and the action to fire if a suggested search result is clicked.

Example 10-18. Defining application search metadata

<searchable xmlns:android="http://schemas.android.com/apk/res/android"
  android:label="@string/app_name"
  android:searchSuggestAuthority="myauthority"
  android:searchSuggestIntentAction="android.intent.action.VIEW">
</searchable>

Next, you will need to create an Activity that will be used to display the search results. In many cases this will be a simple List View-based Activity, but it can use any user interface you require. As shown in Listing 10-19, include a <meta-data> tag that includes a name attribute that specifies android.app.searchable and a resource attribute that specifies the XML resource you created in Listing 10-18.

You must also include an Intent Filter registered for the android.intent.action.SEARCH action and the DEFAULT category.

Example 10-19. Registering a search results Activity

<activity android:name=".EarthquakeSearch" android:label="Earthquake Search">
  <intent-filter>
    <action android:name="android.intent.action.SEARCH" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
  <meta-data
    android:name="android.app.searchable"
    android:resource="@xml/searchable"
  />
</activity>

The search query that caused this search result Activity to be displayed will be returned within the calling Intent using the SearchMananger.USER_QUERY extra as shown in the following:

String searchTerm = getIntent().getStringExtra(SearchManager.USER_QUERY);

It's good practice to use the same search results form for your entire application. To set an Activity as the default search results provider for an application you need to add a new <meta-data> tag to the <application> manifest node as shown in Listing 10-20.

Set the name attribute to android.app.default_searchable and specify your search Activity using the value attribute.

Example 10-20. Setting a default search result Activity for an application

<meta-data
  android:name="android.app.default_searchable"
  android:value=".EarthquakeSearch"
/>

Responding to Search Queries from a Content Provider

The search Activity described in the previous section can be used to initiate a search and display the results for an application. In order for it to have data to display you need to create (or modify) a Content Provider to handle search queries and return results.

To support the Android search framework you need to support specific query path URI values. Listing 10-21 shows a URI Matcher that compares a requested URI to the known search query path values.

Example 10-21. Detecting search requests in Content Providers

private static int SEARCH = 1;

static {
  uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
  uriMatcher.addURI("com.paad.provider.earthquake", "earthquakes", QUAKES);
  uriMatcher.addURI("com.paad.provider.earthquake", "earthquakes/#", QUAKE_ID);
  uriMatcher.addURI("com.paad.provider.earthquake",
    SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH);
  uriMatcher.addURI("com.paad.provider.earthquake",
    SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH);
  uriMatcher.addURI("com.paad.provider.earthquake",
    SearchManager.SUGGEST_URI_PATH_SHORTCUT, SEARCH);
  uriMatcher.addURI("com.paad.provider.earthquake",
    SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", SEARCH);
}

Use a similar URI Matcher pattern within your Content Provider to return the appropriate MIME type for search queries as shown in Listing 10-22. Search results should be returned as SearchManager.SUGGEST_MIME_TYPE in order to support live search suggestions.

Example 10-22. Returning the correct MIME type for search results

@Override
public String getType(Uri uri) {
  switch (uriMatcher.match(uri)) {
    case QUAKES  : 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);
  }
}

The URI Matcher can also be used within the query method. If an incoming search query is detected, find the search term by examining the last segment of the query URI.

uri.getPathSegments().get(1);

To return search results that can be displayed using the Android search framework you will need to create and apply a projection that assigns your column names to those supplied and supported by the Search Manager. The Search Manager class includes a number of static constants of the form SUGGEST_COLUMN_* that can be used in the projection.

There are two required columns, SUGGEST_COLUMN_TEXT_1 which displays the search result text and id_, which indicates the unique row ID.

Listing 10-23 shows the skeleton code for creating and applying a projection within a query that returns a Cursor suitable for search results.

Example 10-23. Returning search results from a query

private static final HashMap<String, String> SEARCH_PROJECTION_MAP;
static {
  SEARCH_PROJECTION_MAP = new HashMap<String, String>();
  SEARCH_PROJECTION_MAP.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
                            KEY_SEARCH_COLUMN + " AS " +
                            SearchManager.SUGGEST_COLUMN_TEXT_1);
  SEARCH_PROJECTION_MAP.put("_id", KEY_ID + " AS " + "_id");
}

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[]
                    selectionArgs, String sort) {
  SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
  qb.setTables(MY_TABLE);

  switch (uriMatcher.match(uri)) {
    case SINGLE_ID:
      qb.appendWhere(KEY_ID + "=" + uri.getPathSegments().get(1));
      break;
    case SEARCH   :  qb.appendWhere(KEY_SEARCH_COLUMN + " LIKE "%" +
                                    uri.getPathSegments().get(1) + "%"");
                     qb.setProjectionMap(SEARCH_PROJECTION_MAP);
                     break;
    default       : break;
  }

  Cursor c = qb.query(MyDB,
                      projection,
                      selection, selectionArgs,
                      null, null, orderBy);

  return c;
}

Surfacing Search Results to the Quick Search Box

Android 1.6 (API Level 4) introduced the ability to serve your application search results through the universal Quick Search Box widget.

The Quick Search Box is positioned prominently on the home screen, and the user can launch it at any time by pressing the hardware search key. 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.

To serve your search results to the Quick Search Box, you must first implement search functionality within your application as described in the previous section.

To make your results available globally, modify the searchable.xml file that describes the application search metadata and add two new attributes as shown in Listing 10-24:

  • searchSettingsDescription Used to describe your search results in the Settings menu.

  • includeInGlobalSearch Set this to true to surface these results to the quick search box.

Example 10-24. Adding your search result to the Quick Search Box

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

Note that your search results will not automatically be surfaced directly to the Quick Search Box. To avoid the possibility of misuse, adding new search providers requires users to opt-in.

To add new Quick Search Box search providers, use the system settings. Navigate to Settings

Adding your search result to the Quick Search Box

Note

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

Adding Search to the Earthquake Example

In the following example you'll add search functionality to the Earthquake project, and make sure results are available from the home-screen Quick Search Box.

  1. Start by adding two new string resources to the strings.xml file in the res/values folder. One will be the name used to identify the earthquake search results; the other will be a description of what they represent.

    <string name="search_label">Earthquakes</string>
    <string name="search_description">Earthquake locations</string>
  2. Next, create a new XML resources folder, res/xml. Create a new searchable.xml file which will define the metadata for your Earthquake search results provider. Specify the strings from Step 1 as the label and description values. Specify the Earthquake Content Provider's authority and set the includeInGlobalSearch attribute to true.

    <searchable xmlns:android="http://schemas.android.com/apk/res/android"
      android:label="@string/app_name"
      android:searchSettingsDescription="@string/app_name"
      android:includeInGlobalSearch="true"
      android:searchSuggestAuthority="com.paad.provider.earthquake"
      android:searchSuggestIntentAction="android.intent.action.VIEW">
    </searchable>
  3. Open the Earthquake Content Provider. Start by adding a new SEARCH_URI static constant that you can use to execute a search within the application.

    public static final Uri SEARCH_URI =
      Uri.parse("content://com.paad.provider.earthquake/" +
                SearchManager.SUGGEST_URI_PATH_QUERY);
  4. Now create a new Projection that will be used to supply search results.

    private static final HashMap<String, String> SEARCH_PROJECTION_MAP;
    static {
      SEARCH_PROJECTION_MAP = new HashMap<String, String>();
      SEARCH_PROJECTION_MAP.put(SearchManager.SUGGEST_COLUMN_TEXT_1, KEY_DETAILS +
        " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
      SEARCH_PROJECTION_MAP.put("_id", KEY_ID +
        " AS " + "_id");
    }
  5. Now modify the UriMatcher to include search queries.

    private static int SEARCH = 3;
    
    static {
      uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
      uriMatcher.addURI("com.paad.provider.earthquake", "earthquakes", QUAKES);
      uriMatcher.addURI("com.paad.provider.earthquake", "earthquakes/#", QUAKE_ID);
      uriMatcher.addURI("com.paad.provider.earthquake",
        SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH);
      uriMatcher.addURI("com.paad.provider.earthquake",
        SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH);
      uriMatcher.addURI("com.paad.provider.earthquake",
        SearchManager.SUGGEST_URI_PATH_SHORTCUT, SEARCH);
      uriMatcher.addURI("com.paad.provider.earthquake",
        SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", SEARCH);
    }
  6. Modify the getType method to return the appropriate MIME type for search results.

    @Override
    public String getType(Uri uri) {
      switch (uriMatcher.match(uri)) {
    case QUAKES  : 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);
      }
    }
  7. The final change to the Content Provider is to modify the query method to apply the search term and return the result query using the Projection you created in Step 4. This will allow the Quick Search Box search suggestions, and your search Activity, to display the results.

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[]
                        selectionArgs, String sort) {
      SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
      qb.setTables(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_DETAILS + " LIKE "%" +
                                       uri.getPathSegments().get(1) + "%"");
                        qb.setProjectionMap(SEARCH_PROJECTION_MAP);
                        break;
        default      : break;
      }
    
      [ ... existing query method ... ]
    }
  8. Now create a new Activity that will be used to display the search results. For these purposes, create a simple EarthquakeSearch Activity that extends ListActivity. EarthquakeSearch will only be displayed as a result of a search query, so extract the user query from the search Intent that launched the application and use it to query the Earthquake Content Provider. Create a Simple Cursor Adapter to bind the search results cursor to the Activity's List View.

    import android.app.ListActivity;
    import android.app.SearchManager;
    import android.database.Cursor;
    import android.net.Uri;
    import android.os.Bundle;
    import android.widget.SimpleCursorAdapter;
    
    public class EarthquakeSearch extends ListActivity {
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        String searchTerm = getIntent().getStringExtra(SearchManager.USER_QUERY);
        String searchQuery = Uri.withAppendedPath(EarthquakeProvider.SEARCH_URI,
                                                   searchTerm);
    Cursor c = getContentResolver().query(searchQuery, null, null, null, null);
        startManagingCursor(c);
    
        String[] from = new String[] {SearchManager.SUGGEST_COLUMN_TEXT_1};
        int[] to = new int[] {android.R.id.text1};
        SimpleCursorAdapter searchResults = new SimpleCursorAdapter(this,
          android.R.layout.simple_list_item_1, c, from, to);
          setListAdapter(searchResults);
      }
    }
  9. Open the application Manifest and add the new EarthquakeSearch Activity. Make sure you add an Intent Filter for the SEARCH action in the DEFAULT category. You will also need to add a <meta-data> tag that specifies the searchable XML resource you created in Step 2.

    <activity android:name=".EarthquakeSearch" android:label="Earthquake Search">
      <intent-filter>
        <action android:name="android.intent.action.SEARCH" />
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
      <meta-data
        android:name="android.app.searchable"
        android:resource="@xml/searchable"
      />
    </activity>
  10. The final step is to add a new <meta-data> tag to the <application> node in the manifest that describes the EarthquakeSearch Activity as the default search provider for the application.

    <application android:icon="@drawable/icon">
      <meta-data
        android:name="android.app.default_searchable"
        android:value=".EarthquakeSearch"
      />
      [ ... existing application node ... ]
    </application>
    
    Adding Search to the Earthquake Example

If you run this application, pressing the hardware search key in any of the Activities will provide a search box that returns suggestions and search results as you type. To have your results available in the home screen Quick Search Box, you will need to go to Settings

Adding Search to the Earthquake Example

CREATING LIVE WALLPAPER

Live Wallpaper is a new way to add an application component to the home screen introduced in Android 2.1 (API level 7). Live Wallpaper lets you create dynamic, interactive home-screen backgrounds, providing you with an exciting new alternative for displaying information to your users directly on the home screen.

Live Wallpaper uses a Surface to render a dynamic display and listens for screen touch events to let users interact with the display.

To create a new Live Wallpaper you need three components:

  • A Live Wallpaper XML resource

  • A Wallpaper Service implementation

  • A Wallpaper Engine implementation (returned through the Wallpaper Service)

Creating a Live Wallpaper Definition Resource

The Live Wallpaper resource definition is an XML file stored in the res/xml folder. Use attributes within the <wallpaper> tag to define the author name, wallpaper description, and thumbnail to display in the Live Wallpaper gallery at run time. You can also use the settingsActivity tag to specify an Activity to launch to configure the wallpaper's settings.

Listing 10-25 shows a sample Live Wallpaper resource.

Example 10-25. 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"
/>

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

Creating a Wallpaper Service

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

All the drawing and interaction for Live Wallpaper is handled in the Wallpaper Service Engine class described later in this chapter. Override the onCreateEngine handler to return a new instance of your custom Wallpaper Service Engine as shown in Listing 10-26.

Example 10-26. A Live Wallpaper Service

public class MyWallpaperService extends WallpaperService {
  @Override
  public Engine onCreateEngine() {
    return new MyWallpaperServiceEngine();
  }
}

Once you've created it, add your Live Wallpaper Service to your application manifest using the <service> tag. A Live Wallpaper must also include an Intent Filter to listen for the android.service.wallpaper.WallpaperService action, and a <meta-data> node that specifies the android.service.wallpaper as the name attribute, and associates it with the resource file described in the previous section using a resource attribute.

Your Live Wallpaper Service must also require the android.permission.BIND_WALLPAPER permission using the android.permission attribute. Listing 10-27 shows how to add the Live Wallpaper from Listing 10-26 to the manifest.

Example 10-27. Adding a Live Wallpaper Service to the manifest

<service android:name=".MyWallpaperService"
  android.permission="android.permission.BIND_WALLPAPER">
  <intent-filter>
    <action android:name="android.service.wallpaper.WallpaperService" />
  </intent-filter>
  <meta-data
    android:name="android.service.wallpaper"
    android:resource="@xml/wallpaper"
  />
</service>

Creating a Wallpaper Service Engine

The WallpaperService.Engine class is where you create the Live Wallpaper itself.

The Wallpaper Service Engine encapsulates a Surface which is used to display the wallpaper and handle touch events. A Surface is a specialized drawing canvas that supports updates from background threads, making it ideal for creating smooth, dynamic, and interactive graphics. Both the Surface View, and handling touch events, are covered in more detail in Chapter 15.

To implement your own Wallpaper Service engine, extend the WallpaperService.Engine class. Before you can start drawing on the Surface, you must wait for it to complete initialization, indicated by the onSurfaceCreated event handler.

The Wallpaper Service Engine also includes an onTouchEvent callback to provide user-interactivity with the wallpaper, and the onOffsetsChanged handler to notify you that the parent Activity (usually the home screen) has been offset (panned).

Listing 10-28 shows the skeleton code for a Wallpaper Service Engine implementation. Refer to Chapter 15 for more details on how to draw on a Surface and use the onTouchEvent handler and Motion Events.

Example 10-28. Wallpaper Service Engine skeleton code

public class MyWallpaperServiceEngine extends WallpaperService.Engine {
  @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);
    // TODO Handle homescreen offset events.
  }

  @Override
  public void onTouchEvent(MotionEvent event) {
    super.onTouchEvent(event);
    // TODO Handle touch and motion events.
  }

  @Override
  public void onSurfaceCreated(SurfaceHolder holder) {
    super.onSurfaceCreated(holder);
    // TODO Surface has been created, run the Thread that will
    // update the display.
  }
}

SUMMARY

In this chapter you learned how to create App Widgets and Live Folders for your application.

In particular you saw how to do the following:

  • Implement widgets and add them to your applications.

  • Control the update rate of your widgets by setting the minimum refresh rate or using Intents and Alarms.

  • Update the UI of your widgets using Remote Views.

  • Add interactivity to your widgets.

  • Create and register a Live Folder for your application's Content Provider.

  • Add a projection to your Content Provider to provide a Live Folder schema.

  • Create and use Live Wallpaper.

  • Add search to your application and surface search results to the Quick Search Box.

In the following chapter you will explore the audiovisual APIs available in Android. You'll take a look at multimedia playback and recording using the microphone and camera.

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

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