Chapter 5. Intents, Broadcast Receivers, Adapters, and the Internet

WHAT'S IN THIS CHAPTER?

  • An introduction to Intents

  • Starting new Activities and sub-Activities using implicit and explicit Intents

  • Intent filters and intent resolution

  • Using linkify

  • Intents, broadcast actions and Broadcast Receivers

  • Using Adapters to bind data to Views

  • Using the Internet in Android

  • How to create and use Dialogs

At first glance the subjects of this chapter might appear to have little in common; in practice they represent the glue that binds applications and their components.

Mobile applications on most platforms run in their own sandboxes. They're isolated from each other, and have strict limitations applied to their interaction with hardware and native components. Android applications are also sandboxed but they can use Intents, Broadcast Receivers, Adapters, Content Providers, and the Internet to interact through those boundaries.

In this chapter you'll look at Intents. Intents are probably the most unique, and important, concept in Android development. You'll learn how to use Intents to broadcast data between applications and application components, and start Activities or Services, both explicitly and using late runtime binding.

Using implicit Intents you'll learn how to request that an action be performed on a piece of data, letting Android determine which application components can best service that request.

Broadcast Intents are used to announce events system-wide. You'll learn how to transmit these broadcasts, and receive them using Broadcast Receivers.

You'll examine Adapters and learn how to use them to bind your presentation layer to data sources, before examining dialog boxes.

Having looked at the mechanisms for transmitting and consuming local data, you'll be introduced to Android's Internet connectivity model and some of the Java techniques for parsing Internet data feeds.

An earthquake-monitoring example will then demonstrate how to tie all these features together. The earthquake monitor will form the basis of an ongoing example that you'll improve and extend in later chapters.

INTRODUCING INTENTS

Intents are used as a message-passing mechanism that works both within your application, and between applications. Intents can be used to:

  • Declare your intention that an Activity or Service be started to perform an action, usually with (or on) a particular piece of data

  • Broadcast that an event (or action) has occurred

  • Explicitly start a particular Service or Activity

You can use Intents to support interaction among any of the application components installed on an Android device, no matter which application they're a part of. This turns your device from a platform containing a collection of independent components into a single interconnected system.

One of the most common uses for Intents is to start new Activities, either explicitly (by specifying the class to load) or implicitly (by requesting that an action be performed on a piece of data). In the latter case the action need not be performed by an Activity within the calling application.

Intents can also be used to broadcast messages across the system. Any application can register Broadcast Receivers to listen for, and react to, these broadcast Intents. This lets you create event-driven applications based on internal, system, or third-party-application events.

Android broadcasts Intents to announce system events, like changes in Internet connection status or battery charge levels. The native Android applications, such as the phone dialer and SMS manager, simply register components that listen for specific broadcast Intents — such as "incoming phone call" or "SMS message received" — and react accordingly.

Using Intents to propagate actions — even within the same application — is a fundamental Android design principle. It encourages the decoupling of components, to allow the seamless replacement of application elements. It also provides the basis of a simple model for extending an application's functionality.

Using Intents to Launch Activities

The most common use of Intents is to bind your application components. Intents are used to start, and transition between, Activities.

Note

The instructions given in this section refer to starting new Activities, but the same details also apply to Services. Details on starting (and creating) Services are available in Chapter 9.

To open an Activity, call startActivity, passing in an Intent as shown in the following snippet:

startActivity(myIntent);

The Intent can either explicitly specify the Activity class to open, or include an action that an Activity must perform. In the latter case the run time will choose an Activity dynamically using a process known as Intent resolution.

The startActivity method finds and starts the single Activity that best matches your Intent.

When you use startActivity your application won't receive any notification when the newly launched Activity finishes. To track feedback from the opened screen use the startActivityForResult method described in more detail in the next section.

Explicitly Starting New Activities

You learned in Chapter 2 that applications consist of a number of interrelated screens — Activities — that must be included in the application manifest. To connect them you may want to explicitly specify an Activity to open.

To explicitly select an Activity class to start, create a new Intent, specifying the current application Context and Activity class to launch. Pass this Intent in to startActivity as shown in Listing 5-1.

Example 5-1. Explicitly starting an Activity

Intent intent = new Intent(MyActivity.this, MyOtherActivity.class);
startActivity(intent);

After startActivity is called, the new Activity (in this example MyOtherActivity) will be created and become visible and active, moving to the top of the Activity stack.

Calling finish on the new Activity, or pressing the hardware back button, will close it and remove it from the stack. Alternatively, developers can navigate to the previous Activity, or yet another Activity, by calling startActivity.

Implicit Intents and Late Runtime Binding

An implicit Intent is a mechanism that lets anonymous application components service action requests. That means you can ask the system to launch an Activity that can perform a given action without knowing which application, or Activity, will do so.

When constructing a new implicit Intent to use with startActivity, you nominate an action to perform and, optionally, supply the URI of the data to perform that action on. You can also send additional data to the target Activity by adding extras to the Intent.

When you use this Intent to start an Activity, Android will — at run time — resolve it into the Activity class best suited to performing the required action on the type of data specified. This means you can create projects that use functionality from other applications, without knowing exactly which application you're borrowing functionality from ahead of time.

For example, to let users make calls from your application you could implement a new dialer, or you could use an implicit Intent that requests the action (dialing) be performed on a phone number (represented as a URI), as shown in Listing 5-2.

Example 5-2. Implicitly starting an Activity

if (somethingWeird && itDontLookGood) {
  Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:555-2368"));
  startActivity(intent);
}

Android resolves this Intent and starts an Activity that provides the dial action on a telephone number — in this case the dialer Activity.

In circumstances where multiple Activities are capable of performing a given action, the user is presented with a choice. The full process of Intent resolution is described later in this chapter.

Various native applications provide Activities to handle actions performed on specific data. Third-party applications, including your own, can be registered to support new actions, or to provide an alternative provider of native actions. You'll be introduced to some of the native actions, and how to register your own Activities to support them, later in this chapter.

Returning Results from Activities

An Activity started via startActivity is independent of its parent and will not provide any feedback when it closes.

Alternatively, you can start an Activity as a sub-Activity that's inherently connected to its parent. A sub-Activity triggers an event handler within its parent Activity when it closes. Sub-Activities are perfect for situations in which one Activity is providing data input (such as a user's selecting an item from a list) for another.

Sub-Activities are really just Activities opened in a different way. As such they must be registered in the application manifest — in fact any manifest-registered Activity can be opened as a sub-Activity including system or third-party application Activities.

Launching Sub-Activities

The startActivityForResult method works much like startActivity, but with one important difference. As well as the explicit or implicit Intent used to determine which Activity to launch, you also pass in a request code. This value will later be used to uniquely identify the sub-Activity that has returned a result.

The skeleton code for launching a sub-Activity is shown in Listing 5-3.

Example 5-3. Starting an Activity for a result

private static final int SHOW_SUBACTIVITY = 1;

Intent intent = new Intent(this, MyOtherActivity.class);
startActivityForResult(intent, SHOW_SUBACTIVITY);

Like regular Activities, sub-Activities can be started implicitly or explicitly. Listing 5-4 uses an implicit Intent to launch a new sub-Activity to pick a contact.

Example 5-4. Implicitly starting an Activity for a result

private static final int PICK_CONTACT_SUBACTIVITY = 2;

Uri uri = Uri.parse("content://contacts/people");
Intent intent = new Intent(Intent.ACTION_PICK, uri);
startActivityForResult(intent, PICK_CONTACT_SUBACTIVITY);

Returning Results

When your sub-Activity is ready to return, call setResult before finish to return a result to the calling Activity.

The setResult method takes two parameters: the result code and the result itself, represented as an Intent.

The result code is the "result;" of running the sub-Activity — generally either Activity.RESULT_OK or Activity.RESULT_CANCELED. In some circumstances you'll want to use your own response codes to handle application specific choices; setResult supports any integer value.

The Intent returned as a result often includes a URI to a piece of content (such as the selected contact, phone number, or media file) and a collection of extras used to return additional information.

Listing 5-5 is taken from a sub-Activity's onCreate method, and shows how an OK and Cancel button might return different results to the calling Activity.

Example 5-5. Creating new Shared Preferences

Button okButton = (Button) findViewById(R.id.ok_button);
  okButton.setOnClickListener(new View.OnClickListener() {
  public void onClick(View view) {
    Uri data = Uri.parse("content://horses/" + selected_horse_id);

    Intent result = new Intent(null, data);
    result.putExtra(IS_INPUT_CORRECT, inputCorrect);
    result.putExtra(SELECTED_PISTOL, selectedPistol);
setResult(RESULT_OK, result);
    finish();
  }
});

Button cancelButton = (Button) findViewById(R.id.cancel_button);
cancelButton.setOnClickListener(new View.OnClickListener() {
  public void onClick(View view) {
    setResult(RESULT_CANCELED, null);
    finish();
  }
});

If the Activity is closed by the user pressing the hardware back key, or finish is called without a prior call to setResult, the result code will be set to RESULT_CANCELED and the result Intent set to null.

Handling Sub-Activity Results

When a sub-Activity closes, the onActivityResult event handler is fired within the calling Activity. Override this method to handle the results returned by sub-Activities.

The onActivityResult handler receives a number of parameters:

  • Request code The request code that was used to launch the returning sub-Activity.

  • Result code The result code set by the sub-Activity to indicate its result. It can be any integer value, but typically will be either Activity.RESULT_OK or Activity.RESULT_CANCELED.

    Note

    If the sub-Activity closes abnormally, or doesn't specify a result code before it closes, the result code is Activity.RESULT_CANCELED.

  • Data An Intent used to package returned data. Depending on the purpose of the sub-Activity, it may include a URI that represents a selected piece of content. Alternatively, or additionally, the sub-Activity can return extra information as primitive values using the Intent extras Bundle.

The skeleton code for implementing the onActivityResult event handler within an Activity is shown in Listing 5-6.

Example 5-6. Implementing an On Activity Result Handler

private static final int SHOW_SUB_ACTIVITY_ONE = 1;
private static final int SHOW_SUB_ACTIVITY_TWO = 2;

