Chapter     7

Interacting with the System

The Android operating system provides a number of useful services that applications can leverage. Many of these services are designed to allow your application to function within the mobile system in ways beyond just interacting briefly with a user. Applications can schedule themselves for alarms, run background services, and send messages to each other—all of which allows an Android application to integrate to the fullest extent with the mobile device. In addition, Android provides a set of standard interfaces that are designed to expose all the data collected by its core applications to your software. Through these interfaces, any application may integrate with, add to, and improve upon the core functionality of the platform, thereby enhancing the experience for the user.

7-1. Notifying from the Background

Problem

Your application is running in the background, with no currently visible interface to the user, but must notify the user of an important event that has occurred.

Solution

(API Level 4)

Use NotificationManager to post a status bar notification. Notifications provide an unobtrusive way of indicating that you want the user’s attention. Perhaps new messages have arrived, an update is available, or a long-running job is complete; notifications are perfect for accomplishing these tasks.

How It Works

A notification can be posted to the NotificationManager from just about any system component, such as a Service, BroadcastReceiver, or Activity. In Listing 7-1, we will look at an activity that posts a series of different notification types when the user leaves the activity and goes to the home screen.

Listing 7-1. Activity Firing a Notification

public class NotificationActivity extends Activity {
 
    //Unique notification id values
    public static final int NOTE_BASIC = 100;
    public static final int NOTE_BIGTEXT = 200;
    public static final int NOTE_PICTURE = 300;
    public static final int NOTE_INBOX = 400;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView tv = new TextView(this);
        tv.setText("When You Leave Here, Notifications Will Post");
        tv.setGravity(Gravity.CENTER);
        
        setContentView(tv);
    }
 
    @Override
    public void onStop() {
        super.onStop();
        
        NotificationManager manager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
 
        //Post 4 unique notifications
        Notification note = buildNotification(NOTE_BASIC);
        manager.notify(NOTE_BASIC, note);
        note = buildNotification(NOTE_BIGTEXT);
        manager.notify(NOTE_BIGTEXT, note);
        note = buildNotification(NOTE_PICTURE);
        manager.notify(NOTE_PICTURE, note);
        note = buildNotification(NOTE_INBOX);
        manager.notify(NOTE_INBOX, note);
    }
    
    private Notification buildNotification(int type) {
        Intent launchIntent =
                new Intent(this, NotificationActivity.class);
        PendingIntent contentIntent =
                PendingIntent.getActivity(this, 0, launchIntent, 0);
        
 
        // Create notification using a builder
        NotificationCompat.Builder builder = new NotificationCompat.Builder(
                NotificationActivity.this);
        
        
        builder.setSmallIcon(R.drawable.ic_launcher)
                .setTicker("Something Happened")
                .setWhen(System.currentTimeMillis())
                .setAutoCancel(true)
                .setDefaults(Notification.DEFAULT_SOUND)
                .setContentTitle("We're Finished!")
                .setContentText("Click Here!")
                .setContentIntent(contentIntent);
        
        switch (type) {
            case NOTE_BASIC:
                //Return the simple notification
                return builder.build();
            case NOTE_BIGTEXT:
                //Include two actions
                builder.addAction(android.R.drawable.ic_menu_call,
                        "Call", contentIntent);
                builder.addAction(android.R.drawable.ic_menu_recent_history,
                        "History", contentIntent);
                //Use the BigTextStyle when expanded
                NotificationCompat.BigTextStyle textStyle =
                        new NotificationCompat.BigTextStyle(builder);
                textStyle.bigText(
                        "Here is some additional text to be displayed when the notification is "
                        +"in expanded mode.  I can fit so much more content into this giant view!");
                
                return textStyle.build();
            case NOTE_PICTURE:
                //Add one additional action
                builder.addAction(android.R.drawable.ic_menu_compass,
                        "View Location", contentIntent);
                //Use the BigPictureStyle when expanded
                NotificationCompat.BigPictureStyle pictureStyle =
                        new NotificationCompat.BigPictureStyle(builder);
                pictureStyle.bigPicture(BitmapFactory.decodeResource(getResources(), R.drawable.dog));
                
                return pictureStyle.build();
            case NOTE_INBOX:
                //Use the InboxStyle when expanded
                NotificationCompat.InboxStyle inboxStyle =
                        new NotificationCompat.InboxStyle(builder);
                inboxStyle.setSummaryText("4 New Tasks");
                inboxStyle.addLine("Make Dinner");
                inboxStyle.addLine("Call Mom");
                inboxStyle.addLine("Call Wife First");
                inboxStyle.addLine("Pick up Kids");
                
                return inboxStyle.build();
            default:
                throw new IllegalArgumentException("Unknown Type");
        }
    }
}

A series of new notification elements are created using Notification.Builder when the user leaves the activity. We will discuss the expanded types shortly, and just focus on the basic type for now. An icon resource and title string may be provided, and these items will display in the status bar at the time the notification occurs. In addition, we pass a time value (in milliseconds) to display in the notification list as the event time. Here, we are setting that value to the time the notification fired, but it may take on a different meaning in your application.

Important  We using NotificationCompat.Builder in this example, which is part of the Support Library and allows us to use the new API, which was introduced in Android 3.0 (API Level 11), going back to Android 1.6. If you are targeting Android 3.0+ only, you can replace NotificationCompat.Builder with Notification.Builder within the code.

Prior to creating the notification, we can fill it out with some other useful parameters, such as more-detailed text to be displayed in the notifications list when the user pulls down the status bar.

One of the parameters passed to the builder is a PendingIntent that points back to our activity. This Intent makes the notification interactive, allowing the user to tap it in the list and launch the activity.

Note  This Intent will launch a new activity with each event. If you would rather an existing instance of the activity respond to the launch, if one exists in the stack, be sure to include Intent flags and manifest parameters appropriately to accomplish this, such as Intent.FLAG_ACTIVITY_CLEAR_TOP and android:launchMode="singleTop".

To enhance the notification beyond the visual animation in the status bar, the notification defaults are modified to include that the system’s default notification sound be played when the notification fires. Values such as Notification.DEFAULT_VIBRATION and Notification.DEFAULT_LIGHTS may also be added.

Tip  If you would like to customize the sound played with a notification, set the Notification.sound parameter to a Uri that references a file or ContentProvider to read from.

We finally add a series of flags to the notification for further customization. This example uses setAutoCancel() in the builder to enable Notification.FLAG_AUTO_CANCEL, which cancels or removes the notification from the list as soon as the user selects it. Without this flag, the notification remains in the list until it is manually dismissed or canceled programmatically by calling NotificationManager.cancel() or NotificationManager.cancelAll(). Another helpful flag to set with the builder is setOngoing(), which disables any user ability to remove the notification. It can be cancelled only programmatically. This is useful for notifying the user of background operations currently running, such as music playing or location tracking underway.

Additionally, here are some other useful flags to apply that do not have methods inside the builder. These flags can be set directly on the notification after it is constructed:

  • FLAG_INSISTENT: Repeats the notification sounds until the user responds.
  • FLAG_NO_CLEAR: Does not allow the notification to be cleared with the user’s Clear Notifications button, but only through a call to cancel(). Once the notification is prepared, it is posted to the user with NotificationManager.notify(), which takes an ID parameter as well. Each notification type in your application should have a unique ID. The manager will allow only one notification with the same ID in the list at a time, and new instances with the same ID will take the place of those existing. In addition, the ID is required to cancel a specific notification manually.

When we run this example, an activity displays with instructions to leave the application immediately. Upon leaving, you can see the notification set post sometime later, even though the activity is no longer visible (see Figure 7-1).

9781430263227_Fig07-01.jpg

Figure 7-1. Notification that is occurring (left) and being displayed in the list (right)

Expanded Notification Styles

(API Level 16)

Starting with Android 4.1, notifications have the capability to display additional rich information with interactivity directly in the notification view. These are known as notification styles. Any notification that is currently at the top of the window shade is expanded by default, and the user can expand any other notification with a two-finger gesture. Therefore, expanded views don’t replace the traditional view; rather, they enhance the experience at certain times.

There are three default styles (implementations of Notification.Style) provided by the platform:

  • BigTextStyle: Displays an extended amount of text, such as the full contents of a message or post
  • BigPictureStyle: Displays a large, full-color image
  • InboxStyle: Provides a list of items, similar to the inbox view from an application such as Gmail

You are not limited to using these, however. Notification.Style is an interface that your application can implement to display any custom expanded layout that may best fit your needs.

In addition to styles, Android 4.1 added inline actions for an expanded notification. This means that you can add multiple action items for the user to take directly from the window shade view rather than just the single callback Intent when the user clicks the whole notification item. These items will show up on top of the expanded view, lined up at the bottom. Listing 7-2 shows the code from the previous example to add a BigTextStyle expanded notification collected together, and Figure 7-2 shows the result.

Listing 7-2. BigTextStyle Notification

//Create notification with the time it was fired
NotificationCompat.Builder builder =
        new NotificationCompat.Builder(NotificationActivity.this);
 
builder.setSmallIcon(R.drawable.icon)
       .setTicker("Something Happened")
       .setWhen(System.currentTimeMillis())
       .setAutoCancel(true)
       .setDefaults(Notification.DEFAULT_SOUND)
       .setContentTitle("We're Finished!")
       .setContentText("Click Here!")
       .setContentIntent(contentIntent);
 
//Add some custom actions
builder.addAction(android.R.id.drawable.ic_menu_call, "Call Back", contentIntent);
builder.addAction(android.R.id.drawable.ic_menu_recent_history,
        "Call History", contentIntent);
 
//Apply an expanded style
NotificationCompat.BigTextStyle expandedStyle =
        new NotificationCompat.BigTextStyle(builder);
expandedStyle.bigText("Here is some additional text to be displayed when"
    + " the notification is in expanded mode.  "
    + " I can fit so much more content into this giant view!");
 
Notification note = expandedStyle.build();

9781430263227_Fig07-02.jpg

Figure 7-2. BigTextStyle in the window shade

You can attach custom actions by using the addAction() method on the builder. You can see here how the actions that are added lay out with respect to the overall view. In this example, each action goes to the same place, but you can attach any PendingIntent to each action to make them travel to different places in your application.

The only necessary modification to the previous example is that we wrap our existing Builder object in the BigTextStyle and apply any specific customizations there. In this case, the only additional piece of information is setting bigText() with the text to display in expanded mode. Then the notification is created from the build() method on the style, rather than the builder.

Let’s take a look at BigPictureStyle in Listing 7-3 and Figure 7-3.

Listing 7-3. BigPictureStyle Notification

//Create notification with the time it was fired
NotificationCompat.Builder builder =
        new NotificationCompat.Builder(NotificationActivity.this);
 
builder.setSmallIcon(R.drawable.icon)
       .setTicker("Something Happened")
       .setWhen(System.currentTimeMillis())
       .setAutoCancel(true)
       .setDefaults(Notification.DEFAULT_SOUND)
       .setContentTitle("We're Finished!")
       .setContentText("Click Here!")
       .setContentIntent(contentIntent);
 
//Add some custom actions
builder.addAction(android.R.id.drawable.ic_menu_compass,
        "View Location", contentIntent);
 
//Apply an expanded style
NotificationCompat.BigPictureStyle expandedStyle =
        new NotificationCompat.BigPictureStyle(builder);
expandedStyle.bigPicture(
        BitmapFactory.decodeResource(getResources(), R.drawable.icon) );
 
Notification note = expandedStyle.build();

9781430263227_Fig07-03.jpg

Figure 7-3. BigPictureStyle in the window shade

This code is almost identical to BigTextStyle, except that here we use the bigPicture() method to pass in the Bitmap that will be used as the full-color image. Finally, take a look at InboxStyle in Listing 7-4 and Figure 7-4.

Listing 7-4. InboxStyle Notification

//Create notification with the time it was fired
NotificationCompat.Builder builder =
        new NotificationCompat.Builder(NotificationActivity.this);
 
builder.setSmallIcon(R.drawable.icon)
       .setTicker("Something Happened")
       .setWhen(System.currentTimeMillis())
       .setAutoCancel(true)
       .setDefaults(Notification.DEFAULT_SOUND)
       .setContentTitle("We're Finished!")
       .setContentText("Click Here!")
       .setContentIntent(contentIntent);
 
//Apply an expanded style
NotificationCompat.InboxStyle expandedStyle =
        new NotificationCompat.InboxStyle(builder);
expandedStyle.setSummaryText("4 New Tasks");
expandedStyle.addLine("Make Dinner");
expandedStyle.addLine("Call Mom");
expandedStyle.addLine("Call Wife First");
expandedStyle.addLine("Pick up Kids");
 
Notification note = expandedStyle.build();

9781430263227_Fig07-04.jpg

Figure 7-4. InboxStyle in the window shade

With Notification.InboxStyle, multiple items are added to the list by using the addLine() method. We also topped off the example with a summary line noting the number of items with setSummaryText(), a method that is available for use with all the previous styles as well.

As before, we’ve used the Support Library’s NotificationCompat class, which allows us to call all these methods in an application running back to API Level 4. If your application is targeting Android 4.1 as the minimum platform, you can replace this with the native Notification.Builder.

One of the real powers of the Support Library is shown in this particular case. We are calling methods that are not available until API Level 16, but the Support Library takes care of version checking for us under the hood and simply ignores methods that a certain platform doesn’t support; we don’t have to branch our code to use new APIs.

As a result, when this same code is used on a device running Android 4.0 or earlier, the traditional notification will simply appear as if we hadn’t taken advantage of the new features.

Note  One of the great powers of the Support Library is that you can use new APIs in applications running on older Android devices, and you don’t have to branch your own code to do so.

NotificationListenerService

(API Level 18)

As of Android 4.3, a new service is available to applications that wish to monitor the status of all the notifications on the device. Applications can extend NotificationListenerService to receive updates whenever any application posts a new notification or when the user clears an existing notification. In addition, the application can programmatically cancel any specific notification, or clear all of them at once.

ENABLING NOTIFICATION ACCESS

Because this service provides your application global access to the active notifications list, permission must first be granted. However, in this case, your application cannot declare this as a standard permission it would like to obtain. Instead, the user must explicitly grant access from the Security section of the device’s Settings application. There is a section for Notification Access, which will list all installed applications that have an exported NotificationListenerService for the user to enable or disable this feature.

Figure 7-5 shows what the Notification Access section of Settings looks like on a Nexus device, along with the prompt that a user will see after tapping your application item to enable access to this information.

9781430263227_Fig07-05.jpg

Figure 7-5. Allowing notification access to an application

It is up to your application to guide the user to Settings to enable this service to receive events; the framework will not do it for you. You can take the user directly to the appropriate screen in Settings by firing an Intent with the android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS action string to start the activity. As of Android 4.3, this string is not public in the SDK, so be aware that it may change in future versions.

Listing 7-5 shows a simple extension of NotificationListenerService.

Listing 7-5. NotificationListenerService Example

public class MonitorService extends NotificationListenerService {
    private static final String TAG = "RecipesMonitorService";
    
    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
        //Validate the notification came from this application
        if (!TextUtils.equals(sbn.getPackageName(), getPackageName())) {
            return;
        }
        
        Log.i(TAG, "Notification "+sbn.getId()+" Posted");
    }
 
    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {
        //Validate the notification came from this application
        if (!TextUtils.equals(sbn.getPackageName(), getPackageName())) {
            return;
        }
        //We are looking for the basic notification
        if (NotificationActivity.NOTE_BASIC != sbn.getId()) {
            return;
        }
        
        //If the basic notification cancels, dismiss all of ours
        for (StatusBarNotification note : getActiveNotifications()) {
            if (TextUtils.equals(note.getPackageName(), getPackageName())) {
                cancelNotification(note.getPackageName(),
                        note.getTag(),
                        note.getId());
            }
        }
    }
 
}

There are two abstract methods you must implement: onNotificationPosted() and onNotificationRemoved(). These will be called by the framework when a new notification comes in or another is dismissed, respectively. The content passed in is a StatusBarNotification instance, which is just a basic wrapper around the original notification with some additional metadata (for example, the package name of the application that posted it and the ID or tag applied). The original notification is also still accessible as a parameter.

In this example, when a notification is added, we simply log the event if the notification came from our application. If a notification is removed, we check whether it was the basic style notification element and, if so, dismiss all the notifications posted from our application that are still active. The getActiveNotifications() method is helpful in obtaining everything currently visible to the user. We can verify which notifications came from us by comparing the package names of each one. When the package matches, we call cancelNotification() with the metadata from the notification element to remove it programmatically. You can also call cancelAllNotifications() to clear the entire window shade without any regard for where the active elements came from.

Listing 7-6 shows the AndroidManifest.xml snippet you will need to add.

Listing 7-6. NotificationListenerService Manifest Element

<service android:name=".MonitorService"
    android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
    <intent-filter>
        <action android:name="android.service.notification.NotificationListenerService" />
    </intent-filter>
</service>

The two required elements here are the action string of the <intent-filter> and the declared permission. The framework will look for both of these when determining which NotificationListenerService elements it can bind to.

7-2. Creating Timed and Periodic Tasks

Problem

Your application needs to run an operation on a timer, such as updating the UI on a scheduled basis.

Solution

(API Level 1)

Use the timed operations provided by Handler. With Handler, operations can efficiently be scheduled to occur at a specific time or after a specified delay.

How It Works

Let’s look at an example activity that displays the current time in a TextView. See Listing 7-7.

Listing 7-7. Activity Updated with a Handler

public class TimingActivity extends Activity {
    
    TextView mClock;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mClock = new TextView(this);
        setContentView(mClock);
    }
    
    private Handler mHandler = new Handler();
    private Runnable timerTask = new Runnable() {
        @Override
        public void run() {
            Calendar now = Calendar.getInstance();
            mClock.setText(String.format("%02d:%02d:%02d",
                    now.get(Calendar.HOUR),
                    now.get(Calendar.MINUTE),
                    now.get(Calendar.SECOND)) );
            //Schedule the next update in one second
            mHandler.postDelayed(timerTask,1000);
        }
    };
    
    @Override
    public void onResume() {
        super.onResume();
        mHandler.post(timerTask);
    }
    
    @Override
    public void onPause() {
        super.onPause();
        mHandler.removeCallbacks(timerTask);
    }
}

Here we’ve wrapped up the operation of reading the current time and updating the UI into a Runnable named timerTask, which will be triggered by the Handler that has also been created. When the activity becomes visible, the task is executed as soon as possible with a call to Handler.post(). After the TextView has been updated, the final operation of timerTask is to invoke the Handler to schedule another execution 1 second (1,000 milliseconds) from now by using Handler.postDelayed().

As long as the activity remains uninterrupted, this cycle will continue, with the UI being updated every second. As soon as the activity is paused (the user leaves or something else grabs his or her attention), Handler.removeCallbacks() removes all pending operations and ensures the task will not be called further until the activity becomes visible once more.

Tip  In this example, we are safe to update the UI because the Handler was created on the main thread. A Handler will always execute operations on the thread in which it was created, unless a Looper from another thread is passed explicitly to its constructor. We will see how this can be used for background queues in a later recipe, but it is also worth noting here that you can create a Handler from a background thread that posts to the main thread by passing it the result of Looper.getMainLooper(), which is a static reference to the Looper of the main UI thread.

7-3. Scheduling a Periodic Task

Problem

Your application needs to register to run a task periodically, such as checking a server for updates or reminding the user to do something.

Solution

(API Level 1)

Utilize the AlarmManager to manage and execute your task. AlarmManager is useful for scheduling future single or repeated operations that need to occur even if your application is not running. AlarmManager is handed a PendingIntent to fire whenever an alarm is scheduled. This Intent can point to any system component, such as an Activity, BroadcastReceiver, or Service, that can be executed when the alarm triggers.

It should be noted that this method is best suited to operations that need to occur even when the application code may not be running. The AlarmManager requires too much overhead to be useful for simple timing operations that may be needed while an application is in use. These are better handled using the postAtTime() and postDelayed() methods of a Handler.

How It Works

Let’s take a look at how AlarmManager can be used to trigger a BroadcastReceiver on a regular basis. See Listings 7-8 through 7-10.

Listing 7-8. BroadcastReceiver to Be Triggered

public class AlarmReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        //Perform an interesting operation, we'll just display the current time
        Calendar now = Calendar.getInstance();
        DateFormat formatter = SimpleDateFormat.getTimeInstance();
        Toast.makeText(context, formatter.format(now.getTime()),
            Toast.LENGTH_SHORT).show();
    }
}

Reminder  BroadcastReceiver (AlarmReceiver, in this case) must be declared in the manifest with a <receiver> tag in order for AlarmManager to be able to trigger it. Be sure to include one within your <application> tag like so:
<application>
...
<receiver android:name=".AlarmReceiver" />
</application>

Listing 7-9. res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/start"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start Alarm" />
    <Button
        android:id="@+id/stop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Cancel Alarm" />
</LinearLayout>

Listing 7-10. Activity to Register/Unregister Alarms

public class AlarmActivity extends Activity implements View.OnClickListener {
    
    private PendingIntent mAlarmIntent;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        //Attach the listener to both buttons
        findViewById(R.id.start).setOnClickListener(this);
        findViewById(R.id.stop).setOnClickListener(this);
        //Create the launch sender
        Intent launchIntent = new Intent(this, AlarmReceiver.class);
        mAlarmIntent = PendingIntent.getBroadcast(this, 0, launchIntent, 0);
    }
 
    @Override
    public void onClick(View v) {
        AlarmManager manager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
        long interval = 5*1000; //5 seconds
        
        switch(v.getId()) {
        case R.id.start:
            Toast.makeText(this, "Scheduled", Toast.LENGTH_SHORT).show();
            manager.setRepeating(AlarmManager.ELAPSED_REALTIME,
                    SystemClock.elapsedRealtime()+interval,
                    interval,
                    mAlarmIntent);
            break;
        case R.id.stop:
            Toast.makeText(this, "Canceled", Toast.LENGTH_SHORT).show();
            manager.cancel(mAlarmIntent);
            break;
        default:
            break;
        }
    }
}

In this example, we have provided a very basic BroadcastReceiver that, when triggered, will simply display the current time as a Toast. That receiver must be registered in the application’s manifest with a <receiver> tag. Otherwise, AlarmManager—which is external to your application—will not be aware of how to trigger it. The sample activity presents two buttons: one to begin firing regular alarms, and the other to cancel them.

The operation to trigger is referenced by a PendingIntent, which will be used to both set and cancel the alarms. We create an Intent referencing the application’s BroadcastReceiver directly, and then we wrap that Intent inside a PendingIntent obtained with getBroadcast() (because we are creating a reference to a BroadcastReceiver).

Reminder  PendingIntent has the creator methods getActivity() and getService() as well. Be sure to reference the correct application component you are triggering when creating this piece.

When the Start button is pressed, the activity registers a repeating alarm by using AlarmManager.setRepeating(). In addition to PendingIntent, this method takes some parameters to determine when to trigger the alarms. The first parameter defines the alarm type, in terms of the units of time to use and whether the alarm should occur when the device is in sleep mode. In the example, we chose ELAPSED_REALTIME, which indicates a value (in milliseconds) since the last device boot. In addition, there are three other modes that may be used:

  • ELAPSED_REALTIME_WAKEUP: The alarm times are referenced to time elapsed and will wake the device to trigger if it is asleep.
  • RTC: The alarm times are referenced to UTC time.
  • RTC_WAKEUP: The alarm times are referenced to UTC time and will wake the device to trigger if it is asleep. The remaining parameters (respectively) refer to the first time the alarm will trigger and the interval on which it should repeat. Because the chosen alarm type is ELAPSED_REALTIME, the start time must also be relative to elapsed time; SystemClock.elapsedRealtime() provides the current time in this format.

The alarm in the example is registered to trigger 5 seconds after the button is pressed, and then every 5 seconds after that. Every 5 seconds, a Toast will come onscreen with the current time value, even if the application is no longer running or in front of the user. When the user displays the activity and presses the Stop button, any pending alarms matching our PendingIntent are immediately canceled and will stop the flow of Toasts.

A More Precise Example

What if we wanted to schedule an alarm to occur at a specific time? Perhaps once per day at 9:00 AM? Setting AlarmManager with some slightly different parameters could accomplish this. See Listing 7-11.

Listing 7-11. Precision Alarm

long oneDay = 24*3600*1000; //24 hours
long firstTime;
 
//Get a Calendar (defaults to today)
//Set the time to 09:00:00
Calendar startTime = Calendar.getInstance();
startTime.set(Calendar.HOUR_OF_DAY, 9);
startTime.set(Calendar.MINUTE, 0);
startTime.set(Calendar.SECOND, 0);
 
//Get a Calendar at the current time
Calendar now = Calendar.getInstance();
 
if(now.before(startTime)) {
    //It's not 9AM yet, start today
    firstTime = startTime.getTimeInMillis();
} else {
    //Start 9AM tomorrow
    startTime.add(Calendar.DATE, 1);
    firstTime = startTime.getTimeInMillis();
}
 
//Set the alarm
 manager.setRepeating(AlarmManager.RTC_WAKEUP,
                firstTime,
                oneDay,
                mAlarmIntent);

This example uses an alarm that is referenced to real time. A determination is made whether the next occurrence of 9:00 AM will be today or tomorrow, and that value is returned as the initial trigger time for the alarm. The calculated value of 24 hours in terms of milliseconds is then passed as the interval so that the alarm triggers once per day from that point forward.

Important  Alarms do not persist through a device reboot. If a device is powered off and then back on, any previously registered alarms must be rescheduled.

7-4. Creating Sticky Operations

Problem

Your application needs to execute one or more background operations that will run to completion even if the user suspends the application.

Solution

(API Level 3)

Create an IntentService to handle the work. IntentService is a wrapper around Android’s base service implementation, the key component to doing work in the background without interaction from the user. IntentService queues incoming work (expressed using Intents), processing each request in turn, and then stops itself when the queue is empty.

IntentService also handles creation of the worker thread needed to do the work in the background, so it is not necessary to use AsyncTask or Java threads to ensure that the operation is properly in the background.

This recipe provides an example of using IntentService to create a central manager of background operations. In the example, the manager will be invoked externally with calls to Context.startService(). The manager will queue up all requests received, and process them individually with a call to onHandleIntent().

How It Works

Let’s take a look at how to construct a simple IntentService implementation to handle a series of background operations. See Listing 7-12.

Listing 7-12. IntentService Handling Operations

public class OperationsManager extends IntentService {
 
    public static final String ACTION_EVENT = "ACTION_EVENT";
    public static final String ACTION_WARNING = "ACTION_WARNING";
    public static final String ACTION_ERROR = "ACTION_ERROR";
    public static final String EXTRA_NAME = "eventName";
 
    private static final String LOGTAG = "EventLogger";
    
    private IntentFilter matcher;
    
    public OperationsManager() {
        super("OperationsManager");
        //Create the filter for matching incoming requests
        matcher = new IntentFilter();
        matcher.addAction(ACTION_EVENT);
        matcher.addAction(ACTION_WARNING);
        matcher.addAction(ACTION_ERROR);
    }
 