@Override
public void onActivityResult(int requestCode,
int resultCode,
                             Intent data) {

  super.onActivityResult(requestCode, resultCode, data);

  switch(requestCode) {
    case (SHOW_SUB_ACTIVITY_ONE) : {
      if (resultCode == Activity.RESULT_OK) {
        Uri horse = data.getData();
        boolean inputCorrect = data.getBooleanExtra(IS_INPUT_CORRECT, false);
        String selectedPistol = data.getStringExtra(SELECTED_PISTOL);
      }
      break;
    }
    case (SHOW_SUB_ACTIVITY_TWO) : {
      if (resultCode == Activity.RESULT_OK) {
        // TODO: Handle OK click.
      }
      break;
    }

  }
}

Native Android Actions

Native Android applications also use Intents to launch Activities and sub-Activities.

The following non-comprehensive list shows some of the native actions available as static string constants in the Intent class. When creating implicit Intents you can use these actions, called Activity Intents, to start Activities and sub-Activities within your own applications.

Note

Later you will be introduced to Intent Filters and you'll learn how to register your own Activities as handlers for these actions.

  • ACTION_ANSWER Opens an Activity that handles incoming calls. Currently this is handled by the native in-call screen.

  • ACTION_CALL Brings up a phone dialer and immediately initiates a call using the number supplied in the Intent URI. Generally it's considered better form to use ACTION_DIAL if possible.

  • ACTION_DELETE Starts an Activity that lets you delete the data specified at the Intent's data URI.

  • ACTION_DIAL Brings up a dialer application with the number to dial pre-populated from the Intent URI. By default this is handled by the native Android phone dialer. The dialer can normalize most number schemas: for example, tel:555-1234 and tel:(212) 555 1212 are both valid numbers.

  • ACTION_EDIT Requests an Activity that can edit the data at the specified Intent URI.

  • ACTION_INSERT Opens an Activity capable of inserting new items into the Cursor specified in the Intent URI. When called as a sub-Activity it should return a URI to the newly inserted item.

  • ACTION_PICK Launches a sub-Activity that lets you pick an item from the Content Provider specified by the Intent URI. When closed it should return a URI to the item that was picked. The Activity launched depends on the data being picked: for example, passing content://contacts/people will invoke the native contacts list.

  • ACTION_SEARCH Launches the Activity used for performing a search. Supply the search term as a string in the Intent's extras using the SearchManager.QUERY key.

  • ACTION_SENDTO Launches an Activity to send a message to the contact specified by the Intent URI.

  • ACTION_SEND Launches an Activity that sends the data specified in the Intent. The recipient contact needs to be selected by the resolved Activity. Use setType to set the MIME type of the transmitted data.

    The data itself should be stored as an extra by means of the key EXTRA_TEXT or EXTRA_STREAM, depending on the type. In the case of e-mail, the native Android applications will also accept extras via the EXTRA_EMAIL, EXTRA_CC, EXTRA_BCC, and EXTRA_SUBJECT keys. Use the ACTION_SEND action only to send data to a remote recipient (not another application on the device).

  • ACTION_VIEW The most common generic action. View asks that the data supplied in the Intent's URI be viewed in the most reasonable manner. Different applications will handle view requests depending on the URI schema of the data supplied. Natively http: addresses will open in the browser, tel: addresses will open the dialer to call the number, geo: addresses will be displayed in the Google Maps application, and contact content will be displayed in the contact manager.

  • ACTION_WEB_SEARCH Opens an Activity that performs a web search based on the text supplied in the Intent URI (typically the browser).

Note

As well as these Activity actions, Android includes a large number of broadcast actions used to create Intents that are broadcast to announce system events. These broadcast actions are described later in this chapter.

Using Intent Filters to Service Implicit Intents

If an Intent is a request for an action to be performed on a set of data, how does Android know which application (and component) to use to service the request?

Intent Filters are used to register Activities, Services, and Broadcast Receivers as being capable of performing an action on a particular kind of data. Intent Filters are also used to register Broadcast Receivers as being interested in Intents broadcasting a given action or event.

Using Intent Filters, application components announce that they can respond to action requests from any application installed on the device.

To register an application component as a potential Intent handler, add an intent-filter tag to the component's manifest node using the following tags (and associated attributes) within the Intent Filter node:

  • action Uses the android:name attribute to specify the name of the action being serviced. Each Intent Filter must have one (and only one) action tag. Actions should be unique strings that are self-describing. Best practice is to use a naming system based on the Java package naming conventions.

  • category Uses the android:name attribute to specify under which circumstances the action should be serviced. Each Intent Filter tag can include multiple category tags. You can specify your own categories or use the standard values provided by Android and listed here:

    • ALTERNATIVE This category specifies that this action should be available as an alternative to the default action performed on an item of this data type. For example, where the default action for a contact is to view it, the alternative could be to edit it.

    • SELECTED_ALTERNATIVE Similar to the ALTERNATIVE category, but where that category will always resolve to a single action using the Intent resolution described below, SELECTED_ALTERNATIVE is used when a list of possibilities is required. As you'll see later in this chapter, one of the uses of Intent Filters is to help populate Context Menus dynamically using actions.

    • BROWSABLE Specifies an action available from within the browser. When an Intent is fired from within the browser it will always include the browsable category. If you want your application to respond to actions triggered within the browser (e.g., intercepting links to a particular web site), you must include the browsable category.

    • DEFAULT Set this to make a component the default action for the data type specified in the Intent Filter. This is also necessary for Activities that are launched using an explicit Intent.

    • GADGET By setting the gadget category you specify that this Activity can run embedded inside another Activity.

    • HOME By setting an Intent Filter category as home without specifying an action, you are presenting it as an alternative to the native home screen.

    • LAUNCHER Using this category makes an Activity appear in the application launcher.

  • data The data tag lets you specify which data types your component can act on; you can include several data tags as appropriate. You can use any combination of the following attributes to specify the data your component supports:

    • android:host Specifies a valid hostname (e.g., google.com).

    • android:mimetype Lets you specify the type of data your component is capable of handling. For example, <type android:value="vnd.android.cursor.dir/*"/> would match any Android cursor.

    • android:path Specifies valid "path" values for the URI (e.g., /transport/boats/).

    • android:port Specifies valid ports for the specified host.

    • android:scheme Requires a particular scheme (e.g., content or http).