    @Override
    protected void onHandleIntent(Intent intent) {
        //Check for a valid request
        if(!matcher.matchAction(intent.getAction())) {
            Toast.makeText(this, "OperationsManager: Invalid Request",
                    Toast.LENGTH_SHORT).show();
            return;
        }
 
        //Handle each request directly in this method. Don't create more threads.
        if(TextUtils.equals(intent.getAction(), ACTION_EVENT)) {
            logEvent(intent.getStringExtra(EXTRA_NAME));
        }
        if(TextUtils.equals(intent.getAction(), ACTION_WARNING)) {
            logWarning(intent.getStringExtra(EXTRA_NAME));
        }
        if(TextUtils.equals(intent.getAction(), ACTION_ERROR)) {
            logError(intent.getStringExtra(EXTRA_NAME));
        }
    }
    
    private void logEvent(String name) {
        try {
            //Simulate a long network operation by sleeping
            Thread.sleep(5000);
            Log.i(LOGTAG, name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    private void logWarning(String name) {
        try {
            //Simulate a long network operation by sleeping
            Thread.sleep(5000);
            Log.w(LOGTAG, name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    private void logError(String name) {
        try {
            //Simulate a long network operation by sleeping
            Thread.sleep(5000);
            Log.e(LOGTAG, name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

IntentService does not have a default constructor (one that takes no parameters), so a custom implementation must implement a constructor that calls through to super with a service name. This name is of little technical importance, as it is useful only for debugging; Android uses the name provided to name the worker thread that it creates.

All requests are processed by the service through the onHandleIntent() method. This method is called on the provided worker thread, so all work should be done directly here; no new threads or operations should be created. When onHandleIntent() returns, this is the signal to the IntentService to begin processing the next request in the queue.

This example provides three logging operations that can be requested using different action strings on the request Intents. For demonstration purposes, each operation writes the provided message out to the device log by using a specific logging level (INFO, WARNING, or ERROR). Note that the message itself is passed as an extra of the request Intent. Use the data and extra fields of each Intent to hold any parameters for the operation, leaving the action field to define the operation type.

The service in the example maintains an IntentFilter, which is used for convenience to determine whether a valid request has been made. All of the valid actions are added to the filter when the service is created, allowing us to call IntentFilter.matchAction() on any incoming request to determine whether it includes an action we can process here.

Listings 7-13 and 7-14 reveal an example including an activity calling in to this service to perform work.

Listing 7-13. AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.examples.sticky"
    android:versionCode="1"
    android:versionName="1.0">
    <uses-sdk android:minSdkVersion="3" />
 
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".ReportActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".OperationsManager"></service>
    </application>
</manifest>

Reminder  The package attribute in AndroidManifest.xml must match the package you have chosen for your application; "com.examples.sticky" is simply the chosen package for our example here.

Note  Because IntentService is invoked as a service, it must be declared in the application manifest with a <service> tag.

Listing 7-14. Activity Calling IntentService

public class ReportActivity extends Activity {
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        logEvent("CREATE");
    }
    
    @Override
    public void onStart() {
        super.onStart();
        logEvent("START");
    }
    
    @Override
    public void onResume() {
        super.onResume();
        logEvent("RESUME");
    }
    
    @Override
    public void onPause() {
        super.onPause();
        logWarning("PAUSE");
    }
    
    @Override
    public void onStop() {
        super.onStop();
        logWarning("STOP");
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        logWarning("DESTROY");
    }
    
    private void logEvent(String event) {
        Intent intent = new Intent(this, OperationsManager.class);
        intent.setAction(OperationsManager.ACTION_EVENT);
        intent.putExtra(OperationsManager.EXTRA_NAME, event);
        
        startService(intent);
    }
    
    private void logWarning(String event) {
        Intent intent = new Intent(this, OperationsManager.class);
        intent.setAction(OperationsManager.ACTION_WARNING);
        intent.putExtra(OperationsManager.EXTRA_NAME, event);
        
        startService(intent);
    }
}

This activity isn’t much to look at, as all the interesting events are sent out through the device log instead of to the user interface. Nevertheless, it helps illustrate the queue-processing behavior of the service we created in the previous example. As the activity becomes visible, it will call through all of its normal life-cycle methods, resulting in three requests made of the logging service. As each request is processed, a line will output to the log and the service will move on.

Tip  These log statements are visible through the logcat tool provided with the SDK. The logcat output from a device or emulator is visible from within most development environments (including Eclipse) or from the command line by typing adb logcat.

Notice also that when the service is finished with all three requests, a notification is logged out that the service has been stopped. IntentServices are around in memory for only as long as required to complete the job; this is a very useful feature for your services to have, making them good citizens of the system.

Pressing either the HOME or BACK buttons will cause more of the life-cycle methods to generate requests of the service, and the Pause/Stop/Destroy portion calls a separate operation in the service, causing their messages to be logged as warnings; simply setting the action string of the request Intent to a different value controls this.

Notice that messages continue to be output to the log, even after the application is no longer visible (or even if another application is opened instead). This is the power of the Android service component at work. These operations are protected from the system until they are complete, regardless of user behavior.

A Possible Drawback

In each of the operation methods, a 5-second delay has been placed to simulate the time required for an actual request to be made of a remote API or some similar operation. When running this example, it also helps to illustrate that IntentService handles all requests sent to it in a serial fashion with a single worker thread. The example queues multiple requests in succession from each life-cycle method; however, the result will still be a log message every 5 seconds, because IntentService does not start a new request until the current one is complete (essentially, when onHandleIntent() returns).

If your application requires concurrency from sticky background tasks, you may need to create a more customized service implementation that uses a pool of threads to execute work. The beauty of Android being an open source project is that you can go directly to the source code for IntentService and use it as a starting point for such an implementation, minimizing the amount of time and custom code required.

7-5. Running Persistent Background Operations

Problem

Your application has a component that must be running in the background indefinitely, performing some operation or monitoring certain events to occur.

Solution

(API Level 1)

Build the component into a service. Services are designed as background components that an application may start and leave running for an indefinite amount of time. Services are also given elevated status above other background processes in terms of protection from being killed in low-memory conditions.

Services may be started and stopped explicitly for operations that do not require a direct connection to another component (like an activity). However, if the application must interact directly with the service, a binding interface is provided to pass data. In these instances, the service may be started and stopped implicitly by the system as is required to fulfill its requested bindings.

The key thing to remember with service implementations is to always be user-friendly. An indefinite operation most likely should not be started unless the user explicitly requests it. The overall application should probably contain an interface or setting that allows the user to control enabling or disabling such a service.

How It Works

Listing 7-15 is an example of a persisted service that is used to track and log the user’s location over a certain period.

Listing 7-15. Persistent Tracking Service

public class TrackerService extends Service implements LocationListener {
 
    private static final String LOGTAG = "TrackerService";
    
    private LocationManager manager;
    private ArrayList<Location> storedLocations;
    
    private boolean isTracking = false;
    
    /* Service Setup Methods */
    @Override
    public void onCreate() {
        manager = (LocationManager)getSystemService(LOCATION_SERVICE);
        storedLocations = new ArrayList<Location>();
        Log.i(LOGTAG, "Tracking Service Running...");
    }
 
    @Override
    public void onDestroy() {
        manager.removeUpdates(this);
        Log.i(LOGTAG, "Tracking Service Stopped...");
    }
 
    public void startTracking() {
        if(!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
            return;
        }
        Toast.makeText(this, "Starting Tracker", Toast.LENGTH_SHORT).show();
        manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 30000, 0, this);
        
        isTracking = true;
    }
    
    public void stopTracking() {
        Toast.makeText(this, "Stopping Tracker", Toast.LENGTH_SHORT).show();
        manager.removeUpdates(this);
        isTracking = false;
    }
    
    public boolean isTracking() {
        return isTracking;
    }
    
    /* Service Access Methods */
    public class TrackerBinder extends Binder {
        TrackerService getService() {
            return TrackerService.this;
        }
    }
    
    private final IBinder binder = new TrackerBinder();
    
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }
    
    public int getLocationsCount() {
        return storedLocations.size();
    }
    
    public ArrayList<Location> getLocations() {
        return storedLocations;
    }
    
    /* LocationListener Methods */
    @Override
    public void onLocationChanged(Location location) {
        Log.i("TrackerService", "Adding new location");
        storedLocations.add(location);
    }
 
    @Override
    public void onProviderDisabled(String provider) { }
 
    @Override
    public void onProviderEnabled(String provider) { }
 
    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) { }
}

This service monitors and tracks the updates it receives from the LocationManager. When the service is created, it prepares a blank list of Location items and waits to begin tracking. An external component, such as an activity, can call startTracking() and stopTracking() to enable and disable the flow of location updates to the service. In addition, methods are exposed to access the list of locations that the service has logged.

Because this service requires direct interaction from an activity or other component, a Binder interface is required. The Binder concept can get complex when a service has to communicate across process boundaries, but for instances like this, where everything is local to the same process, a very simple Binder is created with one method, getService(), to return the service instance itself to the caller. We’ll look at this in more detail from the activity’s perspective in a moment.

When tracking is enabled on the service, it registers for updates with LocationManager, and it stores every update received in its locations list. Notice that requestLocationUpdates() was called with a minimum time of 30 seconds. Because this service is expected to be running for a long time, it is prudent to space out the updates to give the GPS (and consequently the battery) a little rest.

Now let’s take a look at a simple activity that allows the user access into this service. See Listings 7-16 through 7-18.

Listing 7-16. AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.examples.service"
    android:versionCode="1"
    android:versionName="1.0">
    <uses-sdk android:minSdkVersion="1" />
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".ServiceActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".TrackerService"></service>
    </application>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
</manifest>

Reminder  The service must be declared in the application manifest by using a <service> tag so Android knows how and where to call on it. Also, for this example, the permission android.permission.ACCESS_FINE_LOCATION is required because we are working with the GPS.

Listing 7-17. res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/enable"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start Tracking" />
    <Button
        android:id="@+id/disable"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Stop Tracking" />
    <TextView
        android:id="@+id/status"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

Listing 7-18. Activity Interacting with Service

public class ServiceActivity extends Activity implements View.OnClickListener {
    
    Button enableButton, disableButton;
    TextView statusView;
    
    TrackerService trackerService;
    Intent serviceIntent;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        enableButton = (Button)findViewById(R.id.enable);
        enableButton.setOnClickListener(this);
        disableButton = (Button)findViewById(R.id.disable);
        disableButton.setOnClickListener(this);
        statusView = (TextView)findViewById(R.id.status);
        
        serviceIntent = new Intent(this, TrackerService.class);
    }
    
    @Override
    public void onResume() {
        super.onResume();
        //Starting the service makes it stick, regardless of bindings
        startService(serviceIntent);
        //Bind to the service
        bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
    }
    
    @Override
    public void onPause() {
        super.onPause();
        if(!trackerService.isTracking()) {
            //Stopping the service lets it die once unbound
            stopService(serviceIntent);
        }
        //Unbind from the service
        unbindService(serviceConnection);
    }
    
    @Override
    public void onClick(View v) {
        switch(v.getId()) {
        case R.id.enable:
            trackerService.startTracking();
            break;
        case R.id.disable:
            trackerService.stopTracking();
            break;
        default:
            break;
        }
        updateStatus();
    }
    
    private void updateStatus() {
        if(trackerService.isTracking()) {
            statusView.setText(
                String.format("Tracking enabled. %d locations
                    logged.",trackerService.getLocationsCount()));
        } else {
            statusView.setText("Tracking not currently enabled.");
        }
    }
    
    private ServiceConnection serviceConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            trackerService = ((TrackerService.TrackerBinder)service).getService();
            updateStatus();
        }
 
        public void onServiceDisconnected(ComponentName className) {
            trackerService = null;
        }
    };
}

Figure 7-6 displays the basic activity with two buttons for the user to enable and disable location-tracking behavior, and a text display for the current service status.

9781430263227_Fig07-06.jpg

Figure 7-6. ServiceActivity layout

While the activity is visible, it is bound to the TrackerService. This is done with the help of the ServiceConnection interface, which provides callback methods when the binding and unbinding operations are complete. With the service bound to the activity, you can now make direct calls on all the public methods exposed by the service.

However, bindings alone will not allow the service to run for the long term; accessing the service solely through its Binder interface causes it to be created and destroyed automatically along with the life cycle of this activity. In this case, we want the service to persist beyond when this activity is in memory. In order to accomplish this, the service is explicitly started via startService() before it is bound. There is no harm in sending start commands to a service that is already running, so we can safely do this in onResume() as well.

The service will now continue running in memory, even after the activity unbinds itself. In onPause(), the example always checks whether the user has activated tracking, and if not, it stops the service first. This allows the service to die if it is not required for tracking, which keeps the service from perpetually hanging out in memory if it has no real work to do.

Running this example and pressing the Start Tracking button will spin up the persisted service and the LocationManager. The user may leave the application at this point, and the service will remain running, all the while logging all incoming location updates from the GPS. Upon returning to this application, the user can see that the service is still running, and the current number of stored location points is displayed. Pressing Stop Tracking will end the process and allow the service to die as soon as the user leaves the activity once more.

7-6. Launching Other Applications

Problem

Your application requires a specific function that another application on the device is already programmed to do. Instead of overlapping functionality, you would like to launch the other application for the job instead.

Solution

(API Level 1)

Use an implicit Intent to tell the system what you are looking to do, and determine whether any applications exist to meet the need. Most often, developers use Intents in an explicit fashion to start another activity or service, like so:

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

By declaring the specific component we want to launch, the Intent is very explicit in its delivery. We also have the power to define an Intent in terms of its action, category, data, and type to define a more implicit requirement of what task we want to accomplish.

External applications are always launched within the same Android task as your application when fired in this fashion, so once the operation is complete (or if the user backs out), the user is returned to your application. This keeps the experience seamless, allowing multiple applications to act as one from the user’s perspective.

How It Works

When defining Intents in this fashion, it can be unclear what information you must include, because there is no published standard and it is possible for two applications offering the same service (reading a PDF file, for example) to define slightly different filters to listen for incoming Intents. You want to make sure to provide enough information for the system (or the user) to pick the best application to handle the required task.

The core piece of information to define on almost any implicit Intent is the action: a string value that is passed either in the constructor or via Intent.setAction(). This value tells Android what you want to do, whether it is to view a piece of content, send a message, select a choice, and so on. From there, the fields provided are scenario specific, and often multiple combinations can arrive at the same result. Let’s take a look at some useful examples.

Read a PDF File

Components to display PDF documents are not included in the core SDK, although almost every consumer Android device on the market today ships with a PDF reader application, and many more are available through Google Play. Because of this, it may not make sense to go through the trouble of embedding PDF display capabilities in your application.

Instead, Listing 7-19 illustrates how to find and launch another app to view the PDF.

Listing 7-19. Method to View PDF

private void viewPdf(Uri file) {
        Intent intent;
        intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(file, "application/pdf");
        try {
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            //No application to view, ask to download one
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("No Application Found");
            builder.setMessage("We could not find an application to view PDFs."
                    +"  Would you like to download one from Android Market?");
            builder.setPositiveButton("Yes, Please",
                new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    Intent marketIntent = new Intent(Intent.ACTION_VIEW);
                    marketIntent.setData(
                            Uri.parse("market://details?id=com.adobe.reader"));
                    startActivity(marketIntent);
                }
            });
            builder.setNegativeButton("No, Thanks", null);
            builder.create().show();
        }
    }

This example will open any local PDF file on the device (internal or external storage) by using the best application found. If no application is found on the device to view PDFs, a message will encourage the user to go to Google Play and download one.

The Intent we create for this is constructed with the generic Intent.ACTION_VIEW action string, telling the system we want to view the data provided in the Intent. The data file itself and its MIME type are also set to tell the system what kind of data we want to view.

Tip  Intent.setData() and Intent.setType() clear each other’s previous values when used. If you need to set both simultaneously, use Intent.setDataAndType(), as in the example.

If startActivity() fails with an ActivityNotFoundException, it means that no application is installed on the user’s device that can view PDFs. We want users to have the full experience, so if this happens, a dialog box indicates the problem and asks whether the user would like to go to Market and get a reader. If the user presses Yes, another implicit Intent will request that Google Play be opened directly to the application page for Adobe Reader, a free application the user may download to view PDF files. We’ll discuss the Uri scheme used for this Intent in the next recipe.

Notice that the example method takes a Uri parameter to the local file. Here is an example of how to retrieve a Uri for files located on internal storage:

String filename = NAME_OF YOUR_FILE;
File internalFile = getFileStreamPath(filename);
Uri internal = Uri.fromFile(internalFile);

The method getFileStreamPath() is called from a Context, so if this code is not in an activity, you must have reference to a Context object to call on. Here’s how to create a Uri for files located on external storage:

String filename = NAME_OF YOUR_FILE;
File externalFile = new File(Environment.getExternalStorageDirectory(), filename);
Uri external = Uri.fromFile(externalFile);

This same example will work for any other document type as well by simply changing the MIME type attached to the Intent.

Share with Friends

Another popular feature for developers to include in their applications is a method of sharing the application content with others, either through e-mail, text messaging, or prominent social networks. All Android devices include applications for e-mail and text messaging, and most users who wish to share via a social network (for example, Facebook or Twitter) also have those mobile applications on their devices.

As it turns out, this task can also be accomplished using an implicit Intent because most of these applications respond to the Intent.ACTION_SEND action string in some way. Listing 7-20 is an example of allowing a user to post to any medium with a single Intent request.

Listing 7-20. Sharing Intent

private void shareContent(String update) {
    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType("text/plain");
    intent.putExtra(Intent.EXTRA_TEXT, update);
    startActivity(Intent.createChooser(intent, "Share..."));
}

Here, we tell the system that we have a piece of text that we would like to send, passed in as an extra. This is a very generic request, and we expect more than one application to be able to handle it. By default, Android will present the user with a list of applications that the user can select to open. In addition, some devices provide the user with a check box to set a selection as a default so the list is never shown again.

We would prefer to have a little more control over this process because we also expect multiple results every time. Therefore, instead of passing the Intent directly to startActivity(), we first pass it through Intent.createChooser(), which allows us to customize the title and guarantee the selection list will always be displayed.

When the user selects a choice, that specific application will launch with the EXTRA_TEXT prepopulated into the message entry box, ready for sharing!

Use ShareActionProvider

(API Level 14)

Starting with Android 4.0, a new widget was introduced to assist applications in sharing content by using a common mechanism called ShareActionProvider. It is designed to be added to an item in the options menu to show up either on the Action Bar or in the overflow. It also has an added feature for the users in that, by default, it ranks the share options it provides by usage. Options that users click most frequently will always be at the top of the list.

Implementing ShareActionProvider in a menu is quite simple, and it requires only a few more lines of code than creating the share Intent itself. Listing 7-21 shows how to attach the provider to a menu item.

Listing 7-21. res/menu/options.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/menu_share"
        android:showAsAction="ifRoom"
        android:title="Share"
        android:actionProviderClass="android.widget.ShareActionProvider"/>
</menu>

Note  If you do not define your Menu in XML, you can still attach the ShareActionProvider by calling setActionProvider() inside your Java code.

Listing 7-22 shows how to attach the share Intent to the provider widget inside an activity.

Listing 7-22. Providing the Share Intent

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    //Inflate the menu
    getMenuInflater().inflate(R.menu.options, menu);
 
    //Find the item and set the share Intent
    MenuItem item = menu.findItem(R.id.menu_share);
    ShareActionProvider provider = (ShareActionProvider) item.getActionProvider();
 
    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType("text/plain");
    intent.putExtra(Intent.EXTRA_TEXT, update);
    provider.setShareIntent(intent);
 
    return true;
}

And that’s it! The provider handles all the user interaction so your application doesn’t even need to handle the user selection events for that MenuItem.

7-7. Launching System Applications

Problem

Your application requires a specific function that one of the system applications on the device is already programmed to do. Instead of overlapping functionality, you would like to launch the system application for the job instead.

Solution

(API Level 1)

Use an implicit Intent to tell the system which application you are interested in. Each system application subscribes to a custom Uri scheme that can be inserted as data into an implicit Intent to signify the specific application you need to launch.

External applications are always launched in the same task as your application when fired in this fashion, so once the task is complete (or if the user backs out), the user is returned to your application. This keeps the experience seamless, allowing multiple applications to act as one from the user’s perspective.

How It Works

All of the following examples will construct Intents that can be used to launch system applications in various states. Once constructed, you should launch these applications by passing the Intent to startActivity().

Browser

The Browser application may be launched to display a web page or run a web search.

To display a web page, construct and launch the following Intent:

Intent pageIntent = new Intent();
pageIntent.setAction(Intent.ACTION_VIEW);
pageIntent.setData(Uri.parse("http://WEB_ADDRESS_TO_VIEW"));
 
startActivity(pageIntent);

This replaces the Uri in the data field with the page you would like to view. To launch a web search inside the Browser, construct and launch the following Intent:

Intent searchIntent = new Intent();
searchIntent.setAction(Intent.ACTION_WEB_SEARCH);
searchIntent.putExtra(SearchManager.QUERY, STRING_TO_SEARCH);
 
startActivity(searchIntent);

This places the search query you want to execute as an extra in the Intent.

Phone Dialer

The Dialer application may be launched to place a call to a specific number by using the following Intent:

Intent dialIntent = new Intent();
dialIntent.setAction(Intent.ACTION_DIAL);
dialIntent.setData(Uri.Parse("tel:8885551234");
 
startActivity(dialIntent);

This replaces the phone number in the data Uri with the number to call.

Note  This action just brings up the Dialer; it does not place the call. Intent.ACTION_CALL can be used to place the call directly, although Google discourages using this in most cases. Using ACTION_CALL will also require that the android.permission.CALL_PHONE permission be declared in the manifest.

Maps

The Maps application on the device can be launched to display a location or to provide directions between two points. If you know the latitude and longitude of the location you want to map, then create the following Intent:

Intent mapIntent = new Intent();
mapIntent.setAction(Intent.ACTION_VIEW);
mapIntent.setData(Uri.parse("geo:latitude,longitude"));
 
startActivity(mapIntent);

This replaces the coordinates for latitude and longitude of your location. For example, the Uri

"geo:37.422,122.084"

would map the location of Google’s headquarters. If you know the address of the location to display, then create the following Intent:

Intent mapIntent = new Intent();
mapIntent.setAction(Intent.ACTION_VIEW);
mapIntent.setData(Uri.parse("geo:0,0?q=ADDRESS"));
 
startActivity(mapIntent);

This inserts the address you would like to map. For example, the Uri

"geo:0,0?q=1600 Amphitheatre Parkway, Mountain View, CA 94043"

would map the address of Google’s headquarters.

Tip  The Maps application will also accept a Uri that uses the + character to replace spaces in the Address query. If you are having trouble encoding a string with spaces in it, try replacing them with + instead.

If you would like to display directions between two locations, create the following Intent:

Intent mapIntent = new Intent();
mapIntent.setAction(Intent.ACTION_VIEW);
mapIntent.setData(Uri.parse("http://maps.google.com/maps?saddr=lat,lng&daddr=lat,lng"));
 
startActivity(mapIntent);

This inserts the locations for the start and end addresses.

You also can include only one of the parameters if you want to open the Maps application with one address being open-ended. For example, the Uri

"http://maps.google.com/maps?&daddr=37.422,122.084"

would display the Maps application with the destination location prepopulated, but it would allow users to enter their own start address.

E-mail

Any e-mail application on the device can be launched into compose mode by using the following Intent:

Intent mailIntent = new Intent();
mailIntent.setAction(Intent.ACTION_SEND);
mailIntent.setType("message/rfc822");
mailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {"[email protected]"});
mailIntent.putExtra(Intent.EXTRA_CC, new String[] {"[email protected]"});
mailIntent.putExtra(Intent.EXTRA_BCC, new String[] {"[email protected]"});
mailIntent.putExtra(Intent.EXTRA_SUBJECT, "Email Subject");
mailIntent.putExtra(Intent.EXTRA_TEXT, "Body Text");
mailIntent.putExtra(Intent.EXTRA_STREAM, URI_TO_FILE);
 
startActivity(mailIntent);

In this scenario, the action and type fields are the only required pieces to bring up a blank e-mail message. All the remaining extras prepopulate specific fields of the e-mail message. Notice that EXTRA_EMAIL (which fills the To field), EXTRA_CC, and EXTRA_BCC are passed string arrays, even if there is only one recipient to be placed there. File attachments may also be specified in the Intent by using EXTRA_STREAM. The value passed here should be a Uri pointing to the local file to be attached.

If you need to attach more than one file to an e-mail, the requirements change slightly to the following:

Intent mailIntent = new Intent();
mailIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
mailIntent.setType("message/rfc822");
mailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {"[email protected]"});
mailIntent.putExtra(Intent.EXTRA_CC, new String[] {"[email protected]"});
mailIntent.putExtra(Intent.EXTRA_BCC, new String[] {"[email protected]"});
mailIntent.putExtra(Intent.EXTRA_SUBJECT, "Email Subject");
mailIntent.putExtra(Intent.EXTRA_TEXT, "Body Text");
 
ArrayList<Uri> files = new ArrayList<Uri>();
files.add(URI_TO_FIRST_FILE);
files.add(URI_TO_SECOND_FILE);
//...Repeat add() as often as necessary to add all the files you need
mailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, files);
 
startActivity(mailIntent);

Notice that the Intent’s action string is now ACTION_SEND_MULTIPLE. All the primary fields remain the same as before, except for the data that gets added as the EXTRA_STREAM. This example creates a list of Uri elements pointing to the files you want to attach and adds them by using putParcelableArrayListExtra().

It is not uncommon for users to have multiple applications on their devices that can handle this content, so it is usually prudent to wrap either of these constructed Intents with Intent.createChooser() before passing it on to startActivity().

SMS (Messages)

The Messages application can be launched into compose mode for a new SMS message by using the following Intent:

Intent smsIntent = new Intent();
smsIntent.setAction(Intent.ACTION_VIEW);
smsIntent.setType("vnd.android-dir/mms-sms");
smsIntent.putExtra("address", "8885551234");
smsIntent.putExtra("sms_body", "Body Text");
 
startActivity(smsIntent);

As with composing e-mail, you must set the action and type at a minimum to launch the application with a blank message. Including the address and sms_body extras allows the application to prepopulate the recipient (address) and body text (sms_body) of the message.

Neither of these keys has a constant defined in the Android framework, which means that they are subject to change in the future. However, as of this writing, the keys behave as expected on all versions of Android.

Contact Picker

An application may launch the default contact picker, enabling a selection from the user’s Contacts database, by using the following Intent:

static final int REQUEST_PICK = 100;
 
Intent pickIntent = new Intent();
pickIntent.setAction(Intent.ACTION_PICK);
pickIntent.setData(URI_TO_CONTACT_TABLE);
 