Listing 5-7 shows an Intent Filter for an Activity that can perform the SHOW_DAMAGE action as either a primary or an alternative action (you'll create earthquake content in the next chapter).

Example 5-7. Registering an Activity as an Intent Receiver

<activity android:name=".EarthquakeDamageViewer" android:label="View Damage">
  <intent-filter>
    <action android:name="com.paad.earthquake.intent.action.SHOW_DAMAGE"></action>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.SELECTED_ALTERNATIVE"/>
    <data android:mimeType="vnd.earthquake.cursor.item/*"/>
  </intent-filter>
</activity>

How Android Resolves Intent Filters

When you use startActivity, the implicit Intent passed in usually resolves to a single Activity. If there are multiple Activities capable of performing the given action on the specified data, the user will be presented with a list of alternatives.

The process of deciding which Activity to start is called Intent resolution. The aim of Intent resolution is to find the best Intent Filter match possible by means of the following process:

  1. Android puts together a list of all the Intent Filters available from the installed packages.

  2. Intent Filters that do not match the action or category associated with the Intent being resolved are removed from the list.

    • 2.1. Action matches are made if the Intent Filter either includes the specified action or has no action specified. An Intent Filter will fail the action match check only if it has one or more actions defined, and none of them matches the action specified by the Intent.

    • 2.2. Category matching is stricter. Intent Filters must include all the categories defined in the resolving Intent. An Intent Filter with no categories specified matches only Intents with no categories.

  3. Finally, each part of the Intent's data URI is compared to the Intent Filter's data tag. If the Intent Filter specifies a scheme, host/authority, path, or MIME type these values are compared to the Intent's URI. Any mismatch will remove the Intent Filter from the list. Specifying no data values in an Intent Filter will result in a match with all Intent data values.

    • 3.1. The MIME type is the data type of the data being matched. When matching data types you can use wildcards to match subtypes (e.g., earthquakes/*). If the Intent Filter specifies a data type it must match the Intent; specifying no data types results in a match with all of them.

    • 3.2. The scheme is the "protocol" part of the URI — for example, http:, mailto:, or tel:.

    • 3.3. The hostname or data authority is the section of the URI between the scheme and the path (e.g., www.google.com). For a hostname to match, the Intent Filter's scheme must also pass.

    • 3.4. The data path is what comes after the authority (e.g., /ig). A path can match only if the scheme and hostname parts of the data tag also match.

  4. When you implicitly start an Activity, if more than one component is resolved from this process all the matching possibilities are offered to the user.

Native Android application components are part of the Intent resolution process in exactly the same way as third-party applications. They do not have a higher priority, and can be completely replaced with new Activities that declare Intent Filters that service the same actions.

Finding and Using the Launch Intent Within an Activity

When an application component is started through an implicit Intent, it needs to find the action it's to perform and the data to perform it on.

Call the getIntent method — usually from within the onCreate method — to extract the Intent used to start a component, as in Listing 5-8.

Example 5-8. Finding the launch Intent in a sub-Activity

@Override
public void onCreate(Bundle icicle) {
  super.onCreate(icicle);
  setContentView(R.layout.main);

  Intent intent = getIntent();
}

Use the getData and getAction methods to find the data and action associated with the Intent. Use the type-safe get<type>Extra methods to extract additional information stored in its extras Bundle.

String action = intent.getAction();
Uri data = intent.getData();

Passing on Responsibility

Use the startNextMatchingActivity method to pass responsibility for action handling to the next best matching application component, as shown in Listing 5-9.

Example 5-9. Passing on Intent Receiver Handling

Intent intent = getIntent();
if (isDuringBreak)
  startNextMatchingActivity(intent);

This lets you add additional conditions to your components that restrict their use beyond the ability of the Intent Filter-based Intent resolution process.

In some cases your component may wish to perform some processing, or offer the user a choice, before passing the Intent on to an alternative component.

Select a Contact Example

In this example you'll create a new Activity that services ACTION_PICK for contact data. It displays each of the contacts in the contacts database and lets the user select one, before closing and returning the selected contact's URI to the calling Activity.

Note

It's worth noting that this example is somewhat contrived. Android already supplies an Intent Filter for picking a contact from a list that can be invoked by means of the content://contacts/people/ URI in an implicit Intent. The purpose of this exercise is to demonstrate the form, even if this particular implementation isn't particularly useful.

  1. Create a new ContactPicker project that includes a ContactPicker Activity:

    package com.paad.contactpicker;
    
    import android.app.Activity;
    import android.content.Intent;
    import android.database.Cursor;
    import android.net.Uri;
    import android.os.Bundle;
    import android.provider.Contacts.People;
    import android.view.View;
    import android.widget.AdapterView;
    import android.widget.ListView;
    import android.widget.SimpleCursorAdapter;
    import android.widget.AdapterView.OnItemClickListener;
    
    public class ContactPicker extends Activity {
      @Override
      public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.main);
      }
    }
  2. Modify the main.xml layout resource to include a single ListView control. This control will be used to display the contacts.

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      >
      <ListView android:id="@+id/contactListView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
    />
    </LinearLayout>
  3. Create a new listitemlayout.xml layout resource that includes a single TextView. This will be used to display each contact in the List View.

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      >
      <TextView
        android:id="@+id/itemTextView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:padding="10px"
        android:textSize="16px"
        android:textColor="#FFF"
      />
    </LinearLayout>
  4. Return to the ContactPicker Activity. Override the onCreate method and extract the data path from the calling Intent:

    @Override
    public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      setContentView(R.layout.main);
    
      Intent intent = getIntent();
      String dataPath = intent.getData().toString();
    • 4.1. Create a new data URI for the people stored in the contact list, and bind it to the List View using a SimpleCursorArrayAdapter:

      Note

      The SimpleCursorArrayAdapter lets you assign Cursor data, used by Content Providers, to Views. It's used here without further comment but is examined in detail later in this chapter.

      final Uri data = Uri.parse(dataPath + "people/");
        final Cursor c = managedQuery(data, null, null, null);
      
        String[] from = new String[] {People.NAME};
        int[] to = new int[] { R.id.itemTextView };
      
        SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
                                                              R.layout.listitemlayout,
                                                              c,
                                                              from,
                                                              to);
        ListView lv = (ListView)findViewById(R.id.contactListView);
        lv.setAdapter(adapter);
    • 4.2. Add an onItemClickListener to the List View. Selecting a contact from the list should return a path to the item to the calling Activity.

      lv.setOnItemClickListener(new OnItemClickListener() {
          @Override
          public void onItemClick(AdapterView<?> parent, View view, int pos,
                                  long id) {
            // Move the cursor to the selected item
            c.moveToPosition(pos);
            // Extract the row id.
            int rowId = c.getInt(c.getColumnIndexOrThrow("_id"));
            // Construct the result URI.
            Uri outURI = Uri.parse(data.toString() + rowId);
            Intent outData = new Intent();
            outData.setData(outURI);
            setResult(Activity.RESULT_OK, outData);
            finish();
          }
        });
    • 4.3. Close off the onCreate method:

      }
  5. Modify the application manifest and replace the intent-filter tag of the Activity to add support for the ACTION_PICK action on contact data:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.paad.contactpicker">
        <application android:icon="@drawable/icon">
            <activity android:name="ContactPicker" android:label="@string/app_name">
                <intent-filter>
                  <action android:name="android.intent.action.PICK"></action>
                  <category android:name="android.intent.category.DEFAULT"></category>
                  <data android:path="contacts" android:scheme="content"></data>
                </intent-filter>
            </activity>
        </application>
    </manifest>
  6. This completes the sub-Activity. To test it, create a new test harness ContentPickerTester Activity. Create a new layout resource — contentpickertester.xml — that includes a TextView to display the selected contact and a Button to start the sub-Activity:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      >
      <TextView
        android:id="@+id/selected_contact_textview"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
      />
      <Button
    android:id="@+id/pick_contact_button"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Pick Contact"
      />
    </LinearLayout>
  7. Override the onCreate method of the ContentPickerTester to add a click listener to the Button so that it implicitly starts a new sub-Activity by specifying the ACTION_PICK and the contact database URI (content://contacts/):

    package com.paad.contactpicker;
    
    import android.app.Activity;
    import android.content.Intent;
    import android.database.Cursor;
    import android.net.Uri;
    import android.os.Bundle;
    import android.provider.Contacts.People;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Button;
    import android.widget.TextView;
    
    public class ContentPickerTester extends Activity {
    
      public static final int PICK_CONTACT = 1;
    
      @Override
      public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.contentpickertester);
    
        Button button = (Button)findViewById(R.id.pick_contact_button);
    
        button.setOnClickListener(new OnClickListener() {
         @Override
         public void onClick(View _view) {
            Intent intent = new Intent(Intent.ACTION_PICK,
                                       Uri.parse("content://contacts/"));
            startActivityForResult(intent, PICK_CONTACT);
          }
        });
      }
    }
  8. When the sub-Activity returns, use the result to populate the Text View with the selected contact's name:

    @Override
    public void onActivityResult(int reqCode, int resCode, Intent data) {
      super.onActivityResult(reqCode, resCode, data);
    
      switch(reqCode) {
        case (PICK_CONTACT) : {
    if (resCode == Activity.RESULT_OK) {
            Uri contactData = data.getData();
            Cursor c = managedQuery(contactData, null, null, null);
            c.moveToFirst();
            String name = c.getString(c.getColumnIndexOrThrow(People.NAME));
            TextView tv = (TextView)findViewById(R.id.selected_contact_textview);
            tv.setText(name);
          }
          break;
        }
      }
    }
  9. With your test harness complete, simply add it to your application manifest. You'll also need to add a READ_CONTACTS permission within a uses-permission tag, to allow the application to access the contacts database.

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.paad.contactpicker">
        <application android:icon="@drawable/icon">
          <activity android:name=".ContactPicker" android:label="@string/app_name">
            <intent-filter>
              <action android:name="android.intent.action.PICK"></action>
              <category android:name="android.intent.category.DEFAULT"></category>
              <data android:path="contacts" android:scheme="content"></data>
            </intent-filter>
          </activity>
          <activity android:name=".ContentPickerTester"
                    android:label="Contact Picker Test">
            <intent-filter>
              <action android:name="android.intent.action.MAIN" />
              <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
          </activity>
        </application>
        <uses-permission android:name="android.permission.READ_CONTACTS"/>
    </manifest>
    
    Select a Contact Example

When your Activity is running, press the button. The contact picker Activity should be shown as in Figure 5-1.

Once you select a contact, the parent Activity should return to the foreground with the selected contact name displayed, as shown in Figure 5-2.

Using Intent Filters for Plug-Ins and Extensibility

You've now learned how to create implicit Intents to launch Activities, but that's only half the story. Android also lets future packages provide new functionality for existing applications, using Intent Filters to populate menus dynamically at run time.

This provides a plug-in model for your Activities that lets them take advantage of future functionality, provided through application components you haven't yet conceived of, without your having to modify or recompile your projects.

The addIntentOptions method available from the Menu class lets you specify an Intent that describes the data acted upon by the Menu. Android resolves this Intent and returns every action specified in Intent Filters that match the specified data. A new Menu Item is created for each, with the text populated from the matching Intent Filters' labels.

FIGURE 5-1

Figure 5-1. FIGURE 5-1

FIGURE 5-2

Figure 5-2. FIGURE 5-2

The elegance of this concept is best explained by example. If the data your Activity displays is a list of places, the Menu Items available might include View and "Show directions to." Jump a few years ahead and you've created an application that interfaces with your car, allowing your phone to handle driving. Thanks to the runtime menu generation, when a new Intent Filter — with a DRIVE_CAR action — is included within the new Activity's node, Android will automagically add this action as a new Menu Item in your earlier application.

Runtime menu population provides the ability to retrofit functionality when you create new components capable of performing actions on a given type of data. Many of Android's native applications use this functionality, giving you the ability to provide additional actions to native Activities.

Supplying Anonymous Actions to Applications

To use this mechanism to make your Activity's actions available anonymously for existing applications, publish them using intent-filter tags within their manifest nodes.

The Intent Filter describes the action it performs and the data upon which it can be performed. The latter will be used during the Intent resolution process to determine when this action should be available. The category tag must be either ALTERNATIVE or SELECTED_ALTERNATIVE or both. The text used for the Menu Items is specified by the android:label attribute.

Listing 5-10 shows an example of an Intent Filter used to advertise an Activity's ability to nuke moon-bases from orbit.

Example 5-10. Advertising-supported Activity actions

<activity android:name=".NostromoController">
  <intent-filter
    android:label="Nuke From Orbit">
    <action android:name="com.pad.nostromo.NUKE_FROM_ORBIT" />
    <data android:mimeType="vnd.moonbase.cursor.item/*"/>
    <category android:name="android.intent.category.ALTERNATIVE" />
    <category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
  </intent-filter>
</activity>

The Content Provider and other code needed for this example to run aren't provided; in the following sections you'll see how to write the code that adds this action dynamically to another Activity's Menu.

Incorporating Anonymous Actions in Your Activity's Menu

To add Menu Items to your Menus dynamically at run time, use the addIntentOptions method on the Menu object in question: pass in an Intent that specifies the data for which you want to provide actions. Generally this will be handled within your Activities' onCreateOptionsMenu or onCreateContextMenu handlers.

The Intent you create will be used to resolve components with Intent Filters that supply actions for the data you specify. The Intent is being used to find actions, so don't assign it one; it should specify only the data to perform actions on. You should also specify the category of the action, either CATEGORY_ALTERNATIVE or CATEGORY_SELECTED_ALTERNATIVE.

The skeleton code for creating an Intent for menu-action resolution is shown here:

Intent intent = new Intent();
intent.setData(MyProvider.CONTENT_URI);
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);

Pass this Intent in to addIntentOptions on the Menu you wish to populate, as well as any option flags, the name of the calling class, the menu group to use, and the menu ID values. You can also specify an array of Intents you'd like to use to create additional menu items.

Listing 5-11 gives an idea of how to dynamically populate an Activity menu that would include the "moon-base nuker" action from Listing 5-10.

Example 5-11. Dynamic Menu population from advertised actions

@Override
public boolean onCreateOptionsMenu(Menu menu) {
  super.onCreateOptionsMenu(menu);

  // Create the intent used to resolve which actions
  // should appear in the menu.
  Intent intent = new Intent();
  intent.setData(MoonBaseProvider.CONTENT_URI);
  intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE);
// Normal menu options to let you set a group and ID
  // values for the menu items you're adding.
  int menuGroup = 0;
  int menuItemId = 0;
  int menuItemOrder = Menu.NONE;

  // Provide the name of the component that's calling
  // the action -- generally the current Activity.
  ComponentName caller = getComponentName();

  // Define intents that should be added first.
  Intent[] specificIntents = null;
  // The menu items created from the previous Intents
  // will populate this array.
  MenuItem[] outSpecificItems = null;

  // Set any optional flags.
  int flags = Menu.FLAG_APPEND_TO_GROUP;

  // Populate the menu
  menu.addIntentOptions(menuGroup,
                        menuItemId,
                        menuItemOrder,
                        caller,
                        specificIntents,
                        intent,
                        flags,
                        outSpecificItems);

  return true;
}

Introducing Linkify

Linkify is a helper class that automagically creates hyperlinks within Text View (and Text View-derived) classes through RegEx pattern matching.

Text that matches a specified RegEx pattern will be converted into a clickable hyperlink that implicitly fires startActivity(new Intent(Intent.ACTION_VIEW, uri)), using the matched text as the target URI.

You can specify any string pattern you want to turn into links; for convenience, the Linkify class provides presets for common content types (like phone numbers and e-mail/web addresses), as described in the following section.

The Native Linkify Link Types

The static Linkify.addLinks method accepts the View to linkify, and a bitmask of one or more of the default content types supported and supplied by the Linkify class: WEB_URLS, EMAIL_ADDRESSES, PHONE_NUMBERS, and ALL.

Listing 5-12 shows how to linkify a Text View to display web and e-mail addresses as hyperlinks. When clicked, they will open the browser and an e-mail application respectively.

Example 5-12. Using Linkify in code

TextView textView = (TextView)findViewById(R.id.myTextView);
Linkify.addLinks(textView, Linkify.WEB_URLS|Linkify.EMAIL_ADDRESSES);

Note

Most Android devices have at least two e-mail applications: Gmail and Email. In situations in which multiple Activities are resolved as possible action consumers the user is asked to select his or her preference.

You can also linkify Views from within a layout resource using the android:autoLink attribute. It supports one or more (separated by |) of the following self-describing values: none, web, email, phone, and all.

Listing 5-13 shows how to add hyperlinks for phone numbers and e-mail addresses:

Example 5-13. Using Linkify in XML

<TextView
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:text="@string/linkify_me"
  android:autoLink="phone|email"
/>

Creating Custom Link Strings

To define your own linkify strings you create a new RegEx pattern to match the text you want to display as hyperlinks.

As with the native types, you linkify the target View by calling Linkify.addLinks, but this time pass in the new RegEx pattern. You can also pass in a prefix that will be prepended to the target URI when a link is clicked.

Listing 5-14 shows a View being linkified to support earthquake data provided by an Android Content Provider (that you will create in Chapter 7). Rather than include the entire schema, the linkify pattern matches any text that starts with "quake" and is followed by a number. The content schema is then prepended to the URI before the Intent is fired.

Example 5-14. Creating custom link strings in Linkify

int flags = Pattern.CASE_INSENSITIVE;
Pattern p = Pattern.compile("\bquake[0-9]*\b", flags);
Linkify.addLinks(myTextView, p,
                 "content://com.paad.earthquake/earthquakes/");

Linkify also supports TransformFilter and MatchFilter interfaces. These offer additional control over the target URI structure and the definition of matching strings, and are used as in the following skeleton code:

Linkify.addLinks(myTextView, pattern, prefixWith,
                 new MyMatchFilter(), new MyTransformFilter());

Using the Match Filter

Implement the acceptMatch method in your Match Filter to add additional conditions to RegEx pattern matches. When a potential match is found acceptMatch is triggered, with the match start and end index (along with the full text being searched) passed in as parameters.

Listing 5-15 shows a MatchFilter implementation that cancels any match immediately preceded by an exclamation mark.

Example 5-15. Using a Linkify Match Filter

class MyMatchFilter implements MatchFilter {
  public boolean acceptMatch(CharSequence s, int start, int end) {
    return (start == 0 || s.charAt(start-1) != '!'),
  }
}

Using the Transform Filter

The Transform Filter gives you more freedom to format your text strings by letting you modify the implicit URI generated by the link text. Decoupling the link text from the target URI gives you more freedom in how you display data strings to your users.

To use the Transform Filter, implement the transformUrl method in your Transform Filter. When linkify finds a successful match it calls transformUrl, passing in the RegEx pattern used and the default URI string it creates. You can modify the matched string and return the URI as a target suitable to be "viewed" by another Android application.

The TransformFilter implementation shown in Listing 5-16 transforms the matched text into a lowercase URI.

Example 5-16. Using a Linkify Transform Filter

class MyTransformFilter implements TransformFilter {
  public String transformUrl(Matcher match, String url) {
    return url.toLowerCase();
  }
}

Using Intents to Broadcast Events

As a system-level message-passing mechanism, Intents are capable of sending structured messages across process boundaries.

So far you've looked at using Intents to start new application components, but they can also be used to broadcast messages anonymously between components via the sendBroadcast method. You can implement Broadcast Receivers to listen for, and respond to, these broadcast Intents within your applications.

Broadcast Intents are used to notify listeners of system or application events, extending the event-driven programming model between applications.

Broadcasting Intents helps make your application more open; by broadcasting an event using an Intent you let yourself and third-party developers react to events without having to modify your original application. Within your applications you can listen for broadcast Intents to replace or enhance native (or third-party) applications, or react to system changes and application events.

Android uses broadcast Intents extensively to broadcast system events like battery-charging levels, network connections, and incoming calls.

Broadcasting Events with Intents

Broadcasting Intents is simple. Within your application, construct the Intent you want to broadcast and use the sendBroadcast method to send it.

Set the action, data, and category of your Intent in a way that lets Broadcast Receivers accurately determine their interest. In this scenario the Intent action string is used to identify the event being broadcast, so it should be a unique string that identifies the event. By convention, action strings are constructed with the same form as Java package names:

public static final String NEW_LIFEFORM_DETECTED =
  "com.paad.action.NEW_LIFEFORM";

If you wish to include data within the Intent you can specify a URI using the Intent's data property. You can also include extras to add additional primitive values. Considered in terms of an event-driven paradigm, the extras equate to optional parameters passed into an event handler.

Listing 5-17 shows the basic creation of a broadcast Intent using the action defined previously, with additional event information stored as extras.

Example 5-17. Broadcasting an Intent

Intent intent = new Intent(NEW_LIFEFORM_DETECTED);
intent.putExtra("lifeformName", lifeformType);
intent.putExtra("longitude", currentLongitude);
intent.putExtra("latitude", currentLatitude);
sendBroadcast(intent);

Listening for Broadcasts with Broadcast Receivers

Broadcast Receivers are used to listen for broadcast Intents. For a Broadcast Receiver to be enabled it needs to be registered, either in code or within the application manifest. When registering a Broadcast Receiver you must use an Intent Filter to specify which Intents it is listening for.

To create a new Broadcast Receiver, extend the BroadcastReceiver class and override the onReceive event handler as shown in Listing 5-18.

Example 5-18. Broadcast Receiver skeleton implementation

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class MyBroadcastReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    //TODO: React to the Intent received.
  }
}

The onReceive method will be executed when a broadcast Intent is received that matches the Intent Filter used to register the Receiver. The onReceive handler must complete within five seconds or the Force Close dialog will be displayed.

Applications with Broadcast Receivers registered in the manifest don't have to be running when the Intent is broadcast for the receivers to execute. They will be started automatically when a matching Intent is broadcast. This is excellent for resource management as it lets you create event-driven applications that will still respond to broadcast events even after they've been closed or killed.

Typically Broadcast Receivers will update content, launch Services, update Activity UI, or notify the user using the Notification Manager. The five-second execution limit ensures that major processing cannot, and should not, be done within the Broadcast Receiver itself.

Listing 5-19 shows how to implement a Broadcast Receiver. In the following sections you will learn how to register it in code or in your application manifest.

Example 5-19. Implementing a Broadcast Receiver

public class LifeformDetectedBroadcastReceiver extends BroadcastReceiver {

  public static final String BURN = "com.paad.alien.action.BURN_IT_WITH_FIRE";

  @Override
  public void onReceive(Context context, Intent intent) {
    // Get the lifeform details from the intent.
    Uri data = intent.getData();
    String type = intent.getStringExtra("type");
    double lat = intent.getDoubleExtra("latitude", 0);
    double lng = intent.getDoubleExtra("longitude", 0);
    Location loc = new Location("gps");
    loc.setLatitude(lat);
    loc.setLongitude(lng);
if (type.equals("alien")) {
      Intent startIntent = new Intent(BURN, data);
      startIntent.putExtra("latitude", lat);
      startIntent.putExtra("longitude", lng);

      context.startActivity(startIntent);
    }
  }
}

Registering Broadcast Receivers in Your Application Manifest

To include a Broadcast Receiver in the application manifest, add a <receiver> tag within the application node, specifying the class name of the Broadcast Receiver to register. The receiver node needs to include an intent-filter tag that specifies the action string being listened for, as shown in Listing 5-20.

Example 5-20. Registering a Broadcast Reveiver in XML

<receiver android:name=".LifeformDetectedBroadcastReceiver">
  <intent-filter>
    <action android:name="com.paad.action.NEW_LIFEFORM"/>
  </intent-filter>
</receiver>

Broadcast Receivers registered this way are always active, and will receive broadcast Intents even when the application has been killed or hasn't been started.

Registering Broadcast Receivers in Code

You can also register Broadcast Receivers in code. A receiver registered programmatically will respond to broadcast Intents only when the application component it is registered within is running.

This is typically useful when the Receiver is being used to update UI elements in an Activity. In this case it's good practice to un-register the Broadcast Receiver when the Activity isn't visible (or active).

Listing 5-21 shows how to register a Broadcast Receiver in code using the IntentFilter class.

Example 5-21. Registering a Broadcast Receiver in code

// Create and register the broadcast receiver.
IntentFilter filter = new IntentFilter(NEW_LIFEFORM_DETECTED);
LifeformDetectedBroadcastReceiver r = new LifeformDetectedBroadcastReceiver();
registerReceiver(r, filter);

To un-register a Broadcast Receiver use the unregisterReceiver method on your application context, passing in a Broadcast Receiver instance as follows:

unregisterReceiver(receiver);

Further examples can also be found in Chapter 9, where you learn to create your own background Services and use Intents to broadcast events to your Activities.

Broadcasting Sticky and Ordered Intents

When broadcasting an Intent using sendBroadcast, your Intent will be received by all registered Broadcast Receivers, but you cannot control the order and they cannot propagate results.

In circumstances where the order in which the Broadcast Receivers receive the Intent is important, or where you require the Receivers to be able to affect the Intent being broadcast, you can use the sendOrderedBroadcast method.

sendOrderedBroadcast(intent, null);

Using this method, your Intent will be delivered to all registered Receivers in order of priority. You can optionally assign your own Broadcast Receiver, which will then receive the Intent after it has been handled (and potentially modified) by all the other registered Broadcast Receivers.

sendOrderedBroadcast(intent, null, myBroadcastReceiver, null,
                     Activity.RESULT_OK, null, null);

For efficiency reasons, some broadcasts are sticky. When you call registerReceiver specifying an Intent Filter that matches a sticky broadcast, the return value will be the sticky broadcast Intent. To broadcast a sticky Intent your application must have the BROADCAST_STICKY uses-permission.

sendStickyBroadcast(intent);

To remove a sticky intent call removeStickyBroadcast, passing in the sticky Intent to remove.

removeStickyBroadcast(intent);

Native Android Broadcast Actions

Android broadcasts Intents for many of the system Services. You can use these messages to add functionality to your own projects based on system events such as time-zone changes, data-connection status, incoming SMS messages, or phone calls.

The following list introduces some of the native actions exposed as constants in the Intent class; these actions are used primarily to track device status changes.

  • ACTION_BOOT_COMPLETED Fired once when the device has completed its startup sequence. An application requires the RECEIVE_BOOT_COMPLETED permission to receive this broadcast.

  • ACTION_CAMERA_BUTTON Fired when the camera button is clicked.

  • ACTION_DATE_CHANGED and ACTION_TIME_CHANGED These actions are broadcast if the date or time on the device is manually changed (as opposed to changing through the inexorable progression of time).

  • ACTION_MEDIA_BUTTON Fired when the media button is clicked.

  • ACTION_MEDIA_EJECT If the user chooses to eject the external storage media, this event is fired first. If your application is reading or writing to the external media storage you should listen for this event in order to save and close any open file handles.

  • ACTION_MEDIA_MOUNTED and ACTION_MEDIA_UNMOUNTED These two events are broadcast whenever new external storage media are successfully added to or removed from the device.

  • ACTION_NEW_OUTGOING_CALL Broadcast when a new outgoing call is about to be placed. Listen for this broadcast to intercept outgoing calls. The number being dialed is stored in the EXTRA_PHONE_NUMBER extra, while the resultData in the returned Intent will be the number actually dialed. To register a Broadcast Receiver for this action your application must declare the PROCESS_OUTGOING_CALLS uses-permission.

  • ACTION_SCREEN_OFF and ACTION_SCREEN_ON Broadcast when the screen turns off or on respectively.

  • ACTION_TIMEZONE_CHANGED This action is broadcast whenever the phone's current time zone changes. The Intent includes a time-zone extra that returns the ID of the new java.util.TimeZone.

A comprehensive list of the broadcast actions used and transmitted natively by Android to notify applications of system state changes is available at http://developer.android.com/reference/android/content/Intent.html

Android also uses broadcast Intents to announce application-specific events like incoming SMS messages. The actions and Intents associated with these events will be discussed in more detail in later chapters when you learn more about the associated Services.

INTRODUCING PENDING INTENTS

The PendingIntent class provides a mechanism for creating Intents that can be fired by another application at a later time.

A Pending Intent is commonly used to package an Intent that will be fired in response to a future event, such as a widget View being clicked or a Notification being selected from the notification panel.

Note

When used, Pending Intents execute the packaged Intent with the same permissions and identity as if you had executed them yourself, within your own application.

As shown in Listing 5-22, the PendingIntent class offers static methods to construct Pending Intents used to start an Activity, start a Service, or broadcast an Intent.

Example 5-22. Creating new Pending Intents

// Start an Activity
Intent startActivityIntent = new Intent(this, MyOtherActivity.class);
PendingIntent.getActivity(this, 0, startActivityIntent, 0);

// Broadcast an Intent
Intent broadcastIntent = new Intent(NEW_LIFEFORM_DETECTED);
PendingIntent.getBroadcast(this, 0, broadcastIntent, 0);

You'll learn more about using Pending Intents in later chapters when they're used to support other Services such as widgets and Notifications.

INTRODUCING ADAPTERS

Adapters are bridging classes that bind data to Views (such as List Views) used in the user interface. The adapter is responsible for creating the child Views used to represent each item within the parent View, and providing access to the underlying data.

Views that support Adapter binding must extend the AdapterView abstract class. It's possible to create your own AdapterView-derived controls and to create new Adapter classes to bind them.

Introducing Some Native Adapters

In many cases you won't have to create your own Adapter from scratch. Android supplies a set of Adapters that pump data into native UI controls.

Because Adapters are responsible both for supplying the data and for creating the Views that represent each item, Adapters can radically modify the appearance and functionality of the controls they're bound to.

The following list highlights two of the most useful and versatile native Adapters:

  • ArrayAdapter The Array Adapter uses generics to bind an Adapter View to an array of objects of the specified class. By default the Array Adapter uses the toString value of each object in the array to create and populate Text Views. Alternative constructors enable you to use more complex layouts, or you can extend the class to use alternatives to Text Views as shown in the next section.

  • SimpleCursorAdapter The Simple Cursor Adapter attaches Views specified within a layout to the columns of Cursors returned from Content Provider queries. You specify an XML layout definition, and then bind each column to a View within that layout. The adapter will create a new View for each Cursor entry and inflate the layout into it, populating each View within the layout using the Cursor column values.

The following sections will delve into these Adapter classes in more detail. The examples provided bind data to List Views, though the same logic will work just as well for other Adapter View classes such as Spinners and Galleries.

Customizing the Array Adapter

By default the Array Adapter will use the toString value of the object array it is binding to populate the Text View available within the specified layout.

In most cases you will need to customize the layout used to represent each View. To do that, you will need to extend ArrayAdapter with a type-specific variation, overriding the getView method to assign object properties to layout Views as shown in Listing 5-23.

The getView method is used to construct, inflate, and populate the View that will be displayed within the parent Adapter View class (e.g., List View) which is being bound to the underlying array using this Adapter.

The getView method receives parameters that describe the position of the item to be displayed, the View being updated (or null), and the View Group into which this new View will be placed. A call to getItem will return the value stored at the specified index in the underlying array.

Return the new populated View instance as a result from this method.

Example 5-23. Customizing the Array Adapter

public class MyArrayAdapter extends ArrayAdapter<MyClass> {

  int resource;

  public MyArrayAdapter(Context context,
                         int _resource,
                         List<MyClass> items) {
    super(context, _resource, items);
    resource = _resource;
  }

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    LinearLayout newView;

    MyClass classInstance = getItem(position);

    // TODO Retrieve values to display from the
    // classInstance variable.

    // Inflate a new view if this is not an update.
    if (convertView == null) {
      newView = new LinearLayout(getContext());
      String inflater = Context.LAYOUT_INFLATER_SERVICE;
      LayoutInflater vi = (LayoutInflater)getContext().getSystemService(inflater);
      vi.inflate(resource, newView, true);
    } else {
      newView = (LinearLayout)convertView;
    }

    // TODO Retrieve the Views to populate
    // TODO Populate the Views with object property values.

    return newView;
  }
}

Using Adapters for Data Binding

To apply an Adapter to an AdapterView-derived class you call the View's setAdapter method, passing in an Adapter instance as shown in Listing 5-24.

Example 5-24. Creating and applying an Adapter

ArrayList<String> myStringArray = new ArrayList<String>();
ArrayAdapter<String> myAdapterInstance;

int layoutID = android.R.layout.simple_list_item_1;
myAdapterInstance = new ArrayAdapter<String>(this, layoutID , myStringArray);

myListView.setAdapter(myAdapterInstance);

This snippet shows the most simplistic case, in which the array being bound contains Strings and each List View item is represented by a single Text View.

The first of the following examples demonstrates how to bind an array of complex objects to a List View using a custom layout. The second shows how to use a Simple Cursor Adapter to bind a query result to a custom layout within a List View.

Customizing the To-Do List Array Adapter

This example extends the To-Do List project, storing each item as a ToDoItem object that includes the date each item was created.

You will extend ArrayAdapter to bind a collection of ToDoItem objects to the ListView and customize the layout used to display each List View item.

  1. Return to the To-Do List project. Create a new ToDoItem class that stores the task and its creation date. Override the toString method to return a summary of the item data.

    package com.paad.todolist;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    public class ToDoItem {
    
      String task;
      Date created;
    
      public String getTask() {
        return task;
      }
    
      public Date getCreated() {
        return created;
      }
    
      public ToDoItem(String _task) {
        this(_task, new Date(java.lang.System.currentTimeMillis()));
      }
    
    
      public ToDoItem(String _task, Date _created) {
        task = _task;
    created = _created;
      }
    
      @Override
      public String toString() {
        SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yy");
        String dateString = sdf.format(created);
        return "(" + dateString + ") " + task;
      }
    }
  2. Open the ToDoList Activity and modify the ArrayList and ArrayAdapter variable types to store ToDoItem objects rather than Strings. You'll then need to modify the onCreate method to update the corresponding variable initialization. You'll also need to update the onKeyListener handler to support the ToDoItem objects.

    private ArrayList<ToDoItem> todoItems;
    private ListView myListView;
    private EditText myEditText;
    private ArrayAdapter<ToDoItem> aa;
    
    @Override
    public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
    
      // Inflate your view
      setContentView(R.layout.main);
    
      // Get references to UI widgets
      myListView = (ListView)findViewById(R.id.myListView);
      myEditText = (EditText)findViewById(R.id.myEditText);
    
      todoItems = new ArrayList<ToDoItem>();
      int resID = R.layout.todolist_item;
      aa = new ArrayAdapter<ToDoItem>(this, resID, todoItems);
      myListView.setAdapter(aa);
    
      myEditText.setOnKeyListener(new OnKeyListener() {
        public boolean onKey(View v, int keyCode, KeyEvent event) {
          if (event.getAction() == KeyEvent.ACTION_DOWN)
            if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
              ToDoItem newItem = new ToDoItem(myEditText.getText().toString());
              todoItems.add(0, newItem);
              myEditText.setText("");
              aa.notifyDataSetChanged();
              cancelAdd();
              return true;
            }
          return false;
        }
      });
    
      registerForContextMenu(myListView);
    }
  3. If you run the Activity it will now display each to-do item as shown in Figure 5-3.

    FIGURE 5-3

    Figure 5-3. FIGURE 5-3

  4. Now you can create a custom layout to display each to-do item.

    Start by modifying the custom layout you created in Chapter 4 to include a second TextView. It will be used to show the creation date of each to-do item.

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      android:background="@color/notepad_paper">
      <TextView
        android:id="@+id/rowDate"
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:padding="10dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:textColor="@color/notepad_text"
        android:layout_alignParentRight="true"
      />
      <TextView
        android:id="@+id/row"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:padding="10dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:textColor="@color/notepad_text"
        android:layout_alignParentLeft="@+id/rowDate"
      />
    </RelativeLayout>
  5. Create a new class (ToDoItemAdapter) that extends an ArrayAdapter with a ToDoItem-specific variation. Override getView to assign the task and date properties in the ToDoItem object to the Views in the layout you created in Step 4:

    import java.text.SimpleDateFormat;
    import android.content.Context;
    import java.util.*;
    import android.view.*;
    import android.widget.*;
    
    public class ToDoItemAdapter extends ArrayAdapter<ToDoItem> {
    
      int resource;
    
      public ToDoItemAdapter(Context _context,
                                 int _resource,
                                 List<ToDoItem> _items) {
        super(_context, _resource, _items);
        resource = _resource;
      }
    
      @Override
      public View getView(int position, View convertView, ViewGroup parent) {
        LinearLayout todoView;
    
        ToDoItem item = getItem(position);
    
        String taskString = item.getTask();
        Date createdDate = item.getCreated();
        SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yy");
        String dateString = sdf.format(createdDate);
    
        if (convertView == null) {
          todoView = new LinearLayout(getContext());
          String inflater = Context.LAYOUT_INFLATER_SERVICE;
          LayoutInflater vi = (LayoutInflater)getContext().getSystemService(inflater);
          vi.inflate(resource, todoView, true);
        } else {
          todoView = (LinearLayout) convertView;
        }
    
        TextView dateView = (TextView)todoView.findViewById(R.id.rowDate);
        TextView taskView = (TextView)todoView.findViewById(R.id.row);
    
        dateView.setText(dateString);
        taskView.setText(taskString);
    
        return todoView;
      }
    }
  6. Finally, replace the ArrayAdapter declaration with a ToDoItemAdapter:

    private ToDoItemAdapter aa;

    Within onCreate, replace the ArrayAdapter<String> instantiation with the new ToDoItemAdapter:

    aa = new ToDoItemAdapter(this, resID, todoItems);
  7. If you run your Activity it should appear as shown in the screenshot in Figure 5-4.

    FIGURE 5-3
FIGURE 5-4

Figure 5-4. FIGURE 5-4

Using the Simple Cursor Adapter

The SimpleCursorAdapter lets you bind a Cursor to a List View, using a custom layout definition to define the layout of each row/item, which is populated by a row's column values.

Construct a Simple Cursor Adapter by passing in the current context, a layout resource, a Cursor, and two arrays: one that contains the names of the columns to be used, and a second (equally-sized) array that has resource IDs for the Views to use to display the contents of the corresponding columns.

Listing 5-25 shows how to construct a Simple Cursor Adapter to display contact information.

Example 5-25. Creating a Simple Cursor Adapter

String uriString = "content://contacts/people/";
Cursor myCursor = managedQuery(Uri.parse(uriString), null, null, null);

String[] fromColumns = new String[] {People.NUMBER, People.NAME};

int[] toLayoutIDs = new int[] { R.id.nameTextView, R.id.numberTextView};

SimpleCursorAdapter myAdapter;
myAdapter = new SimpleCursorAdapter(this,
                                    R.layout.simplecursorlayout,
myCursor,
                                    fromColumns,
                                    toLayoutIDs);

myListView.setAdapter(myAdapter);

The Simple Cursor Adapter was used earlier in this chapter in the Contact Picker example. You'll learn more about Content Providers and Cursors in Chapter 7, where you'll also find more Simple Cursor Adapter examples.

USING INTERNET RESOURCES

With Internet connectivity and a WebKit browser, you might well ask if there's any reason to create native Internet-based applications when you could make a web-based version instead.

There are a number of benefits to creating thick- and thin-client native applications rather than relying on entirely web-based solutions:

  • Bandwidth Static resources like images, layouts, and sounds can be expensive data consumers on devices with limited and often expensive bandwidth restraints. By creating a native application you can limit the bandwidth requirements to updated data only.

  • Caching Mobile Internet access has not yet reached the point of ubiquity. With a browser-based solution a patchy Internet connection can result in intermittent application availability. A native application can cache data to provide as much functionality as possible without a live connection.

  • Native features Android devices are more than a simple platform for running a browser: they include location-based services, Notifications, widgets, camera hardware, and accelerometers. By creating a native application you can combine the data available online with the hardware features available on the device to provide a richer user experience.

Modern mobile devices offer a number of alternatives for accessing the Internet. Looked at broadly, Android provides two connection techniques for Internet connectivity. Each is offered transparently to the application layer.

  • Mobile Internet GPRS, EDGE, and 3G Internet access is available through carriers that offer mobile data plans.

  • Wi-Fi Wi-Fi receivers and mobile hotspots are becoming increasingly common.

Connecting to an Internet Resource

While the details of working with specific web services won't be covered within this book, it's useful to know the general principles of connecting to the Internet, and getting an input stream from a remote data source.

Before you can access Internet resources, you need to add an INTERNET uses-permission node to your application manifest, as shown in the following XML snippet:

<uses-permission android:name="android.permission.INTERNET"/>

Listing 5-26 shows the basic pattern for opening an Internet data stream.

Example 5-26. Opening a data stream

String myFeed = getString(R.string.my_feed);
try {
  URL url = new URL(myFeed);

  URLConnection connection = url.openConnection();
  HttpURLConnection httpConnection = (HttpURLConnection)connection;

  int responseCode = httpConnection.getResponseCode();
  if (responseCode == HttpURLConnection.HTTP_OK) {
    InputStream in = httpConnection.getInputStream();
    [ ... Process the input stream as required ... ]
  }
}
catch (MalformedURLException e) { }
catch (IOException e) { }

Android includes several classes to help you handle network communications. They are available in the java.net.* and android.net.* packages.

Later in this chapter is a fully worked example that shows how to obtain and process an Internet feed to get a list of earthquakes felt in the last 24 hours.

Chapter 13 features more information on managing specific Internet connections, including information on monitoring connection status and configuring Wi-Fi access point connections.

Using Internet Resources

Android offers several ways to leverage Internet resources.

At one extreme you can use a WebView to include a WebKit-based browser View within an Activity. At the other extreme you can use client-side APIs such as Google's GData APIs to interact directly with server processes. Somewhere in between, you can process remote XML feeds to extract and process data using a Java-based XML parser such as SAX or the more efficient XmlPullParser.

Detailed instructions for parsing XML and interacting with specific web services are outside the scope of this book. That said, the Earthquake example, included later in this chapter, gives a fully worked example of parsing an XML feed using the SAX parser.

If you're using Internet resources in your application, remember that your users' data connections are dependent on the communications technology available to them. EDGE and GSM connections are notoriously low-bandwidth, while a Wi-Fi connection may be unreliable in a mobile setting.

Optimize the user experience by limiting the quantity of data being transmitted, and ensure that your application is robust enough to handle network outages and bandwidth limitations.

INTRODUCING DIALOGS

Dialog boxes are a common UI metaphor in desktop, web, and mobile applications. They're used to help users answer questions, make selections, and confirm actions, and to display warning or error messages. Dialog boxes in Android are partially transparent, floating Activities that partially obscure the Activities that launched them.

As in Figure 5-5, they generally obscure the Activities behind them using a blur or dim filter.

FIGURE 5-5

Figure 5-5. FIGURE 5-5

There are three ways to implement a dialog in Android:

  • Using the Dialog class (or its extensions) As well as the general-purpose AlertDialog class, Android includes a number of specialist classes that extend Dialog. Each is designed to provide specific dialog-box functionality. A Dialog-class-based screen is constructed entirely within its calling Activity, so it doesn't need to be registered in the manifest as its life cycle is controlled entirely by the calling Activity.

  • Dialog-themed Activities You can apply the dialog theme to a regular Activity to give it the appearance of a standard dialog box.

  • Toasts Toasts are special non-modal transient message boxes, often used by Broadcast Receivers and Services to notify users of events occurring in the background. You can learn more about Toasts in Chapter 9.

Introducing the Dialog Classes

To use the base Dialog class you create a new instance and set the title and layout, using the setTitle and setContentView methods as shown in Listing 5-27.

Example 5-27. Creating a new dialog using the Dialog class

Dialog d = new Dialog(MyActivity.this);

// Have the new window tint and blur the window it
// obscures.
Window window = d.getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND,
WindowManager.LayoutParams.FLAG_BLUR_BEHIND);

// Set the title
d.setTitle("Dialog Title");
// Inflate the layout
d.setContentView(R.layout.dialog_view);

// Find the TextView used in the layout
// and set its text value
TextView text = (TextView)d.findViewById(R.id.dialogTextView);
text.setText("This is the text in my dialog");

Once it's configured to your liking, use the show method to display it.

d.show();

The Alert Dialog Class

The AlertDialog class is one of the most versatile Dialog-class implementations. It offers a number of options that let you construct screens for some of the most common dialog-box use cases, including:

  • Presenting a message to the user offering them one to three options in the form of buttons. This functionality is probably familiar to you if you've done any desktop programming for which the buttons presented are usually a combination of OK, Cancel, Yes, and No.

  • Offering a list of options in the form of checkboxes or radio buttons.

  • Providing a text entry box for user input.

To construct the Alert Dialog us er interface, create a new AlertDialog.Builder object as follows:

AlertDialog.Builder ad = new AlertDialog.Builder(context);

You can then assign values for the title and message to display, and optionally assign values to be used for any buttons, selection items, and text input boxes you wish to display. That includes setting event listeners to handle user interaction.

Listing 5-28 gives an example of a new Alert Dialog used to display a message and offer two button options to continue. Clicking either button will close the Dialog after executing the attached Click Listener.

Example 5-28. Configuring an Alert Dialog

Context context = MyActivity.this;
String title = "It is Pitch Black";
String message = "You are likely to be eaten by a grue.";
String button1String = "Go Back";
String button2String = "Move Forward";

AlertDialog.Builder ad = new AlertDialog.Builder(context);
ad.setTitle(title);
ad.setMessage(message);
ad.setPositiveButton(button1String,
new OnClickListener() {
                       public void onClick(DialogInterface dialog, int arg1) {
                         eatenByGrue();
                       }
                     });
ad.setNegativeButton(button2String,
                     new OnClickListener(){
                       public void onClick(DialogInterface dialog, int arg1) {
                         // do nothing
                       }
                     });
ad.setCancelable(true);
ad.setOnCancelListener(new OnCancelListener() {
                             public void onCancel(DialogInterface dialog) {
                               eatenByGrue();
                             }
                       });

To display an Alert Dialog that you've created call show:

ad.show();

A better alternative is using your Activity's onCreateDialog and onPrepareDialog handlers to create dialog instances that can persist state. This technique is examined later in this chapter.

Specialist Input Dialogs

One of the major uses of dialog boxes is to provide an interface for user input. Android includes several specialist dialog boxes that encapsulate controls designed to facilitate common user-input requests. They include the following:

  • CharacterPickerDialog Lets users select an accented character based on a regular character source.

  • DatePickerDialog Lets users select a date from a DatePicker View. The constructor includes a callback listener to alert your calling Activity when the date has been set.

  • TimePickerDialog Similar to the Date Picker Dialog, this dialog lets users select a time from a TimePicker View.

  • ProgressDialog A dialog that displays a progress bar beneath a message text box. Perfect for keeping the user informed of ongoing progress of a time-consuming operation.

Using Activities as Dialogs

Dialogs offer a simple and lightweight technique for displaying screens, but there will still be times when you need more control over the content and life cycle of your dialog box.

The solution is to implement it as a full Activity. By creating an Activity you lose the lightweight nature of the Dialog class, but you gain the ability to implement any screen you want and full access to the Activity life-cycle event handlers.

The easiest way to make an Activity look like a dialog is to apply the android:style/Theme.Dialog theme when you add it to your manifest, as shown in the following XML snippet:

<activity android:name="MyDialogActivity"
          android:theme="@android:style/Theme.Dialog">
</activity>

This will cause your Activity to behave as a Dialog, floating on top of, and partially obscuring, the Activity beneath it.

Managing and Displaying Dialogs

Rather than creating new instances of a dialog each time it's required, Android provides the onCreateDialog and onPrepareDialog event handlers within the Activity class to persist and manage dialog-box instances.

By overriding the onCreateDialog handler you can specify dialogs that will be created on demand when showDialog is used to display a specific dialog. As shown in Listing 5-29, the overridden method includes a switch statement that lets you determine which dialog is required.

Example 5-29. Using the On Create Dialog event handler

static final private int TIME_DIALOG = 1;

@Override
public Dialog onCreateDialog(int id) {
  switch(id) {
    case (TIME_DIALOG) :
      AlertDialog.Builder timeDialog = new AlertDialog.Builder(this);
      timeDialog.setTitle("The Current Time Is...");
      timeDialog.setMessage("Now");
      return timeDialog.create();
  }
  return null;
}

After the initial creation, each time showDialog is called it will trigger the onPrepareDialog handler. By overriding this method you can modify a dialog each time it is displayed. This lets you contextualize any of the display values, as shown in Listing 5-30 that assigns the current time to the dialog created in Listing 5-29.

Example 5-30. Using the On Prepare Dialog event handler

@Override
public void onPrepareDialog(int id, Dialog dialog) {
  switch(id) {
    case (TIME_DIALOG) :
      SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
Date currentTime = new Date(java.lang.System.currentTimeMillis());
      String dateString = sdf.format(currentTime);
      AlertDialog timeDialog = (AlertDialog)dialog;
      timeDialog.setMessage(dateString);

      break;
  }
}

Once you've overridden these methods you can display the dialogs by calling showDialog as shown below. Pass in the identifier for the dialog you wish to display, and Android will create (if necessary) and prepare the dialog before displaying it.

showDialog(TIME_DIALOG);

As well as providing improved resource use, this technique lets your Activity handle the persistence of state information within Dialogs. Any selection or data input (such as item selection and text entry) will be persisted between displays of each Dialog instance.

CREATING AN EARTHQUAKE VIEWER

In the following example you'll create a tool that uses a USGS earthquake feed to display a list of recent earthquakes.

Note

You will return to this earthquake application several times, first in Chapters 6 and 7 to save preferences and share the earthquake data with a Content Provider, and again in Chapters 8 and 9 to add mapping support and to move the earthquake updates into a Service.

In this example you'll create a list-based Activity that connects to an earthquake feed and displays the location, magnitude, and time of the earthquakes it contains. You'll use an Alert Dialog to provide a details window that includes a linkified Text View with a link to the USGS web site.

  1. Start by creating an Earthquake project featuring an Earthquake Activity. Modify the main.xml layout resource to include a List View control — be sure to name it so you can reference it from the Activity code.

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent">
      <ListView
        android:id="@+id/earthquakeListView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
      />
    </LinearLayout>
  2. Create a new public Quake class. This class will be used to store the details (date, details, location, magnitude, and link) of each earthquake. Override the toString method to provide the string that will be used to represent each quake in the List View.

    package com.paad.earthquake;
    
    import java.util.Date;
    import java.text.SimpleDateFormat;
    import android.location.Location;
    
    public class Quake {
      private Date date;
      private String details;
      private Location location;
      private double magnitude;
      private String link;
    
      public Date getDate() { return date; }
      public String getDetails() { return details; }
      public Location getLocation() { return location; }
      public double getMagnitude() { return magnitude; }
      public String getLink() { return link; }
    
      public Quake(Date _d, String _det, Location _loc, double _mag, String _link) {
        date = _d;
        details = _det;
        location = _loc;
        magnitude = _mag;
        link = _link;
      }
    
      @Override
      public String toString() {
        SimpleDateFormat sdf = new SimpleDateFormat("HH.mm");
        String dateString = sdf.format(date);
        return dateString + ": " + magnitude + " " + details;
      }
    
    }
  3. In the Earthquake Activity, override the onCreate method to store an ArrayList of Quake objects and bind that to the ListView using an ArrayAdapter:

    package com.paad.earthquake;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.HttpURLConnection;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.net.URLConnection;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.GregorianCalendar;
    import javax.xml.parsers.DocumentBuilder;
    import javax.xml.parsers.DocumentBuilderFactory;
    import javax.xml.parsers.ParserConfigurationException;
    import org.w3c.dom.Document;
    import org.w3c.dom.Element;
    import org.w3c.dom.NodeList;
    import org.xml.sax.SAXException;
    import android.app.Activity;
    import android.app.Dialog;
    import android.location.Location;
    import android.os.Bundle;
    import android.view.Menu;
    import android.view.View;
    import android.view.WindowManager;
    import android.view.MenuItem;
    import android.widget.AdapterView;
    import android.widget.ArrayAdapter;
    import android.widget.ListView;
    import android.widget.TextView;
    import android.widget.AdapterView.OnItemClickListener;
    
    public class Earthquake extends Activity {
    
      ListView earthquakeListView;
      ArrayAdapter<Quake> aa;
    
      ArrayList<Quake> earthquakes = new ArrayList<Quake>();
    
      @Override
      public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.main);
    
        earthquakeListView = (ListView)this.findViewById(R.id.earthquakeListView);
    
        int layoutID = android.R.layout.simple_list_item_1;
        aa = new ArrayAdapter<Quake>(this, layoutID , earthquakes);
        earthquakeListView.setAdapter(aa);
      }
    }
  4. Next, start processing the earthquake feed. For this example the feed used is the one-day USGS feed for earthquakes with a magnitude greater than 2.5.

    Note

    Add the location of your feed as an external string resource. This lets you potentially specify a different feed based on a user's location.

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
      <string name="app_name">Earthquake</string>
      <string name="quake_feed">
        http://earthquake.usgs.gov/eqcenter/catalogs/1day-M2.5.xml
      </string>
    </resources>
  5. Before your application can access the Internet it needs to be granted permission for Internet access. Add the uses-permission to the manifest:

    <uses-permission android:name="android.permission.INTERNET"/>
  6. Returning to the Earthquake Activity, create a new refreshEarthquakes method that connects to and parses the earthquake feed. Extract each earthquake and parse the details to obtain the date, magnitude, link, and location. As you finish parsing each earthquake, pass it in to a new addNewQuake method.

    Note

    The earthquake feed XML is parsed here by the SAX parser. Several alternatives exist, including the XmlPullParser. An analysis of the alternative XML parsing techniques (and how to use them) is beyond the scope of this book, but it's important to evaluate and compare the options available within your own applications.

    private void refreshEarthquakes() {
      // Get the XML
      URL url;
      try {
        String quakeFeed = getString(R.string.quake_feed);
        url = new URL(quakeFeed);
    
        URLConnection connection;
        connection = url.openConnection();
    
        HttpURLConnection httpConnection = (HttpURLConnection)connection;
        int responseCode = httpConnection.getResponseCode();
    
        if (responseCode == HttpURLConnection.HTTP_OK) {
          InputStream in = httpConnection.getInputStream();
    
          DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
          DocumentBuilder db = dbf.newDocumentBuilder();
    
          // Parse the earthquake feed.
          Document dom = db.parse(in);
          Element docEle = dom.getDocumentElement();
    
          // Clear the old earthquakes
          earthquakes.clear();
    
          // Get a list of each earthquake entry.
          NodeList nl = docEle.getElementsByTagName("entry");
          if (nl != null && nl.getLength() > 0) {
            for (int i = 0 ; i < nl.getLength(); i++) {
              Element entry = (Element)nl.item(i);
              Element title = (Element)entry.getElementsByTagName("title").item(0);
              Element g = (Element)entry.getElementsByTagName("georss:point").item(0);
              Element when = (Element)entry.getElementsByTagName("updated").item(0);
              Element link = (Element)entry.getElementsByTagName("link").item(0);
    String details = title.getFirstChild().getNodeValue();
              String hostname = "http://earthquake.usgs.gov";
              String linkString = hostname + link.getAttribute("href");
    
              String point = g.getFirstChild().getNodeValue();
              String dt = when.getFirstChild().getNodeValue();
              SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'");
              Date qdate = new GregorianCalendar(0,0,0).getTime();
              try {
                qdate = sdf.parse(dt);
              } catch (ParseException e) {
                e.printStackTrace();
              }
    
              String[] location = point.split(" ");
              Location l = new Location("dummyGPS");
              l.setLatitude(Double.parseDouble(location[0]));
              l.setLongitude(Double.parseDouble(location[1]));
    
              String magnitudeString = details.split(" ")[1];
              int end =  magnitudeString.length()-1;
              double magnitude = Double.parseDouble(magnitudeString.substring(0, end));
    
              details = details.split(",")[1].trim();
    
              Quake quake = new Quake(qdate, details, l, magnitude, linkString);
    
    
              // Process a newly found earthquake
              addNewQuake(quake);
            }
          }
        }
      } catch (MalformedURLException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (ParserConfigurationException e) {
        e.printStackTrace();
      } catch (SAXException e) {
        e.printStackTrace();
      }
      finally {
      }
    }
    
    private void addNewQuake(Quake _quake) {
      // TODO: Add the earthquakes to the array list.
    }
  7. Update the addNewQuake method so that it takes each newly processed quake and adds it to the earthquake Array List. It should also notify the Array Adapter that the underlying data has changed.

    private void addNewQuake(Quake _quake) {
    // Add the new quake to our list of earthquakes.
      earthquakes.add(_quake);
    
      // Notify the array adapter of a change.
      aa.notifyDataSetChanged();
    }
  8. Modify your onCreate method to call refreshEarthquakes on startup:

    @Override
    public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      setContentView(R.layout.main);
    
      earthquakeListView = (ListView)this.findViewById(R.id.earthquakeListView);
    
      int layoutID = android.R.layout.simple_list_item_1;
      aa = new ArrayAdapter<Quake>(this, layoutID , earthquakes);
      earthquakeListView.setAdapter(aa);
    
      refreshEarthquakes();
    }

    Note

    The Internet lookup is currently happening on the main UI thread. This is bad form, as the application will become unresponsive if the lookup takes longer than a few seconds. In Chapter 9 you'll learn how to move expensive or time-consuming operations like this into a Service and onto a background thread.

  9. If you run your project, you should see a List View that features the earthquakes from the last 24 hours with a magnitude greater than 2.5, as shown in the screen shot in Figure 5-6.

    FIGURE 5-6

    Figure 5-6. FIGURE 5-6

  10. There are two more steps needed to make this a more useful application. First, create a new Menu Item to let users refresh the earthquake feed on demand.

    • 10.1. Start by adding a new external string for the menu option:

      <string name="menu_update">
       Refresh Earthquakes
      </string>
    • 10.2. Then override the Activity's onCreateOptionsMenu and onOptionsItemSelected methods to display and handle the Refresh Earthquakes Menu Item:

      static final private int MENU_UPDATE = Menu.FIRST;
      
      @Override
      public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
      
        menu.add(0, MENU_UPDATE, Menu.NONE, R.string.menu_update);
      
        return true;
      }
      
      
      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
        super.onOptionsItemSelected(item);
      
        switch (item.getItemId()) {
          case (MENU_UPDATE): {
            refreshEarthquakes();
            return true;
          }
        }
        return false;
      }
  11. Now add some interaction. Let users find more details by opening a dialog box when they select an earthquake from the list.

    • 11.1. Create a new quake_details.xml layout resource for the dialog box you'll display when an item is clicked:

      <?xml version="1.0" encoding="utf-8"?>
      
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:padding="10dp">
        <TextView
          android:id="@+id/quakeDetailsTextView"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:textSize="14sp"
        />
      </LinearLayout>
    • 11.2. Then modify your onCreate method to add an ItemClickListener to the List View that displays a dialog box whenever an earthquake item is clicked:

      static final private int QUAKE_DIALOG = 1;
      Quake selectedQuake;
      
      @Override
      public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.main);
      
        earthquakeListView = (ListView)this.findViewById(R.id.earthquakeListView);
      earthquakeListView.setOnItemClickListener(new OnItemClickListener() {
          @Override
          public void onItemClick(AdapterView _av, View _v, int _index,
             long arg3) {
            selectedQuake = earthquakes.get(_index);
            showDialog(QUAKE_DIALOG);
          }
        });
      
        int layoutID = android.R.layout.simple_list_item_1;
        aa = new ArrayAdapter<Quake>(this, layoutID , earthquakes);
        earthquakeListView.setAdapter(aa);
      
        refreshEarthquakes();
      }
    • 11.3. Now override the onCreateDialog and onPrepareDialog methods to create and populate the earthquake details dialog box:

      @Override
      public Dialog onCreateDialog(int id) {
        switch(id) {
          case (QUAKE_DIALOG) :
      
            LayoutInflater li = LayoutInflater.from(this);
            View quakeDetailsView = li.inflate(R.layout.quake_details, null);
      
            AlertDialog.Builder quakeDialog = new AlertDialog.Builder(this);
            quakeDialog.setTitle("Quake Time");
            quakeDialog.setView(quakeDetailsView);
            return quakeDialog.create();
        }
        return null;
      }
      
      @Override
      public void onPrepareDialog(int id, Dialog dialog) {
        switch(id) {
          case (QUAKE_DIALOG) :
            SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
            String dateString = sdf.format(selectedQuake.getDate());
            String quakeText = "Magnitude " + selectedQuake.getMagnitude() +
                               "
      " + selectedQuake.getDetails() + "
      " +
                               selectedQuake.getLink();
      
            AlertDialog quakeDialog = (AlertDialog)dialog;
            quakeDialog.setTitle(dateString);
            TextView tv = (TextView)quakeDialog.findViewById
               (R.id.quakeDetailsTextView);
            tv.setText(quakeText);
      
            break;
        }
      }
    • 11.4. The final step is to linkify the dialog to make the link to the USGS a hyperlink. Adjust the dialog box's XML layout resource definition to include an autolink attribute:

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:padding="10dp">
        <TextView
          android:id="@+id/quakeDetailsTextView"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:textSize="14sp"
          android:autoLink="all"
        />
      </LinearLayout>
      
      FIGURE 5-6

Launch your application again. When you click a particular earthquake a dialog will appear, partially obscuring the list, as shown in Figure 5-7.

FIGURE 5-7

Figure 5-7. FIGURE 5-7

SUMMARY

The focus of this chapter has been on binding your application components.

Intents provide a versatile messaging system that lets you pass intentions between your application and others, to perform actions and signal events. You learned how to use implicit and explicit Intents to start new Activities, and how to populate an Activity menu dynamically through runtime resolution of Activity Intent Filters.

You were introduced to broadcast Intents, and saw how they can be used to send messages throughout the device, particularly to support an event-driven model based on system- and application-specific events.

You learned how to use sub-Activities to pass data between Activities, and how to use Dialogs to display information and facilitate user input.

Adapters were introduced and used to bind underlying data to visual components. In particular you saw how to use an Array Adapter and Simple Cursor Adapter to bind a List View to Array Lists and Cursors.

Finally, you learned the basics behind connecting to the Internet and using remote feeds as data sources for your native client applications.

You also learned:

  • To use linkify to add implicit Intents to Text Views at run time.

  • Which native Android actions are available for you to extend, replace, or embrace.

  • How to use Intent Filters to let your own Activities become handlers for completing action requests from your own or other applications.

  • How to listen for broadcast Intents using Broadcast Receivers.

  • How to use an Activity as a dialog box.

In the next chapter you will learn how to persist information within your applications. Android provides a number of mechanisms for saving application data, including files, simple preferences, and fully featured relational databases (using the SQLite database library). Chapter 6 will focus on using Preferences and saving Activity state, while Chapter 7 will examine Content Providers and SQLite databases.

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

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