startActivityForResult(pickIntent, REQUEST_PICK);

This Intent requires the CONTENT_URI of the Contacts table you are interested in to be passed in the data field. Because of the major changes to the Contacts API in API Level 5 (Android 2.0) and later, this may not be the same Uri if you are supporting versions across that boundary.

For example, to pick a person from the contacts list on a device previous to 2.0, we would pass

android.provider.Contacts.People.CONTENT_URI

However, in 2.0 and later, similar data would be gathered by passing

android.provider.ContactsContract.Contacts.CONTENT_URI

Be sure to consult the API documentation with regards to the contact data you need to access. This activity is also designed to return a Uri representing the selection the user made, so you will want to launch this by using startActivityForResult().

Google Play

Google Play can be launched from within an application to display a specific application’s details page or to run a search for specific keywords. To launch a specific application’s page, use the following Intent:

Intent marketIntent = new Intent();
marketIntent.setAction(Intent.ACTION_VIEW);
marketIntent.setData(Uri.parse("market://details?id=PACKAGE_NAME_HERE");
 
startActivity(marketIntent);

This inserts the unique package name (such as com.adobe.reader) of the application you want to display. If you would like to open Google Play with a search query, use this Intent:

Intent marketIntent = new Intent();
marketIntent.setAction(Intent.ACTION_VIEW);
marketIntent.setData(Uri.parse("market://search?q=SEARCH_QUERY"));
 
startActivity(marketIntent);

This will insert the query string you would like to search on. The search query itself can take one of three main forms:

  • q=<simple text string here>: In this case, the search will be a keyword-style search of the market.
  • q=pname:<package name here>: In this case, the package names will be searched, and only exact matches will be returned.
  • q=pub:<developer name here>: In this case, the developer name field will be searched, and only exact matches will be returned.

7-8. Letting Other Applications Launch Your Application

Problem

You’ve created an application that is absolutely the best at doing a specific task, and you would like to expose an interface for other applications on the device to be able to run your application.

Solution

(API Level 1)

Create an IntentFilter on the activity or service you would like to expose. Then publicly document the actions, data types, and extras that are required to access it properly. Recall that the action, category, and data/type of an Intent can all be used as criteria to match requests to your application. Any additional required or optional parameters should be passed in as extras.

How It Works

Let’s say you have created an application that includes an activity to play a video and will marquee the video’s title at the top of the screen during playback. You want to allow other applications to play video using your application, so we need to define a useful Intent structure for applications to pass in the required data and then create an IntentFilter on the activity in the application’s manifest to match.

This hypothetical activity requires two pieces of data to do its job:

  • The Uri of a video, either local or remote
  • A string representing the video’s title

If the application specializes in a certain type of video, we could define that a generic action (such as ACTION_VIEW) be used and filter more specifically on the data type of the video content we want to handle. Listing 7-23 is an example of how the activity would be defined in the manifest to filter Intents in this manner.

Listing 7-23. AndroidManifest.xml <activity> Element with Data Type Filter

<activity android:name=".PlayerActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="video/h264" />
    </intent-filter>
</activity>

This filter will match any Intent with Uri data that is either explicitly declared as an H.264 video clip or is determined to be H.264 upon inspecting the Uri file. An external application would then be able to call on this activity to play a video by using the following lines of code:

Uri videoFile = A_URI_OF_VIDEO_CONTENT;
Intent playIntent = new Intent(Intent.ACTION_VIEW);
playIntent.setDataAndType(videoFile, "video/h264");
playIntent.putExtra(Intent.EXTRA_TITLE, "My Video");
startActivity(playIntent);

In some cases, it may be more useful for an external application to directly reference this player as the target, regardless of the type of video they want to pass in. In this case, we would create a unique custom action string for Intents to implement. The filter attached to the activity in the manifest would then need to match only the custom action string. See Listing 7-24.

Listing 7-24. AndroidManifest.xml <activity> Element with Custom Action Filter

<activity android:name=".PlayerActivity">
    <intent-filter>
        <action android:name="com.examples.myplayer.PLAY" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

An external application could call on this activity to play a video by using the following code:

Uri videoFile = A_URI_OF_VIDEO_CONTENT;
Intent playIntent = new Intent("com.examples.myplayer.PLAY");
playIntent.setData(videoFile);
playIntent.putExtra(Intent.EXTRA_TITLE, "My Video");
startActivity(playIntent);

Processing a Successful Launch

Regardless of how the Intent is matched to the activity, once it is launched, we want to inspect the incoming Intent for the two pieces of data the activity needs to complete its intended purpose. See Listing 7-25.

Listing 7-25. Activity Inspecting Intent

public class PlayerActivity extends Activity {
 
    public static final String ACTION_PLAY = "com.examples.myplayer.PLAY";
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        //Inspect the Intent that launched us
        Intent incoming = getIntent();
        //Get the video URI from the data field
        Uri videoUri = incoming.getData();
        //Get the optional title extra, if it exists
        String title;
        if(incoming.hasExtra(Intent.EXTRA_TITLE)) {
            title = incoming.getStringExtra(Intent.EXTRA_TITLE);
        } else {
            title = "";
        }
        
        /* Begin playing the video and displaying the title */
    }
 
    /* Remainder of the Activity Code */
 
}

When the activity is launched, the calling Intent can be retrieved with Activity.getIntent(). Because the Uri for the video content is passed in the data field of the Intent, it is unpacked by calling Intent.getData(). The video’s title is an optional value for calling Intents, so we check the extras bundle to first see whether the caller decided to pass it in; if it exists, that value is unpacked from the Intent as well.

Notice that the PlayerActivity in this example did define the custom action string as a constant, but it was not referenced in the sample Intent we constructed to launch the activity. Since this call is coming from an external application, it does not have access to the shared public constants defined in this application.

For this reason, it is also a good idea to reuse the Intent extra keys already in the SDK whenever possible, as opposed to defining new constants. In this example, we chose the standard Intent.EXTRA_TITLE to define the optional extra to be passed instead of creating a custom key for this value.

7-9. Interacting with Contacts

Problem

Your application needs to interact directly with the ContentProvider exposed by Android to the user’s contacts to add, view, change, or remove information from the database.

Solution

(API Level 5)

Use the interface exposed by ContactsContract to access the data. ContactsContract is a vast ContentProvider API that attempts to aggregate the contact information stored in the system from multiple user accounts into a single data store. The result is a maze of Uris, tables, and columns, from which data may be accessed and modified.

The Contact structure is a hierarchy with three tiers: Contacts, RawContacts, and Data:

  • A Contact conceptually represents a person, and it is an aggregation of all RawContacts believed by Android to represent that same person.
  • RawContacts represents a collection of data stored in the device from a specific device account, such as the user’s e-mail address book or Facebook account.
  • Data elements are the specific pieces of information attached to RawContacts, such as an e-mail address, phone number, or postal address.

The complete API has too many combinations and options for us to cover them all here, so consult the SDK documentation for all possibilities. We will investigate how to construct the basic building blocks for performing queries and making changes to the Contacts data set.

How It Works

The Android Contacts API boils down to a complex database with multiple tables and joins. Therefore, the methods for accessing the data are no different than those used to access any other SQLite database from an application.

Listing/Viewing Contacts

Let’s look at an example activity that lists all contact entries in the database and that displays more detail when an item is selected. See Listing 7-26.

Important  In order to display information from the Contacts API in your application, you will need to declare android.permission.READ_CONTACTS in the application manifest.

Listing 7-26. Activity Displaying Contacts

public class ContactsActivity extends FragmentActivity {
 
    private static final int ROOT_ID = 100;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FrameLayout rootView = new FrameLayout(this);
        rootView.setId(ROOT_ID);
        
        setContentView(rootView);
        
        //Create and add a new list fragment
        getSupportFragmentManager().beginTransaction()
            .add(ROOT_ID, ContactsFragment.newInstance())
            .commit();
    }
    
    public static class ContactsFragment extends ListFragment
            implements AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor> {
        
        public static ContactsFragment newInstance() {
            return new ContactsFragment();
        }
        
        private SimpleCursorAdapter mAdapter;
 
        @Override
        public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
 
            // Display all contacts in a ListView
            mAdapter = new SimpleCursorAdapter(getActivity(),
                    android.R.layout.simple_list_item_1, null,
                    new String[] { ContactsContract.Contacts.DISPLAY_NAME },
                    new int[] { android.R.id.text1 },
                    0);
            setListAdapter(mAdapter);
            // Listen for item selections
            getListView().setOnItemClickListener(this);
            
            getLoaderManager().initLoader(0, null, this);
        }
 
        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            // Return all contacts, ordered by name
            String[] projection = new String[] {
                    ContactsContract.Contacts._ID,
                    ContactsContract.Contacts.DISPLAY_NAME
            };
 
            return new CursorLoader(getActivity(),
                    ContactsContract.Contacts.CONTENT_URI,
                    projection, null, null,
                    ContactsContract.Contacts.DISPLAY_NAME);
        }
 
        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            mAdapter.swapCursor(data);
        }
 
        @Override
        public void onLoaderReset(Loader<Cursor> loader) {
            mAdapter.swapCursor(null);
        }
 
        @Override
        public void onItemClick(AdapterView<?> parent, View v,
                int position, long id) {
            final Cursor contacts = mAdapter.getCursor();
            if (contacts.moveToPosition(position)) {
                int selectedId = contacts.getInt(0); // _ID column
                // Gather email data from email table
                Cursor email = getActivity().getContentResolver()
                        .query(ContactsContract.CommonDataKinds.Email.CONTENT_URI,
                                new String[] {ContactsContract.CommonDataKinds.Email.DATA},
                                ContactsContract.Data.CONTACT_ID
                                        + " = " + selectedId,
                                null, null);
                // Gather phone data from phone table
                Cursor phone = getActivity().getContentResolver()
                        .query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                                new String[] {ContactsContract.CommonDataKinds.Phone.NUMBER},
                                ContactsContract.Data.CONTACT_ID
                                        + " = " + selectedId,
                                null, null);
                // Gather addresses from address table
                Cursor address = getActivity().getContentResolver()
                        .query(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI,
                                new String[] {ContactsContract.CommonDataKinds
                                        .StructuredPostal.FORMATTED_ADDRESS},
                                ContactsContract.Data.CONTACT_ID
                                        + " = " + selectedId,
                                null, null);
 
                // Build the dialog message
                StringBuilder sb = new StringBuilder();
                sb.append(email.getCount() + " Emails ");
                if (email.moveToFirst()) {
                    do {
                        sb.append("Email: " + email.getString(0));
                        sb.append(' '),
                    } while (email.moveToNext());
                    sb.append(' '),
                }
                sb.append(phone.getCount() + " Phone Numbers ");
                if (phone.moveToFirst()) {
                    do {
                        sb.append("Phone: " + phone.getString(0));
                        sb.append(' '),
                    } while (phone.moveToNext());
                    sb.append(' '),
                }
                sb.append(address.getCount() + " Addresses ");
                if (address.moveToFirst()) {
                    do {
                        sb.append("Address: "
                                + address.getString(0));
                    } while (address.moveToNext());
                    sb.append(' '),
                }
 
                AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
                builder.setTitle(contacts.getString(1)); // Display name
                builder.setMessage(sb.toString());
                builder.setPositiveButton("OK", null);
                builder.create().show();
 
                // Finish temporary cursors
                email.close();
                phone.close();
                address.close();
            }
        }
    }
}

As you can see, referencing all the tables and columns in this API can result in very verbose code. All of the references to Uri elements, tables, and columns in this example are inner classes stemming off of ContactsContract. It is important to verify when interacting with the Contacts API that you are referencing the proper classes, as any Contacts classes not stemming from ContactsContract are deprecated and incompatible.

When the fragment containing the UI for the activity is created, we construct a simple query on the core Contacts table through a CursorLoader referencing Contacts.CONTENT_URI, requesting only the columns we need to wrap the cursor in a ListAdapter. The resulting cursor is displayed in a list on the user interface. The example leverages the convenience behavior of ListFragment to provide a ListView as the content view so that we do not have to manage these components.

At this point, the user can scroll through all the contact entries on the device, and can tap one to get more information. When a list item is selected, the ID value of that particular contact is recorded and the application goes out to the other ContactsContract.Data tables to gather more-detailed information. Notice that the information for this single contact is spread across multiple tables (e-mails in an e-mail table, phone numbers in a phone table, and so on), requiring multiple queries to obtain.

Each CommonDataKinds table has a unique CONTENT_URI for the query to reference, as well as a unique set of column aliases for requesting the data. All of the rows in these data tables are linked to the specific contact through the Data.CONTACT_ID, so each cursor asks to return only rows where the values match.

With all the data collected for the selected contact, we iterate through the results to display in a dialog to the user. Because the data in these tables are an aggregation of multiple sources, it is not uncommon for all of these queries to return multiple results. With each cursor, we display the number of results, and then append each value included. When all the data is composed, the dialog is created and shown to the user.

As a final step, all temporary and unmanaged cursors are closed as soon as they are no longer required.

Running the Application

The first thing that you may notice when running this application on a device that has any number of accounts set up is that the list seems insurmountably long, certainly much longer than what shows up when running the Contacts application bundled with the device. The Contacts API allows for the storage of grouped entries that may be hidden from the user and are used for internal purposes. Gmail often uses this to store incoming e-mail addresses for quick access, even if an address is not associated with a true contact.

In the next example, we will show how to filter this list, but for now marvel at the amount of data truly stored in the Contacts table.

Changing/Adding Contacts

Now let’s look at an example activity that manipulates the data for a specific contact. See Listing 7-27.

Listing 7-27. Activity Writing to Contacts API

public class ContactsEditActivity extends FragmentActivity {
 
    private static final String TEST_EMAIL = "[email protected]";
    private static final int ROOT_ID = 100;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FrameLayout rootView = new FrameLayout(this);
        rootView.setId(ROOT_ID);
        
        setContentView(rootView);
        
        //Create and add a new list fragment
        getSupportFragmentManager().beginTransaction()
            .add(ROOT_ID, ContactsEditFragment.newInstance())
            .commit();
    }
    
    public static class ContactsEditFragment extends ListFragment implements
            AdapterView.OnItemClickListener,
            DialogInterface.OnClickListener,
            LoaderManager.LoaderCallbacks<Cursor> {
 
        public static ContactsEditFragment newInstance() {
            return new ContactsEditFragment();
        }
        
        private SimpleCursorAdapter mAdapter;
        private Cursor mEmail;
        private int selectedContactId;
 
        @Override
        public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
 
            // Display all contacts in a ListView
            mAdapter = new SimpleCursorAdapter(getActivity(),
                    android.R.layout.simple_list_item_1, null,
                    new String[] { ContactsContract.Contacts.DISPLAY_NAME },
                    new int[] { android.R.id.text1 },
                    0);
            setListAdapter(mAdapter);
            // Listen for item selections
            getListView().setOnItemClickListener(this);
            
            getLoaderManager().initLoader(0, null, this);
        }
 
        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            // Return all contacts, ordered by name
            String[] projection = new String[] { ContactsContract.Contacts._ID,
                    ContactsContract.Contacts.DISPLAY_NAME };
            //List only contacts visible to the user
            return new CursorLoader(getActivity(),
                    ContactsContract.Contacts.CONTENT_URI,
                    projection, ContactsContract.Contacts.IN_VISIBLE_GROUP+" = 1",
                    null,
                    ContactsContract.Contacts.DISPLAY_NAME);
        }
        
        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            mAdapter.swapCursor(data);
        }
 
        @Override
        public void onLoaderReset(Loader<Cursor> loader) {
            mAdapter.swapCursor(null);
        }
        
        @Override
        public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
            final Cursor contacts = mAdapter.getCursor();
            if (contacts.moveToPosition(position)) {
                selectedContactId = contacts.getInt(0); // _ID column
                // Gather email data from email table
                String[] projection = new String[] {
                        ContactsContract.Data._ID,
                        ContactsContract.CommonDataKinds.Email.DATA };
                mEmail = getActivity().getContentResolver().query(
                        ContactsContract.CommonDataKinds.Email.CONTENT_URI,
                        projection,
                        ContactsContract.Data.CONTACT_ID + " = " + selectedContactId,
                        null,
                        null);
 
                AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
                builder.setTitle("Email Addresses");
                builder.setCursor(mEmail, this, ContactsContract.CommonDataKinds.Email.DATA);
                builder.setPositiveButton("Add", this);
                builder.setNegativeButton("Cancel", null);
                builder.create().show();
            }
        }
 
        @Override
        public void onClick(DialogInterface dialog, int which) {
            //Data must be associated with a RAW contact, retrieve the first raw ID
            Cursor raw = getActivity().getContentResolver().query(
                    ContactsContract.RawContacts.CONTENT_URI,
                    new String[] { ContactsContract.Contacts._ID },
                    ContactsContract.Data.CONTACT_ID + " = " + selectedContactId, null, null);
            if(!raw.moveToFirst()) {
                return;
            }
 
            int rawContactId = raw.getInt(0);
            ContentValues values = new ContentValues();
            switch(which) {
                case DialogInterface.BUTTON_POSITIVE:
                    //User wants to add a new email
                    values.put(ContactsContract.CommonDataKinds.Email.RAW_CONTACT_ID, rawContactId);
                    values.put(ContactsContract.Data.MIMETYPE, ContactsContract
                            .CommonDataKinds.Email.CONTENT_ITEM_TYPE);
                    values.put(ContactsContract.CommonDataKinds.Email.DATA, TEST_EMAIL);
                    values.put(ContactsContract.CommonDataKinds.Email.TYPE,
                            ContactsContract.CommonDataKinds.Email.TYPE_OTHER);
                    getActivity().getContentResolver()
                            .insert(ContactsContract.Data.CONTENT_URI, values);
                    break;
                default:
                    //User wants to edit selection
                    values.put(ContactsContract.CommonDataKinds.Email.DATA, TEST_EMAIL);
                    values.put(ContactsContract.CommonDataKinds.Email.TYPE,
                            ContactsContract.CommonDataKinds.Email.TYPE_OTHER);
                    getActivity().getContentResolver()
                            .update(ContactsContract.Data.CONTENT_URI, values,
                                    ContactsContract.Data._ID+" = "+mEmail.getInt(0), null);
                    break;
            }
 
            //Don't need the email cursor anymore
            mEmail.close();
        }
    }
}

Important  In order to interact with the Contacts API in your application, you must declare android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS in the application manifest.

In this example, we start out as before, performing a query for all entries in the Contacts database. This time, we provide a single selection criterion:

ContactsContract.Contacts.IN_VISIBLE_GROUP+" = 1"

The effect of this line is to limit the returned entries to only those that are visible to the user through the Contacts user interface. This will (drastically, in some cases) reduce the size of the list displayed in the activity and will make it more closely match the list displayed in the Contacts application.

When the user selects a contact from this list, a dialog is displayed with a list of all the e-mail entries attached to that contact. If a specific address is selected from the list, that entry is edited; if the Add button is pressed, a new e-mail address entry is added. For the purposes of simplifying the example, we do not provide an interface to enter a new e-mail address. Instead, a constant value is inserted, either as a new record or as an update to the selected one.

Data elements, such as e-mail addresses, can be associated only with a RawContact. Therefore, when we want to add a new e-mail address, we must obtain the ID of one of the RawContacts represented by the higher-level contact that the user selected. For the purposes of the example, we aren’t terribly interested in which one, so we retrieve the ID of the first RawContact that matches. This value is required only for doing an insert, because the update references the distinct row ID of the e-mail record already present in the table.

The Uri provided in CommonDataKinds that was used as an alias to read this data cannot be used to make updates and changes. Inserts and updates must be called directly on the ContactsContract.Data Uri. What this means (besides referencing a different Uri in the operation method) is that an extra piece of metadata, the MIMETYPE, must also be specified. Without setting the MIMETYPE field for inserted data, subsequent queries made may not recognize it as a contact’s e-mail address.

Aggregating at Work

Because this example updates records by adding or editing e-mail addresses with the same value, it offers a unique opportunity to see Android’s aggregation operations in real time. As you run this example application, you may notice that adding or editing contacts to give them the same e-mail address often triggers Android to start thinking that previously separate contacts are now the same people. Even in this sample application, as the managed query attached to the core Contacts table updates, notice that certain contacts will disappear as they become aggregated together.

Note  Contact aggregation behavior is not implemented fully on the Android emulator. To see this effect in full, you will need to run the code on a real device.

Maintaining a Reference

The Android Contacts API introduces one more concept that can be important, depending on the scope of the application. Because of this aggregation process that occurs, the distinct row ID that refers to a contact becomes quite volatile; a certain contact may receive a new _ID when it is aggregated together with another one.

If your application requires a long-standing reference to a specific contact, it is recommended that your application persist the ContactsContract.Contacts.LOOKUP_KEY instead of the row ID. When querying for a contact by using this key, a special Uri is also provided as the ContactsContract.Contacts.CONTENT_LOOKUP_URI. Using these values to query records over the long term will protect your application from getting confused by the automatic aggregation process.

7-10. Reading Device Media and Documents

Problem

Your application needs to import a user-selected document item (such as a text file, audio, video, or image) for display or playback.

Solution

(API Level 1)

Use an implicit Intent targeted with Intent.ACTION_GET_CONTENT to bring up a picker interface from a specific application. Firing this Intent with a matching content type for the media of interest (audio, video, or image are the most common) will present the user with a picker interface to select an item, and the Intent result will include a content Uri pointing to the selection the user made.

(API Level 19)

Use an implicit Intent targeted with Intent.ACTION_OPEN_DOCUMENT to bring up the system’s document picker interface. This is a single common interface where all applications that support the requested content type will list the items the user may select. The content that populates this interface comes from applications that expose DocumentProvider for the requested type. These provider elements can come from the system or other applications. We will look at how to create one of your own later in this chapter.

Tip  ACTION_GET_CONTENT can still be used with API Level 19+, and it will launch the standard document picker into a compatibility mode that includes the newer integrated providers along with the options to pick a single application’s picker interface.

How It Works

Let’s take a look at this technique used in the context of an example activity. See Listings 7-28 and 7-29.

Listing 7-28. res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/imageButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Images" />
    <Button
         android:id="@+id/videoButton"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="Video" />
    <Button
        android:id="@+id/audioButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Audio" />
</LinearLayout>

Listing 7-29. Activity to Pick Media

public class MediaActivity extends Activity implements View.OnClickListener {
 
    private static final int REQUEST_AUDIO = 1;
    private static final int REQUEST_VIDEO = 2;
    private static final int REQUEST_IMAGE = 3;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        Button images = (Button)findViewById(R.id.imageButton);
        images.setOnClickListener(this);
        Button videos = (Button)findViewById(R.id.videoButton);
        videos.setOnClickListener(this);
        Button audio = (Button)findViewById(R.id.audioButton);
        audio.setOnClickListener(this);
        
    }
 
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        
        if(resultCode == Activity.RESULT_OK) {
            //Uri to user selection returned in the Intent
            Uri selectedContent = data.getData();
            
            if(requestCode == REQUEST_IMAGE) {
                //Pass an InputStream to BitmapFactory
            }
            if(requestCode == REQUEST_VIDEO) {
                //Pass the Uri or a FileDescriptor to MediaPlayer
            }
            if(requestCode == REQUEST_AUDIO) {
                //Pass the Uri or a FileDescriptor to MediaPlayer
            }
        }
    }
    
    @Override
    public void onClick(View v) {
        Intent intent = new Intent();
        //Use the proper Intent action
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
        } else {
            intent.setAction(Intent.ACTION_GET_CONTENT);
        }
        //Only return files to which we can open a stream
        intent.addCategory(Intent.CATEGORY_OPENABLE);
 
        //Set correct MIME type and launch
        switch(v.getId()) {
        case R.id.imageButton:
            intent.setType("image/*");
            startActivityForResult(intent, REQUEST_IMAGE);
            return;
        case R.id.videoButton:
            intent.setType("video/*");
            startActivityForResult(intent, REQUEST_VIDEO);
            return;
        case R.id.audioButton:
            intent.setType("audio/*");
            startActivityForResult(intent, REQUEST_AUDIO);
            return;
        default:
            return;
        }
    }
}

This example has three buttons for the user to press, each targeting a specific type of media. When the user presses any one of these buttons, an Intent with the appropriate action for the platform level is fired to the system. On devices running 4.4 and later, this will display the system document picker. Previous devices will launch the proper picker activity from the necessary application, showing a chooser if multiple applications can handle the content type. We have also included CATEGORY_OPENABLE to this Intent, which indicates to the system that only items our application can open a stream to will be displayed in the picker.

If the user selects a valid item, a content Uri pointing to that item is returned in the result Intent with a status of RESULT_OK. If the user cancels or otherwise backs out of the picker, the status will be RESULT_CANCELED and the Intent’s data field will be null.

With the Uri of the media received, the application is now free to play or display the content as deemed appropriate. Classes such as MediaPlayer and VideoView will take a Uri directly to play media content, while most others will take either an InputStream or a FileDescriptor reference. Both of these can be obtained from the Uri by using ContentResolver.openInputStream() and ContentResolver.openFileDescriptor(), respectively.

7-11. Saving Device Media and Documents

Problem

Your application would like to create new documents or media and insert them into the device’s global providers so that they are visible to all applications.

Solution

(API Level 1)

Utilize the ContentProvider interface exposed by MediaStore to perform inserts or media content. In addition to the media content itself, this interface allows you to insert metadata to tag each item, such as a title, description, or time created. The result of the ContentProvider insert operation is a Uri that the application may use as a destination for the new media.

(API Level 19)

On Android 4.4+ devices, we can also trigger an implicit Intent targeted with Intent.ACTION_CREATE_DOCUMENT to save a new document in any of the device’s registered DocumentProvider instances. This can be any type of document content, including (but not restricted to) media files. However, it is not meant to supersede MediaStore,  which is still the best method for saving directly to the system’s core ContentProvider. If you instead need to involve the user more directly in saving the content (including media), the document framework is a better path here.

How It Works

Let’s take a look at an example of inserting an image or video clip into MediaStore. See Listings 7-30 and 7-31.

Listing 7-30. res/layout/save.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    <Button
        android:id="@+id/imageButton"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Images" />
    <Button
        android:id="@+id/videoButton"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Video" />
    <Button
        android:id="@+id/textButton"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Text Document" />
 
</LinearLayout>

Listing 7-31. Activity Saving Data in the MediaStore

public class StoreActivity extends Activity implements View.OnClickListener {
 
    private static final int REQUEST_CAPTURE = 100;
    private static final int REQUEST_DOCUMENT = 101;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.save);
        
        Button images = (Button) findViewById(R.id.imageButton);
        images.setOnClickListener(this);
        Button videos = (Button) findViewById(R.id.videoButton);
        videos.setOnClickListener(this);
 
        //We can only create new documents above API Level 19
        Button text = (Button) findViewById(R.id.textButton);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            text.setOnClickListener(this);
        } else {
            text.setVisibility(View.GONE);
        }
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CAPTURE && resultCode == Activity.RESULT_OK) {
            Toast.makeText(this, "All Done!", Toast.LENGTH_SHORT).show();
        }
        if (requestCode == REQUEST_DOCUMENT && resultCode == Activity.RESULT_OK) {
            //Once the user has selected where to save the new document,
            // we can write the contents into it
            Uri document = data.getData();
            writeDocument(document);
        }
    }
    
    private void writeDocument(Uri document) {
        try {
            ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(document, "w");
            FileOutputStream out = new FileOutputStream(pfd.getFileDescriptor());
            //Construct some content for our file
            StringBuilder sb = new StringBuilder();
            sb.append("Android Recipes Log File:");
            sb.append(" ");
            sb.append("Last Written at: ");
            sb.append(DateFormat.getLongDateFormat(this).format(new Date()));
 
            out.write(sb.toString().getBytes());
            
            // Let the document provider know you're done by closing the stream.
            out.flush();
            out.close();
            // Close our file handle
            pfd.close();
        } catch (FileNotFoundException e) {
            Log.w("AndroidRecipes", e);
        } catch (IOException e) {
            Log.w("AndroidRecipes", e);
        }
    }
    
    @Override
    public void onClick(View v) {
        ContentValues values;
        Intent intent;
        Uri storeLocation;
        final long nowMillis = System.currentTimeMillis();
        
        switch(v.getId()) {
        case R.id.imageButton:
            //Create any metadata for image
            values = new ContentValues(5);
            values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, nowMillis);
            values.put(MediaStore.Images.ImageColumns.DATE_ADDED, nowMillis / 1000);
            values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, nowMillis / 1000);
            values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, "Android Recipes Image Sample");
            values.put(MediaStore.Images.ImageColumns.TITLE, "Android Recipes Image Sample");
            
            //Insert metadata and retrieve Uri location for file
            storeLocation = getContentResolver()
                    .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            //Start capture with new location as destination
            intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            intent.putExtra(MediaStore.EXTRA_OUTPUT, storeLocation);
            startActivityForResult(intent, REQUEST_CAPTURE);
            return;
        case R.id.videoButton:
            //Create any metadata for video
            values = new ContentValues(7);
            values.put(MediaStore.Video.VideoColumns.DATE_TAKEN, nowMillis);
            values.put(MediaStore.Video.VideoColumns.DATE_ADDED, nowMillis / 1000);
            values.put(MediaStore.Video.VideoColumns.DATE_MODIFIED, nowMillis / 1000);
            values.put(MediaStore.Video.VideoColumns.DISPLAY_NAME, "Android Recipes Video Sample");
            values.put(MediaStore.Video.VideoColumns.TITLE, "Android Recipes Video Sample");
            values.put(MediaStore.Video.VideoColumns.ARTIST, "Yours Truly");
            values.put(MediaStore.Video.VideoColumns.DESCRIPTION, "Sample Video Clip");
            
            //Insert metadata and retrieve Uri location for file
            storeLocation = getContentResolver()
                    .insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
            //Start capture with new location as destination
            intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
            intent.putExtra(MediaStore.EXTRA_OUTPUT, storeLocation);
            startActivityForResult(intent, REQUEST_CAPTURE);
            return;
        case R.id.textButton:
            //Create a new document
            intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            
            //This is a text document
            intent.setType("text/plain");
            //Optional title to pre-set on document
            intent.putExtra(Intent.EXTRA_TITLE, "Android Recipes");
            startActivityForResult(intent, REQUEST_DOCUMENT);
        default:
            return;
        }
    }
}

Note  Because this example interacts with the Camera hardware, you should run it on a real device to get the full effect. Emulators will execute the code appropriately, but without real hardware, the example is less interesting.

In this example, when the user clicks the Image or Video button, metadata associated with the media are inserted into a ContentValues instance. Some of the more common metadata columns to both image and video are the following:

  • TITLE: String value for the content title. Displayed in the gallery applications as the content name.
  • DISPLAY_NAME: Name displayed in most selection interfaces such as the system document picker.
  • DATE_TAKEN: Integer value describing the date the media item was captured. Note this value is in milliseconds.
  • DATE_ADDED: Integer value describing when the media was added to MediaStore. Note this value is in seconds, not milliseconds.
  • DATE_MODIFIED: Integer value describing the last change to the media. This is used to sort items in the system document picker. Note this value is also in seconds.

The ContentValues are then inserted into the MediaStore by using the appropriate CONTENT_URI reference. Notice that the metadata are inserted before the media item itself is actually captured. The return value from a successful insert is a fully qualified Uri that the application may use as the destination for the media content.

In the previous example, we are using the simplified methods from Chapter 5 of capturing audio and video by requesting that the system applications handle this process. Recall from Chapter 5 that both the audio and video capture Intent can be passed with an extra, declaring the destination for the result. This is where we pass the Uri that was returned from the insert.

Upon a successful return from the capture activity, there is nothing more for the application to do. The external application has saved the captured image or video into the location referenced by our MediaStore insert. This data is now visible to all applications, including the system’s Gallery application.

Creating Documents

Notice the third button, labeled Text Document, is visible and enabled only if we are running on a device with Android 4.4 or later. If the user clicks this button, we construct an Intent request using ACTION_CREATE_DOCUMENT to launch the system’s document interface. However, in this case, the interface is launched, allowing the user to select where the new file should be saved (that is, which provider application) and what its title should be. Along with this request, we set the MIME type to indicate the document type we want to create, which is plain text in our example. Finally, we can suggest a title by passing EXTRA_TITLE along with the Intent, but the user is always given the right to change it later.

Once the user has selected where to save the new document, we are given a content Uri in onActivityResult() and we can open a stream and write our document’s data to storage. The writeDocument() method of the example opens a FileDescriptor from the Uri and writes some basic text content into the new document. Closing the stream and descriptor signal to the owning provider that the document update is complete.

Tip  ACTION_CREATE_DOCUMENT is used to make a new document that you want to save. For editing an existing document in place, use ACTION_OPEN_DOCUMENT from the previous example to obtain a working Uri to an existing file. Keep in mind, however, that not all providers support writing. You will need to check the permissions of the Uri you are given before attempting to edit a document received in this way.

7-12. Reading Messaging Data

Problem

You need to query the ContentProvider of locally saved information on the device for sent and received SMS/MMS messages.

Solution

(API Level 19)

Use the contract interface exposed via the Telephony framework. The inner classes of Telephony define all the Uris and data columns used to read SMS messages, MMS messages, and additional metadata.

Important  You must request android.permission.READ_SMS in the manifest in order to gain read access to the Telephony provider.

The Telephony provider exposes an interface for the following blocks of data:

  • Telephony.Sms: Contains the message content and recipient/delivery metadata for all SMS messages.
  • Telephony.Mms: Contains the message content and recipient/delivery metadata for all MMS messages.
  • Telephony.MmsSms: Contains the combined messages for SMS and MMS. Also includes custom Uris for requesting a list of conversations, drafts, and searching messages.
  • Telephony.Threads: Provides additional metadata about conversations, such as the message count and read status of the conversation thread.

Text-based SMS messages are relatively straightforward, with their entire content housed within a few columns in the Telephony.Sms tables. Even if a message has multiple recipients, those are broken up into multiple messages with the same text content. MMS messages, however, are composed of multiple parts that are all stored separately in individual tables:

  • Mms.Addr: Contains metadata about all the recipients involved in each MMS message. Each message can have a unique group of recipients.
  • Mms.Part: Contains the contents of each piece included in the MMS message. The message text is stored as one part with any image, video, or other attachments stored as additional pieces. A MIME string designates the content type of each part.

Displaying a single SMS message can be done with a single query to the Telephony.Sms content Uri. Displaying a single MMS message, however, requires iterating through all of these subparts in Telephony.Mms to collect the data we need.

Tip  SMS/MMS data will be present only on a device that has telephony hardware, so you will likely want to add a <uses-feature android:name="android.hardware.telephony"/> declaration to the manifest to filter out devices that don’t have the proper capabilities.

WRITING TO THE TELEPHONY PROVIDER

Writing message data and metadata into the Telephony provider has some special rules associated with it. While any application can request the WRITE_SMS permission (and have it granted), only the application selected by the user as the Default Messaging Application in Settings will be allowed to write data into the content provider.

Nondefault applications sending SMS messages by using the mechanisms we described in Chapter 4 will have their message contents written into the provider automatically by the framework, but the default application (as the sole application with provider access) will be responsible for writing its own content directly.

In order for a messaging application to be considered for selection as the default, the following criteria must exist in the application’s manifest:

  • A broadcast receiver registered for the android.provider.Telephony.SMS_DELIVER action to receive new SMS messages.
  • A broadcast receiver registered for the android.provider.Telephony.WAP_PUSH_DELIVER action to receive new MMS messages.
  • An activity filtering for the android.intent.action.SENDTO action to send new SMS/MMS messages.
  • A service filtering for the android.intent.action.RESPOND_VIA_MESSAGE action to send quick response messages to incoming callers.

If you are writing a messaging application that must have write access to the provider, the same Uri and column structure defined in the Telephony contract that we discuss in this recipe can be used to insert(), update(), or delete() the provider contents as well.

How It Works

In this example, we will create a simple messaging application that reads conversation data from the Telephony provider and displays it in a list. Let’s start with the code to query and parse the data coming from the provider. In Listing 7-32, we’ve created a custom AsyncTaskLoader implementation that allows us to query the provider on a background thread and return the result easily to the UI.

Listing 7-32. Loader for Conversation Data

public class ConversationLoader extends AsyncTaskLoader<List<MessageItem>> {
    
    public static final String[] PROJECTION = new String[] {
        //Determine if message is SMS or MMS
        MmsSms.TYPE_DISCRIMINATOR_COLUMN,
        //Base item ID
        BaseColumns._ID,
        //Conversation (thread) ID
        Conversations.THREAD_ID,
        //Date values
        Sms.DATE,
        Sms.DATE_SENT,
        // For SMS only
        Sms.ADDRESS,
        Sms.BODY,
        Sms.TYPE,
        // For MMS only
        Mms.SUBJECT,
        Mms.MESSAGE_BOX
    };
    
    //Thread ID of the conversation we are loading
    private long mThreadId;
    //This device's number
    private String mDeviceNumber;
    
    public ConversationLoader(Context context) {
        this(context, -1);
    }
    
    public ConversationLoader(Context context, long threadId) {
        super(context);
        mThreadId = threadId;
        //Obtain the phone number of this device, if available
        TelephonyManager manager =
                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
        mDeviceNumber = manager.getLine1Number();
    }
 
    @Override
    protected void onStartLoading() {
        //Reload on every init request
        forceLoad();
    }
    
    @Override
    public List<MessageItem> loadInBackground() {
        Uri uri;
        String[] projection;
        if (mThreadId < 0) {
            //Load all conversations
            uri = MmsSms.CONTENT_CONVERSATIONS_URI;
            projection = null;
        } else {
            //Load just the requested thread
            uri = ContentUris.withAppendedId(MmsSms.CONTENT_CONVERSATIONS_URI, mThreadId);
            projection = PROJECTION;
        }
        
        Cursor cursor = getContext().getContentResolver().query(
                uri,
                projection,
                null,
                null,
                null);
        
        return MessageItem.parseMessages(getContext(), cursor, mDeviceNumber);
    }
}

AysncTaskLoader is fairly simply to customize. You just need to provide an implementation of loadInBackground() to do the interesting work, and include some logic in onStartLoading() to get the process running. In the framework, results are usually cached, and forceLoad() is called only if the content has changed, but for simplicity we are loading data from the provider on every request.

Our ConversationLoader will be used to obtain two message lists: a list of all conversations present, and a list of all the messages for a selected conversation (or thread). So when ConversationLoader is instantiated, the ID of the conversation thread is either passed in or ignored to determine the output results. We also obtain the phone number of our device from TelephonyManager for use later. This step is not integral to reading the provider, but will help us clean up the display later.

Important  When using TelephonyManager to get the device information, your application must also declare android.permission.READ_PHONE_STATE in the manifest.

In both request modes, we are making a query of the MmsSms.CONTENT_CONVERSATIONS_URI in the combined message table. This Uri is convenient for getting a conversation overview because it will return a list of all the known conversation threads by returning the latest message in each thread. This makes it easy to display the results directly to the user.

When listing all the conversations, we don’t need to provide a customized projection, which will return all the columns. However, when looking at a specific thread, we pass in a specific column subset to inspect. This is mainly so we can obtain the MmsSms.TYPE_DISCRIMINATOR_COLUMN value, which tells us whether each message is SMS or MMS. This column is not available for the main conversations list, and it also isn’t returned by default for a null projection.

SORTING COMBINED RESULTS

It is likely that you may want to sort the results by date for these queries. A common implementation would be to use the ordering clause in the provider query to sort the results returned. However, with a combined SMS/MMS query like what we have done here, this is not straightforward. The DATE and DATE_SENT fields of SMS messages present their timestamps in elapsed milliseconds from epoch, while those same fields for MMS messages show timestamps in elapsed seconds from epoch.

The simplest method for sorting combined results is to normalize the timestamps when parsing out into a model (such as MessageItem), and then use the sorting features of Collections to sort the resulting object list.

After we have successfully queried the provider, we want to parse the contents into a common model object that we can easily display in a list. To do this, we pass the result Cursor to a factory method in a MessageItem class we’ve created, which you can see in Listing 7-33.

Listing 7-33. MessageItem Model and Parsing

public class MessageItem {
    /* Message Type Identifiers */
    private static final String TYPE_SMS = "sms";
    private static final String TYPE_MMS = "mms";
    
    static final String[] MMS_PROJECTION = new String[] {
        //Base item ID
        BaseColumns._ID,
        //MIME Type of the content for this part
        Mms.Part.CONTENT_TYPE,
        //Text content of a text/plain part
        Mms.Part.TEXT,
        //Path to binary content of a nontext part
        Mms.Part._DATA
    };
    
    /* Message Id */
    public long id;
    /* Thread (Conversation) Id */
    public long thread_id;
    /* Address string of message */
    public String address;
    /* Body string of message */
    public String body;
    /* Whether this message was sent or received on this device */
    public boolean incoming;
    /* MMS image attachment */
    public Uri attachment;
    
    /*
     * Construct a list of messages from the Cursor data
     * queried by the Loader
     */
    public static List<MessageItem> parseMessages(Context context, Cursor cursor,
            String myNumber) {
 
        List<MessageItem> messages = new ArrayList<MessageItem>();
        if (!cursor.moveToFirst()) {
            return messages;
        }
        //Parse each message based on the type identifiers
        do {
            String type = getMessageType(cursor);
            if (TYPE_SMS.equals(type)) {
                MessageItem item = parseSmsMessage(cursor);
                messages.add(item);
            } else if (TYPE_MMS.equals(type)) {
                MessageItem item = parseMmsMessage(context, cursor, myNumber);
                messages.add(item);
            } else {
                Log.w("TelephonyProvider", "Unknown Message Type");
            }
        } while (cursor.moveToNext());
        cursor.close();
        
        return messages;
    }
    
    /*
     * Read message type, if present in Cursor; otherwise
     * infer it from the column values present in the Cursor
     */
    private static String getMessageType(Cursor cursor) {
        int typeIndex = cursor.getColumnIndex(MmsSms.TYPE_DISCRIMINATOR_COLUMN);
        if (typeIndex < 0) {
            //Type column not in projection, use another discriminator
            String cType = cursor.getString(cursor.getColumnIndex(Mms.CONTENT_TYPE));
            //If a content type is present, this is an MMS message
            if (cType != null) {
                return TYPE_MMS;
            } else {
                return TYPE_SMS;
            }
        } else {
            return cursor.getString(typeIndex);
        }
    }
    
    /*
     * Parse out a MessageItem with contents from an SMS message
     */
    private static MessageItem parseSmsMessage(Cursor data) {
        MessageItem item = new MessageItem();
        item.id = data.getLong(data.getColumnIndexOrThrow(BaseColumns._ID));
        item.thread_id = data.getLong(data.getColumnIndexOrThrow(Conversations.THREAD_ID));
        
        item.address = data.getString(data.getColumnIndexOrThrow(Sms.ADDRESS));
        item.body = data.getString(data.getColumnIndexOrThrow(Sms.BODY));
        item.incoming = isIncomingMessage(data, true);
        return item;
    }
    
    /*
     * Parse out a MessageItem with contents from an MMS message
     */
    private static MessageItem parseMmsMessage(Context context, Cursor data, String myNumber) {
        MessageItem item = new MessageItem();
        item.id = data.getLong(data.getColumnIndexOrThrow(BaseColumns._ID));
        item.thread_id = data.getLong(data.getColumnIndexOrThrow(Conversations.THREAD_ID));
        
        item.incoming = isIncomingMessage(data, false);
        
        long _id = data.getLong(data.getColumnIndexOrThrow(BaseColumns._ID));
 
        //Query the address information for this message
        Uri addressUri = Uri.withAppendedPath(Mms.CONTENT_URI, _id + "/addr");
        Cursor addr = context.getContentResolver().query(
                addressUri,
                null,
                null,
                null,
                null);
        HashSet<String> recipients = new HashSet<String>();
        while (addr.moveToNext()) {
            String address = addr.getString(addr.getColumnIndex(Mms.Addr.ADDRESS));
            //Don't add our own number to the displayed list
            if (myNumber == null || !address.contains(myNumber)) {
                recipients.add(address);
            }
        }
        item.address = TextUtils.join(", ", recipients);
        addr.close();
        
        //Query all the MMS parts associated with this message
        Uri messageUri = Uri.withAppendedPath(Mms.CONTENT_URI, _id + "/part");
        Cursor inner = context.getContentResolver().query(
                messageUri,
                MMS_PROJECTION,
                Mms.Part.MSG_ID + " = ?",
                new String[] {String.valueOf(data.getLong(data.getColumnIndex(Mms._ID)))},
                null);
 
        while(inner.moveToNext()) {
            String contentType = inner.getString(inner.getColumnIndex(Mms.Part.CONTENT_TYPE));
            if (contentType == null) {
                continue;
            } else if (contentType.matches("image/.*")) {
                //Find any part that is an image attachment
                long partId = inner.getLong(inner.getColumnIndex(BaseColumns._ID));
                item.attachment = Uri.withAppendedPath(Mms.CONTENT_URI, "part/" + partId);
            } else if (contentType.matches("text/.*")) {
                //Find any part that is text data
                item.body = inner.getString(inner.getColumnIndex(Mms.Part.TEXT));
            }
        }
        
        inner.close();
        return item;
    }
    
    /*
     * Validate if the message is incoming or outgoing by the
     * type/box information listed in the provider
     */
    private static boolean isIncomingMessage(Cursor cursor, boolean isSms) {
        int boxId;
        if (isSms) {
            boxId = cursor.getInt(cursor.getColumnIndexOrThrow(Sms.TYPE));
            return (boxId == TextBasedSmsColumns.MESSAGE_TYPE_INBOX ||
                    boxId == TextBasedSmsColumns.MESSAGE_TYPE_ALL) ?
                    true : false;
        } else {
            boxId = cursor.getInt(cursor.getColumnIndexOrThrow(Mms.MESSAGE_BOX));
            return (boxId == Mms.MESSAGE_BOX_INBOX || boxId == Mms.MESSAGE_BOX_ALL) ?
                    true : false;
        }
    }
}

The MessageItem itself is a standard placeholder object for the message identifiers, name, text content, and image attachment (for MMS messages). Inside parseMessages(), we iterate through the Cursor data and construct a new MessageItem from each row. SMS and MMS are parsed differently, so we must first determine the message type. When the TYPE_DISCRIMINATOR_COLUMN is present, this is simple and we just check the value. In other cases, we can infer the type based on the column entries for each message.

Parsing an SMS message is straightforward, as we just need to read the ID, thread ID, address, body, and incoming status as columns directly from the main Cursor. Parsing an MMS message is slightly more involved because the message contents are segmented into parts. We can read the ID, thread ID, and incoming status from the main Cursor, but the address and content information need to be retrieved from additional tables.

First, we need to get the recipient information from the Mms.Addr table. MMS messages can be sent to multiple recipients, and each one is represented by a row in this table with a MSG_ID that matches the MMS message. We iterate through these elements and construct a comma-separated list of the results to attach to the MessageItem. Notice also that we are checking for our own number in this list to avoid having it added as well. Each message will also have an Addr entry for the local device number, and we don’t want to display that in our UI each time, so we are filtering it out.

Next we need to parse out the message contents. These values are stored in the Mms.Part table, keyed by the MSG_ID again. MMS messages can have many types of content associated with them (contact data, videos, images, and so forth) but we are interested in displaying only the text or image data that may be present. As we iterate through the parts, we validate the MIME string of the content type to find any text or image components to add to our MessageItem. For image attachments, we simply store the Uri pointing to the content rather than decoding the image and saving it here.

Note  As of this writing, the SDK provides column constants for Mms.Addr and Mms.Part, but there is no exposed content Uri. This will likely change in the future, but for now we have to hard-code the paths off the base Mms.CONTENT_URI constant.

For both SMS and MMS messages, the status of whether the message is incoming or outgoing can be determined by looking at the message’s box type. Messages that are marked in the Inbox or have no designation are considered incoming messages, while all other message box designations (Outbox, Sent, Drafts, and so forth) are considered outgoing.

Now that we have a parsed list of messages, let’s take a look at Listings 7-34 and 7-35 to inspect the user interface implemented in this example.

Listing 7-34. Activity to Display SMS/MMS Messages

public class SmsActivity extends Activity
        implements OnItemClickListener, LoaderCallbacks<List<MessageItem>> {
 
    private MessagesAdapter mAdapter;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ListView list = new ListView(this);
        mAdapter = new MessagesAdapter(this);
        list.setAdapter(mAdapter);
        
        final Intent intent = getIntent();
        
        if (!intent.hasExtra("threadId")) {
            //Items are clickable if we are not showing a conversation
            list.setOnItemClickListener(this);
        }
        //Load the messages data
        getLoaderManager().initLoader(0, getIntent().getExtras(), this);
        
        setContentView(list);
    }
 
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        final MessageItem item = mAdapter.getItem(position);
        long threadId = item.thread_id;
 
        //Launch a new instance to show this conversation
        Intent intent = new Intent(this, SmsActivity.class);
        intent.putExtra("threadId", threadId);
        startActivity(intent);
    }
    
    @Override
    public Loader<List<MessageItem>> onCreateLoader(int id, Bundle args) {
        if (args != null && args.containsKey("threadId")) {
            return new ConversationLoader(this, args.getLong("threadId"));
        } else {
            return new ConversationLoader(this);
        }
    }
 
    @Override
    public void onLoadFinished(Loader<List<MessageItem>> loader,List<MessageItem> data) {
        mAdapter.clear();
        mAdapter.addAll(data);
        mAdapter.notifyDataSetChanged();
    }
 
    @Override
    public void onLoaderReset(Loader<List<MessageItem>> loader) {
        mAdapter.clear();
        mAdapter.notifyDataSetChanged();
    }
    
    private static class MessagesAdapter extends ArrayAdapter<MessageItem> {
 
        int cacheSize = 4 * 1024 * 1024; // 4MiB
        private LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };
        
        public MessagesAdapter(Context context) {
            super(context, 0);
        }
        
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if (convertView == null) {
                convertView = LayoutInflater.from(getContext())
                        .inflate(R.layout.message_item, parent, false);
            }
            
            MessageItem item = getItem(position);
            
            TextView text1 = (TextView) convertView.findViewById(R.id.text1);
            TextView text2 = (TextView) convertView.findViewById(R.id.text2);
            ImageView image = (ImageView) convertView.findViewById(R.id.image);
            
            text1.setText(item.address);
            text2.setText(item.body);
            //Set text style based on incoming/outgoing status
            Typeface tf = item.incoming ?
                    Typeface.defaultFromStyle(Typeface.ITALIC) : Typeface.DEFAULT;
            text2.setTypeface(tf);
            image.setImageBitmap(getAttachment(item));
            
            return convertView;
        }
        
        private Bitmap getAttachment(MessageItem item) {
            if (item.attachment == null) return null;
            
            final Uri imageUri = item.attachment;
            //Pull image thumbnail from cache if we have it
            Bitmap cached = bitmapCache.get(imageUri.toString());
            if (cached != null) {
                return cached;
            }
            
            //Decode the asset from the provider if we don't have it in cache
            try {
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inJustDecodeBounds = true;
                int cellHeight = getContext().getResources()
                        .getDimensionPixelSize(R.dimen.message_height);
                InputStream is = getContext().getContentResolver().openInputStream(imageUri);
                BitmapFactory.decodeStream(is, null, options);
                
                options.inJustDecodeBounds = false;
                options.inSampleSize = options.outHeight / cellHeight;
                is = getContext().getContentResolver().openInputStream(imageUri);
                Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);
                
                bitmapCache.put(imageUri.toString(), bitmap);
                return bitmap;
            } catch (Exception e) {
                return null;
            }
        }
    }
}

Listing 7-35. res/layout/message_item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:minHeight="@dimen/message_height">
    <ImageView
        android:id="@+id/image"
        android:layout_width="@dimen/message_height"
        android:layout_height="@dimen/message_height"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true" />
    <TextView
        android:id="@+id/text1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toLeftOf="@id/image"
        android:layout_marginLeft="6dp"
        android:textStyle="bold" />
    <TextView
        android:id="@+id/text2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/text1"
        android:layout_toLeftOf="@id/image"
        android:layout_marginLeft="12dp" />
 
</RelativeLayout>

Our activity is used to display both types of message data. Upon creation, the activity calls initLoader() to construct a new ConversationLoader to query the provider. The arguments passed in are the extras received by the activity Intent. When the application first launches, there are no incoming Intent extras, so a ConversationLoader is constructed to load all conversation threads. Later, when the activity is launched with a specific thread ID, ConversationLoader will query all messages in that conversation.

Once the loading is complete, the data is presented in a ListView using our custom MessagesAdapter. This adapter inflates a custom item layout (from Listing 7-35) with two text rows and space for an image. The MessageItem address information is loaded into the top label, and the text content into the bottom label. If the message is MMS and an image attachment is present, we attempt to return the image via getAttachment() and insert it into the ImageView.

Loading these images from disk each time is expensive and a tad slow, so to improve scrolling performance in the list, we have added an LruCache to store recently loaded bitmaps in memory. The cache is set to 4MiB in size, so as to not overinflate the application’s heap over time. Additionally, each image is downsampled when returned from BitmapFactory (via BitmapFactory.Options.inSampleSize) to avoid loading an image that is larger than the space available in the list row and wasting memory.

We now have a basic messaging application that presents a read-only window into the SMS/MMS on our device. When launched, this application will list all the conversations by showing the latest message for each thread. When a conversation is tapped, a new activity will display, showing all the individual messages within that thread. Pressing the Back button will return to the main list so the user can select another conversation to view.

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

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