4-8. Parsing XML

Problem

Your application needs to parse responses, from an API or other source, that are formatted as XML.

Solution

(API Level 1)

Implement a subclass of org.xml.sax.helpers.DefaultHandler to parse the data using event-based SAX (Simple API for XML). Android has three primary methods you can use to parse XML data: DOM (Document Object Model), SAX, and Pull. The simplest of these to implement, and the most memory-efficient, is the SAX parser. SAX parsing works by traversing the XML data and generating callback events at the beginning and end of each element.

How It Works

To describe this further, let’s look at the format of the XML that is returned when requesting an RSS/Atom news feed (see Listing 4-27).

Listing 4-27. RSS Basic Structure

<rss version="2.0">
  <channel>
    <item>
      <title></title>
      <link></link>
      <description></description>
    </item>
    <item>
      <title></title>
      <link></link>
      <description></description>
    </item>
    <item>
      <title></title>
      <link></link>
      <description></description>
    </item>
    ...
  </channel>
</rss>

Between each set of <title>, <link>, and <description> tags is the value associated with each item. Using SAX, we can parse this data out into an array of items that the application could then display to the user in a list (see Listing 4-28).

Listing 4-28. Custom Handler to Parse RSS

public class RSSHandler extends DefaultHandler {
 
    public class NewsItem {
        public String title;
        public String link;
        public String description;
        
        @Override
        public String toString() {
            return title;
        }
    }
    
    private StringBuffer buf;
    private ArrayList<NewsItem> feedItems;
    private NewsItem item;
    
    private boolean inItem = false;
    
    public ArrayList<NewsItem> getParsedItems() {
        return feedItems;
    }
    
    //Called at the head of each new element
    @Override
    public void startElement(String uri, String name,
            String qName, Attributes attrs) {
        if("channel".equals(name)) {
            feedItems = new ArrayList<NewsItem>();
        } else if("item".equals(name)) {
            item = new NewsItem();
            inItem = true;
        } else if("title".equals(name) && inItem) {
            buf = new StringBuffer();
        } else if("link".equals(name) && inItem) {
            buf = new StringBuffer();
        } else if("description".equals(name) && inItem) {
            buf = new StringBuffer();
        }
    }
    
    //Called at the tail of each element end
    @Override
    public void endElement(String uri, String name,
            String qName) {
        if("item".equals(name)) {
            feedItems.add(item);
            inItem = false;
        } else if("title".equals(name) && inItem) {
            item.title = buf.toString();
        } else if("link".equals(name) && inItem) {
            item.link = buf.toString();
        } else if("description".equals(name) && inItem) {
            item.description = buf.toString();
        }
        
        buf = null;
    }
    
    //Called with character data inside elements
    @Override
    public void characters(char ch[], int start, int length) {
        //Don't bother if buffer isn't initialized
        if(buf != null) {
            for (int i=start; i<start+length; i++) {
                buf.append(ch[i]);
            }
        }
    }
}

The RSSHandler is notified at the beginning and end of each element via startElement() and endElement(). In between, the characters that make up the element’s value are passed into the characters() callback. As the parser moves through the document, the following steps occur:

  1. When the parser encounters the first element, the list of items is initialized.
  2. When each item element is encountered, a new NewsItem model is initialized.
  3. Inside each item element, data elements are captured in a StringBuffer and inserted into the members of the NewsItem.
  4. When the end of each item is reached, the NewsItem is added to the list.
  5. When parsing is complete, feedItems is a complete list of all the items in the feed.

Let’s look at this in action by using some of the tricks from the API example in Recipe 4-6 to download the latest Google News in RSS form (see Listing 4-29).

Listing 4-29. Activity That Parses the XML and Displays the Items

public class FeedActivity extends Activity implements ResponseCallback {
    private static final String TAG = "FeedReader";
    private static final String FEED_URI =
            "http://news.google.com/?output=rss";
 
    private ListView mList;
    private ArrayAdapter<NewsItem> mAdapter;
    private ProgressDialog mProgress;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        mList = new ListView(this);
        mAdapter = new ArrayAdapter<NewsItem>(this,
                android.R.layout.simple_list_item_1,
                android.R.id.text1);
        mList.setAdapter(mAdapter);
        mList.setOnItemClickListener(
                new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View v,
                    int position, long id) {
                NewsItem item = mAdapter.getItem(position);
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setData(Uri.parse(item.link));
                startActivity(intent);
            }
        });
        
        setContentView(mList);
    }
 
    @Override
    public void onResume() {
        super.onResume();
        //Retrieve the RSS feed
        try{
            HttpGet feedRequest = new HttpGet(new URI(FEED_URI));
            RestTask task = new RestTask();
            task.setResponseCallback(this);
            task.execute(feedRequest);
            mProgress = ProgressDialog.show(this, "Searching",
                    "Waiting For Results…", true);
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
    
    @Override
    public void onRequestSuccess(String response) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        //Process the response data
        try {
            SAXParserFactory factory =
                    SAXParserFactory.newInstance();
            SAXParser p = factory.newSAXParser();
            RSSHandler parser = new RSSHandler();
            p.parse(new InputSource(new StringReader(response)),
                    parser);
            
            mAdapter.clear();
            for(NewsItem item : parser.getParsedItems()) {
                mAdapter.add(item);
            }
            mAdapter.notifyDataSetChanged();
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
    
    @Override
    public void onRequestError(Exception error) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        //Display the error
        mAdapter.clear();
        mAdapter.notifyDataSetChanged();
        Toast.makeText(this, error.getMessage(),
                Toast.LENGTH_SHORT).show();
    }
}

The example has been modified to display a ListView, which will be populated by the parsed items from the RSS feed. In the example, we add an OnItemClickListener to the list that will launch the news item’s link in the browser.

Once the data is returned from the API in the response callback, Android’s built-in SAX parser handles the job of traversing the XML string. SAXParser.parse() uses an instance of our RSSHandler to process the XML, which results in the handler’s feedItems list being populated. The receiver then iterates through all the parsed items and adds them to an ArrayAdapter for display in the ListView.

XmlPullParser

The XmlPullParser provided by the framework is another efficient way of parsing incoming XML data. Like SAX, the parsing is stream based; it does not require much memory to parse large document feeds because the entire XML data structure does not need to be loaded before parsing can begin. Let’s see an example of using XmlPullParser to parse our RSS feed data. Unlike with SAX, however, we must manually advance the parser through the data stream every step of the way, even over the tag elements we aren’t interested in.

Listing 4-30 contains a factory class that iterates over the feed to construct model elements.

Listing 4-30. Factory Class to Parse XML into Model Objects

public class NewsItemFactory {
 
    /* Data Model Class */
    public static class NewsItem {
        public String title;
        public String link;
        public String description;
        
        @Override
        public String toString() {
            return title;
        }
    }
    
    /*
     * Parse the RSS feed out into a list of NewsItem elements
     */
    public static List<NewsItem> parseFeed(XmlPullParser parser)
            throws XmlPullParserException, IOException {
        List<NewsItem> items = new ArrayList<NewsItem>();
        
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG){
                continue;
            }
                
            if (parser.getName().equals("rss") ||
                    parser.getName().equals("channel")) {
                //Skip these items, but allow to drill inside
            } else if (parser.getName().equals("item")) {
                NewsItem newsItem = readItem(parser);
                items.add(newsItem);
            } else {
                //Skip any other elements and their children
                skip(parser);
            }
        }
        
        //Return the parsed list
        return items;
    }
    
    /*
     * Parse each <item> element in the XML into a NewsItem
     */
    private static NewsItem readItem(XmlPullParser parser) throws
            XmlPullParserException, IOException {
        NewsItem newsItem = new NewsItem();
        
        //Must start with an <item> element to be valid
        parser.require(XmlPullParser.START_TAG, null, "item");
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG){
                continue;
            }
            
            String name = parser.getName();
            if (name.equals("title")) {
                parser.require(XmlPullParser.START_TAG,
                        null, "title");
                newsItem.title = readText(parser);
                parser.require(XmlPullParser.END_TAG,
                        null, "title");
            } else if (name.equals("link")) {
                parser.require(XmlPullParser.START_TAG,
                        null, "link");
                newsItem.link = readText(parser);
                parser.require(XmlPullParser.END_TAG,
                        null, "link");
            } else if (name.equals("description")) {
                parser.require(XmlPullParser.START_TAG,
                        null, "description");
                newsItem.description = readText(parser);
                parser.require(XmlPullParser.END_TAG,
                        null, "description");
            } else {
                //Skip any other elements and their children
                skip(parser);
            }
        }
        
        return newsItem;
    }
    
    /*
     * Read the text content of the current element, which is the
     * data contained between the start and end tag
     */
    private static String readText(XmlPullParser parser) throws
            IOException, XmlPullParserException {
        String result = "";
        if (parser.next() == XmlPullParser.TEXT) {
            result = parser.getText();
            parser.nextTag();
        }
        return result;
    }
    
    /*
     * Helper method to skip over the current element and any
     * children it may have underneath it
     */
    private static void skip(XmlPullParser parser) throws
            XmlPullParserException, IOException {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            throw new IllegalStateException();
        }
        
        /*
         * For every new tag, increase the depth counter.
         * Decrease it for each tag's end and return when we
         * have reached an end tag that matches the one we
         * started with.
         */
        int depth = 1;
        while (depth != 0) {
            switch (parser.next()) {
            case XmlPullParser.END_TAG:
                depth--;
                break;
            case XmlPullParser.START_TAG:
                depth++;
                break;
            }
        }
    }
}

Pull parsing works by processing the data stream as a series of events. The application advances the parser to the next event by calling the next() method or one of the specialized variations. The following are the event types the parser will advance within:

  • START_DOCUMENT: The parser will return this event when it is first initialized. It will be in this state only until the first call to next(), nextToken(), or nextTag().
  • START_TAG: The parser has just read a start tag element. The tag name can be retrieved with getName(), and any attributes that were present can be read with getAttributeValue() and associated methods.
  • TEXT: Character data inside the tag element was read and can be obtained with getText().
  • END_TAG: The parser has just read an end tag element. The tag name of the matching start tag can be retrieved with getName().
  • END_DOCUMENT: The end of the data stream has been reached.

Because we must advance the parser ourselves, we have created a helper skip() method to assist in moving the parser past tags we aren’t interested in. This method walks from the current position through all nested child elements until the matching end tag is reached, skipping over them. It does this through a depth counter that increments for each start tag and decrements for each end tag. When the depth counter reaches zero, we have reached the matching end tag for the initial position.

The parser in this example starts iterating through the tags in the stream, looking for <item> tags that it can parse into a NewsItem when the parseFeed() method is called. Every element that is not one of these is skipped over, with the exception of two: <rss> and <channel>. All the items are nested within these two tags, so although we aren’t interested in them directly, we cannot hand them off to skip(), or all our items will be skipped as well.

The task of parsing each <item> element is handled by readItem(), where a new NewsItem is constructed and filled in by the data found within. The method begins by calling require(), which is a security check to ensure the XML is formatted as we expect. The method will quietly return if the current parser event matches the namespace and tag name passed in; otherwise, it will throw an exception. As we iterate through the child elements, we look specifically for the title, link, and description tags so we can read their values into the model data. After finding each tag, readText() advances the parser and pulls the enclosed character data out. Again, there are other elements inside <item> that we aren’t parsing, so we call skip() in the case of any tag we don’t need.

You can see that XmlPullParser is extremely flexible because you control every step of the process, but this also requires more code to accomplish the same result. Listing 4-31 shows our feed display activity reworked to use the new parser.

Listing 4-31. Activity Displaying Parsed XML Feed

public class PullFeedActivity extends Activity implements
        ResponseCallback {
    private static final String TAG = "FeedReader";
    private static final String FEED_URI =
            "http://news.google.com/?output=rss";
 
    private ListView mList;
    private ArrayAdapter<NewsItem> mAdapter;
    private ProgressDialog mProgress;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        mList = new ListView(this);
        mAdapter = new ArrayAdapter<NewsItem>(this,
                android.R.layout.simple_list_item_1,
                android.R.id.text1);
        mList.setAdapter(mAdapter);
        mList.setOnItemClickListener(
                new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View v,
                    int position, long id) {
                NewsItem item = mAdapter.getItem(position);
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setData(Uri.parse(item.link));
                startActivity(intent);
            }
        });
        
        setContentView(mList);
    }
 
    @Override
    public void onResume() {
        super.onResume();
        //Retrieve the RSS feed
        try{
            HttpGet feedRequest = new HttpGet(new URI(FEED_URI));
            RestTask task = new RestTask();
            task.setResponseCallback(this);
            task.execute(feedRequest);
            mProgress = ProgressDialog.show(this, "Searching",
                    "Waiting For Results…", true);
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
    
    @Override
    public void onRequestSuccess(String response) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        //Process the response data
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(new StringReader(response));
            //Jump to the first tag
            parser.nextTag();
            
            mAdapter.clear();
            for(NewsItem i : NewsItemFactory.parseFeed(parser)) {
                mAdapter.add(i);
            }
            mAdapter.notifyDataSetChanged();
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
    
    @Override
    public void onRequestError(Exception error) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        //Display the error
        mAdapter.clear();
        mAdapter.notifyDataSetChanged();
        Toast.makeText(this, error.getMessage(),
                Toast.LENGTH_SHORT).show();
    }
}

A fresh XmlPullParser can be instantiated using Xml.newPullParser(), and the input data source can be a Reader or InputStream instance passed to setInput(). In our case, the response data from the web service is already in a String, so we wrap that in a StringReader to have the parser consume. We can pass the parser to NewsItemFactory, which will then return a list of NewsItem elements that we can add to the ListAdapter and display just as we did before.

Tip  You can also use XmlPullParser to parse local XML data you may want to bundle in your application. By placing your raw XML into resources (such as res/xml/), you can use Resources.getXml() to instantiate an XmlResourceParser preloaded with your local data.

4-9. Receiving SMS

Problem

Your application must react to incoming SMS messages, commonly called text messages.

Solution

(API Level 1)

Register a BroadcastReceiver to listen for incoming messages, and process them in onReceive(). The operating system will fire a broadcast Intent with the android.provider.Telephony.SMS_RECEIVED action whenever there is an incoming SMS message. Your application can register a BroadcastReceiver to filter for this Intent and process the incoming data.

Note  Receiving this broadcast does not prevent the rest of the system’s applications from receiving it as well. The default messaging application will still receive and display any incoming SMS.

How It Works

In previous recipes, we defined BroadcastReceivers as private internal members to an activity. In this case, it is probably best to define the receiver separately and register it in AndroidManifest.xml by using the <receiver> tag. This will allow your receiver to process the incoming events even when your application is not active. Listings 4-32 and 4-33 show an example of a receiver that monitors all incoming SMS and raises a Toast when one arrives from the party of interest.

Listing 4-32. Incoming SMS BroadcastReceiver

public class SmsReceiver extends BroadcastReceiver {
    private static final String SHORTCODE = "55443";
 
    @Override
    public void onReceive(Context context, Intent intent) {
        Bundle bundle = intent.getExtras();
        
        Object[] messages = (Object[])bundle.get("pdus");
        SmsMessage[] sms = new SmsMessage[messages.length];
        //Create messages for each incoming PDU
        for(int n=0; n < messages.length; n++) {
            sms[n] =
                SmsMessage.createFromPdu((byte[]) messages[n]);
        }
 
        for(SmsMessage msg : sms) {
            //Verify if the message came from our known sender
            if(TextUtils.equals(
                    msg.getOriginatingAddress(), SHORTCODE)) {
                //Keep other apps from processing this message
                abortBroadcast();
                
                //Display our own notification
                Toast.makeText(context,
                        "Received message from the mothership: "
                        + msg.getMessageBody(),
                        Toast.LENGTH_SHORT).show();
            }
        }
    }
}

Listing 4-33. Partial AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
 
    <uses-permission
        android:name="android.permission.RECEIVE_SMS" />
 
  <application ...>
    <receiver android:name=".SmsReceiver">
      <!-- Add a priority to catch the ordered broadcast -->
      <intent-filter android:priority="5">
        <action
          android:name="android.provider.Telephony.SMS_RECEIVED"
        />
      </intent-filter>
    </receiver>
  </application>
    
</manifest>

Important  Receiving SMS messages requires that the android.permission.RECEIVE_SMS permission be declared in the manifest!

Incoming SMS messages are passed via the extras of the broadcast Intent as an Object array of byte arrays, each byte array representing an SMS protocol data unit (PDU). SmsMessage.createFromPdu() is a convenience method allowing us to create SmsMessage objects from the raw PDU data. With the setup work complete, we can inspect each message to determine whether there is something interesting to handle or process. In the example, we compare the originating address of each message against a known short code, and the user is notified when one arrives.

The broadcast triggered by the framework is an ordered broadcast message, which means that each registered receiver will receive the message in order and will have an opportunity to modify the broadcast before it is handed to the next receiver or to cancel it and stop any lower-priority receivers from receiving it at all.

In the AndroidManifest.xml entry for the <intent-filter>, we had added an arbitrary priority value to insert our receiver above the core system Messages application (which uses the default priority of zero). This allows our application to process the SMS message first.

Note  With an ordered broadcast, receivers that are registered at the same priority will receive the Intent at the “same time,” such that the order between them is not determined. Additionally, one receiver cannot cancel the broadcast from being delivered to the other(s) of the same priority.

Then, once we verify that the message we are looking at came from the sender we are tracking, a call to abortBroadcast() terminates the responder chain. This keeps the SMS messages we are processing from displaying to the user and cluttering up their SMS inbox.

Important  There is no external method of verifying what other apps on the system may also be registered to handle this broadcast and have a very high priority (or at least, higher than your app). Your application is at the mercy of a higher-priority application not aborting the broadcast for the message you want to process.

At the point in the example where the Toast is raised, you may wish to provide something more useful to the user. Perhaps the SMS message includes an offer code for your application, and you could launch the appropriate activity to display this information to the user within the application.

DEFAULT SMS APPLICATIONS

Starting with Android 4.4, the behavior of applications using SMS has changed. The device’s Settings application now provides the user with a Default SMS App option that selects the application the user would prefer to use for SMS. At the framework level, this augments some of the behaviors around sending and receiving messages.

Applications that are not selected as the default may still send outgoing SMS and monitor incoming messages by using the same ordered broadcast described in this recipe. However, two new broadcast actions have been added for the default SMS app to receive messages:

  • android.provider.telephony.SMS_DELIVER
  • android.provider.telephony.WAP_PUSH_DELIVER

The framework will broadcast incoming SMS/MMS message data to the default SMS app separately from other applications using these two actions. Although the original SMS_RECEIVED action is still an ordered broadcast, aborting that broadcast can no longer be used as a technique to intercept certain messages from being delivered to that application. However, aborting the broadcast will still interrupt the chain from going to any other third-party app that is monitoring incoming SMS.

Additionally, an SMS application marked as the default is responsible for writing all SMS data received on the device to the device’s internal content provider exposed publicly in API Level 19 via android.provider.Telephony. This application is the only one on the system with privileges to write data to the SMS provider, regardless of an application’s request to obtain the android.permission.WRITE_SMS permission.

Other applications may still read the SMS provider data if they have obtained the android.permission.READ_SMS permission. We will look in more detail at reading the SMS provider in Chapter 7.

4-10. Sending an SMS Message

Problem

Your application must issue outgoing SMS messages.

Solution

(API Level 4)

Use the SMSManager to send text and data SMS messages. SMSManager is a system service that handles sending SMS and providing feedback to the application about the status of the operation. SMSManager provides methods to send text messages by using SmsManager.sendTextMessage() and SmsManager.sendMultipartTextMessage(), or data messages by using SmsManager.sendDataMessage(). Each of these methods takes PendingIntent parameters to deliver status for the send operation and the message delivery back to a requested destination.

How It Works

Let’s take a look at a simple example activity that sends an SMS message and monitors its status (see Listing 4-34).

Listing 4-34. Activity to Send SMS Messages

public class SmsActivity extends Activity {
    private static final String SHORTCODE = "55443";
    private static final String ACTION_SENT =
            "com.examples.sms.SENT";
    private static final String ACTION_DELIVERED =
            "com.examples.sms.DELIVERED";
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        Button sendButton = new Button(this);
        sendButton.setText("Hail the Mothership");
        sendButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                sendSMS("Beam us up!");
            }
        });
 
        setContentView(sendButton);
    }
    
    private void sendSMS(String message) {
        PendingIntent sIntent = PendingIntent.getBroadcast(
            this, 0, new Intent(ACTION_SENT), 0);
        PendingIntent dIntent = PendingIntent.getBroadcast(
            this, 0, new Intent(ACTION_DELIVERED), 0);
         //Monitor status of the operation
        registerReceiver(sent, new IntentFilter(ACTION_SENT));
        registerReceiver(delivered,
                new IntentFilter(ACTION_DELIVERED));
         //Send the message
        SmsManager manager = SmsManager.getDefault();
        manager.sendTextMessage(SHORTCODE, null, message,
                sIntent, dIntent);
    }
 
    private BroadcastReceiver sent = new BroadcastReceiver(){
        @Override
        public void onReceive(Context context, Intent intent) {
            switch (getResultCode()) {
            case Activity.RESULT_OK:
                //Handle sent success
                break;
            case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
            case SmsManager.RESULT_ERROR_NO_SERVICE:
            case SmsManager.RESULT_ERROR_NULL_PDU:
            case SmsManager.RESULT_ERROR_RADIO_OFF:
                //Handle sent error
                break;
            }
 
            unregisterReceiver(this);
        }
    };
 
    private BroadcastReceiver delivered = new BroadcastReceiver(){
        @Override
        public void onReceive(Context context, Intent intent) {
            switch (getResultCode()) {
            case Activity.RESULT_OK:
                //Handle delivery success
                break;
            case Activity.RESULT_CANCELED:
                //Handle delivery failure
                break;
            }
 
            unregisterReceiver(this);
        }
    };
}

Important  Sending SMS messages requires that the android.permission.SEND_SMS permission be declared in the manifest!

In the example, an SMS message is sent out via the SMSManager whenever the user taps the button. Because SMSManager is a system service, the static SMSManager.getDefault() method must be called to get a reference to it. sendTextMessage() takes the destination address (number), service center address, and message as parameters. The service center address should be null to allow SMSManager to use the system default.

Two BroadcastReceivers are registered to receive the callback Intents that will be sent: one for status of the send operation and the other for status of the delivery. The receivers are registered only while the operations are pending, and they unregister themselves as soon as the Intent is processed.

4-11. Communicating over Bluetooth

Problem

You want to leverage Bluetooth communication to transmit data between devices in your application.

Solution

(API Level 5)

Use the Bluetooth APIs introduced in API Level 5 to create a peer-to-peer connection over the Radio frequency communications (RFCOMM) protocol interface. Bluetooth is a very popular wireless radio technology that is in almost all mobile devices today. Many users think of Bluetooth as a way for their mobile devices to connect with a wireless headset or integrate with a vehicle’s stereo system. However, Bluetooth can also be a simple and effective way for developers to create peer-to-peer connections in their applications.

How It Works

Important  Bluetooth is not currently supported in the Android emulator. In order to execute the code in this example, Bluetooth must be run on an Android device. Furthermore, to appropriately test the functionality, you need two devices running the application simultaneously.

Bluetooth Peer-to-Peer

Listings 4-35 through 4-37 illustrate an example that uses Bluetooth to find other users nearby and quickly exchange contact information (in this case, just an email address). Connections are made over Bluetooth by discovering available ”services” and connecting to them by referencing their unique 128-bit UUID value. The UUID of the service you want to use must either be discovered or known ahead of time.

In this example, the same application is running on both devices on each end of the connection, so we have the freedom to define the UUID in code as a constant because both devices will have a reference to it.

Note  To ensure that the UUID you choose is unique, use one of the many free UUID generators available on the Web or tools such as uuidgen on Mac/Linux.

Listing 4-35. AndroidManifest.xml

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

Important  Remember that android.permission.BLUETOOTH must be declared in the manifest to use these APIs. In addition, android.permission.BLUETOOTH_ADMIN must be declared to make changes to preferences such as discoverability and to enable/disable the adapter.

Listing 4-36. res/layout/main.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="match_parent">
    <TextView
        android:id="@+id/label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="Enter Your Email:" />
    <EditText
        android:id="@+id/emailField"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/label"
        android:singleLine="true"
        android:inputType="textEmailAddress" />
    <Button
        android:id="@+id/scanButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:text="Connect and Share" />
    <Button
        android:id="@+id/listenButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_above="@id/scanButton"
        android:text="Listen for Sharers" />
</RelativeLayout>

The user interface for this example consists of an EditText for users to enter an email address, and two buttons to initiate communication. The Listen for Sharers button puts the device into listen mode. In this mode, the device will accept and communicate with any device that attempts to connect with it. The Connect and Share button puts the device into search mode. In this mode, the device searches for any device that is currently listening and makes a connection (see Listing 4-37).

Listing 4-37. Bluetooth Exchange Activity

public class ExchangeActivity extends Activity {
 
    // Unique UUID for this application (generated from the web)
    private static final UUID MY_UUID =
        UUID.fromString("321cb8fa-9066-4f58-935e-ef55d1ae06ec");
    //Friendly name to match while discovering
    private static final String SEARCH_NAME = "bluetooth.recipe";
 
    BluetoothAdapter mBtAdapter;
    BluetoothSocket mBtSocket;
    Button listenButton, scanButton;
    EditText emailField;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(
                Window.FEATURE_INDETERMINATE_PROGRESS);
        setContentView(R.layout.main);
 
        //Check the system status
        mBtAdapter = BluetoothAdapter.getDefaultAdapter();
        if(mBtAdapter == null) {
            Toast.makeText(this, "Bluetooth is not supported.",
                Toast.LENGTH_SHORT).show();
            finish();
            return;
        }
        if (!mBtAdapter.isEnabled()) {
            Intent enableIntent = new Intent(
                    BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableIntent, REQUEST_ENABLE);
        }
 
        emailField = (EditText)findViewById(R.id.emailField);
        listenButton = (Button)findViewById(R.id.listenButton);
        listenButton.setOnClickListener(
                new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //Make sure the device is discoverable first
                if (mBtAdapter.getScanMode() != BluetoothAdapter
                        .SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
                    Intent discoverableIntent = new Intent(
                            BluetoothAdapter
                                .ACTION_REQUEST_DISCOVERABLE);
                    discoverableIntent.putExtra(BluetoothAdapter.
                            EXTRA_DISCOVERABLE_DURATION, 300);
                    startActivityForResult(discoverableIntent,
                            REQUEST_DISCOVERABLE);
                    return;
                }
                startListening();
            }
        });
        scanButton = (Button)findViewById(R.id.scanButton);
        scanButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mBtAdapter.startDiscovery();
                setProgressBarIndeterminateVisibility(true);
            }
        });
    }
 
    @Override
    public void onResume() {
        super.onResume();
        //Register the activity for broadcast intents
        IntentFilter filter = new IntentFilter(
                BluetoothDevice.ACTION_FOUND);
        registerReceiver(mReceiver, filter);
        filter = new IntentFilter(
                BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
        registerReceiver(mReceiver, filter);
    }
 
    @Override
    public void onPause() {
        super.onPause();
        unregisterReceiver(mReceiver);
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
        try {
            if(mBtSocket != null) {
                mBtSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    private static final int REQUEST_ENABLE = 1;
    private static final int REQUEST_DISCOVERABLE = 2;
 
    @Override
    protected void onActivityResult(int requestCode,
            int resultCode, Intent data) {
        switch(requestCode) {
        case REQUEST_ENABLE:
            if(resultCode != Activity.RESULT_OK) {
                Toast.makeText(this, "Bluetooth Not Enabled.",
                    Toast.LENGTH_SHORT).show();
                finish();
            }
            break;
        case REQUEST_DISCOVERABLE:
            if(resultCode == Activity.RESULT_CANCELED) {
                Toast.makeText(this, "Must be discoverable.",
                    Toast.LENGTH_SHORT).show();
            } else {
                startListening();
            }
            break;
        default:
            break;
        }
    }
 
    //Start a server socket and listen
    private void startListening() {
        AcceptTask task = new AcceptTask();
        task.execute(MY_UUID);
        setProgressBarIndeterminateVisibility(true);
    }
 
    //AsyncTask to accept incoming connections
    private class AcceptTask extends
            AsyncTask<UUID, Void, BluetoothSocket> {
 
        @Override
        protected BluetoothSocket doInBackground(UUID… params) {
            String name = mBtAdapter.getName();
            try {
                //While listening, set the discovery name to
                // a specific value
                mBtAdapter.setName(SEARCH_NAME);
                BluetoothServerSocket socket = mBtAdapter
                        .listenUsingRfcommWithServiceRecord(
                            "BluetoothRecipe", params[0]);
                BluetoothSocket connected = socket.accept();
                //Reset the BT adapter name
                mBtAdapter.setName(name);
                return connected;
            } catch (IOException e) {
                e.printStackTrace();
                mBtAdapter.setName(name);
                return null;
            }
        }
 
        @Override
        protected void onPostExecute(BluetoothSocket socket) {
            if(socket == null) {
                return;
            }
            mBtSocket = socket;
            ConnectedTask task = new ConnectedTask();
            task.execute(mBtSocket);
        }
 
    }
 
    //AsyncTask to receive a single line of data and post
    private class ConnectedTask extends
            AsyncTask<BluetoothSocket,Void,String> {
 
        @Override
        protected String doInBackground(
                BluetoothSocket... params) {
            InputStream in = null;
            OutputStream out = null;
            try {
                //Send your data
                out = params[0].getOutputStream();
                String email = emailField.getText().toString();
                out.write(email.getBytes());
                //Receive the other's data
                in = params[0].getInputStream();
                byte[] buffer = new byte[1024];
                in.read(buffer);
                //Create a clean string from results
                String result = new String(buffer);
                //Close the connection
                mBtSocket.close();
                return result.trim();
            } catch (Exception exc) {
                return null;
            }
        }
 
        @Override
        protected void onPostExecute(String result) {
            Toast.makeText(ExchangeActivity.this, result,
                    Toast.LENGTH_SHORT).show();
            setProgressBarIndeterminateVisibility(false);
        }
    }
 
    // The BroadcastReceiver that listens for discovered devices
    private BroadcastReceiver mReceiver =
            new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
 
            // When discovery finds a device
            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                // Get the BluetoothDevice object from the Intent
                BluetoothDevice device =
                    intent.getParcelableExtra(
                        BluetoothDevice.EXTRA_DEVICE);
                if(TextUtils.equals(device.getName(),
                        SEARCH_NAME)) {
                    //Matching device found, connect
                    mBtAdapter.cancelDiscovery();
                    try {
                        mBtSocket = device
                            .createRfcommSocketToServiceRecord(
                                MY_UUID);
                        mBtSocket.connect();
                        ConnectedTask task = new ConnectedTask();
                        task.execute(mBtSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            //When discovery is complete
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED
                    .equals(action)) {
                setProgressBarIndeterminateVisibility(false);
            }
 
        }
    };
}

When the application first starts up, it runs some basic checks on the Bluetooth status of the device. If BluetoothAdapter.getDefaultAdapter() returns null, it is an indication that the device does not have Bluetooth support, and the application will go no further. Even with Bluetooth on the device, it must be enabled for the application to use it. If Bluetooth is disabled, the preferred method for enabling the adapter is to send an Intent to the system with BluetoothAdapter.ACTION_REQUEST_ENABLE as the action. This notifies the user of the issue, and that user can then enable Bluetooth. A BluetoothAdapter can be manually enabled with the enable() method, but we strongly discourage you from doing this unless you have requested the user’s permission another way.

With Bluetooth validated, the application waits for user input. As mentioned previously, the example can be put into one of two modes on each device: listen mode or search mode. Let’s look at the path each mode takes.

Listen Mode

Tapping the Listen for Sharers button starts the application listening for incoming connections. In order for a device to accept incoming connections from devices it may not know, it must be set as discoverable. The application verifies this by checking whether the adapter’s scan mode is equal to SCAN_MODE_CONNECTABLE_DISCOVERABLE. If the adapter does not meet this requirement, another Intent is sent to the system to notify the user that they should allow the device to be discoverable, similar to the method used to request that Bluetooth be enabled. If the user accepts this request, the activity will return a result equal to the length of time they allowed the device to be discoverable; if they cancel the request, the activity will return Activity.RESULT_CANCELED. Our example monitors for a user canceling in onActivityResult(), and finishes under those conditions.

If the user allows discovery, or if the device was already discoverable, an AcceptTask is created and executed. This task creates a listener socket for the specified UUID of the service we defined, and it blocks the calling thread while waiting for an incoming connection request. Once a valid request is received, it is accepted, and the application moves into connected mode.

During the period of time while the device is listening, its Bluetooth name is set to a known unique value (SEARCH_NAME) to speed up the discovery process (you’ll see more about why in the “Search Mode” section). Once the connection is established, the default name given to the adapter is restored.

Search Mode

Tapping the Connect and Share button tells the application to begin searching for another device to connect with. It does this by starting a Bluetooth discovery process and handling the results in a BroadcastReceiver. When a discovery is started via BluetoothAdapter.startDiscovery(), Android will asynchronously call back with broadcasts under two conditions: when another device is found, and when the process is complete.

The private receiver mReceiver is registered at all times when the activity is visible to the user, and it will receive a broadcast with each new discovered device. Recall from the discussion on listen mode that the device name of a listening device was set to a unique value. Upon each discovery made, the receiver checks that the device name matches our known value, and it attempts to connect when one is found. This is important to the speed of the discovery process, because otherwise the only way to validate each device is to attempt a connection to the specific service UUID and see whether the operation is successful. The Bluetooth connection process is heavyweight and slow and should be done only when necessary to keep things performing well.

This method of matching devices also relieves the user of the need to select manually which device they want to connect to. The application is smart enough to find another device that is running the same application and in a listening mode to complete the transfer. Removing the user also means that this value should be unique and obscure so as to avoid finding other devices that may accidentally have the same name.

With a matching device found, we cancel the discovery process (as it is also heavyweight and will slow down the connection) and then make a connection to the service’s UUID. With a successful connection made, the application moves into connected mode.

Connected Mode

Once connected, the application on both devices will create a ConnectedTask to send and receive the user contact information. The connected BluetoothSocket has an InputStream and an OutputStream available to do data transfer. First, the current value of the e-mail text field is packaged up and written to the OutputStream. Then, the InputStream is read to receive the remote device’s information. Finally, each device takes the raw data it received and packages this into a clean string to display for the user.

The ConnectedTask.onPostExecute() method is tasked with displaying the results of the exchange to the user; currently, this is done by raising a Toast with the received contents. After the transaction, the connection is closed, and both devices are in the same mode and ready to execute another exchange.

For more information on this topic, take a look at the BluetoothChat sample application provided with the Android SDK. This application provides a great demonstration of making a long-lived connection for users to send chat messages between devices.

Bluetooth Beyond Android

As we mentioned in the beginning of this section, Bluetooth is found in many wireless devices besides mobile phones and tablets. RFCOMM interfaces also exist in devices such as Bluetooth modems and serial adapters. The same APIs that were used to create the peer-to-peer connection between Android devices can also be used to connect to other embedded Bluetooth devices for the purposes of monitoring and control.

The key to establishing a connection with these embedded devices is obtaining the UUID of the RFCOMM services they support. Bluetooth services that are part of a profile standard, and their identifiers, are defined by the Bluetooth Special Interest Group (SIG); so you may be able to obtain the UUID you require for a given device from the documentation provided on www.bluetooth.org. However, if your device manufacturer has defined a device-specific UUID for a custom service type and it is not readily documented, we must have a way to discover it. As with the previous example, with the proper UUID we can create a BluetoothSocket and transmit data.

The capability to do this exists in the SDK, although prior to Android 4.0.3 (API Level 15) it was not part of the public SDK. There are two methods on BluetoothDevice that will provide this information; fetchUuidsWithSdp() and getUuids(). The latter simply returns the cached instances for the device  found during discovery, while the former asynchronously connects to the device and does a fresh query. Because of this, when using fetchUuidsWithSdp(), you must register a BroadcastReceiver that will receive Intents set with the BluetoothDevice.ACTION_UUID action string to discover the UUID values.

Discover a UUID

A quick glance at the source code for BluetoothDevice (thanks to Android’s open source roots) points out that these methods to return UUID information for a remote device have existed for a while. If necessary, we can use reflection to call them in earlier Android versions now that they are part of the public API and won’t change in the future. The simplest to use is the synchronous (blocking) method getUuids(), which returns an array of ParcelUuid objects referring to each service. Here is an example method for reading the UUIDs of service records from a remote device using reflection:

public ParcelUuid servicesFromDevice(BluetoothDevice device) {
    try {
        Class cl =
            Class.forName("android.bluetooth.BluetoothDevice");
        Class[] par = {};
        Method method = cl.getMethod("getUuids", par);
        Object[] args = {};
        ParcelUuid[] retval =
            (ParcelUuid[]) method.invoke(device, args);
        return retval;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

You may also call fetchUuidsWithSdp()in the same fashion, but there were some variations in the Intent structure that was returned in early versions, so we would not recommend doing so for earlier Android versions.

4-12. Querying Network Reachability

Problem

Your application needs to be aware of changes in network connectivity.

Solution

(API Level 1)

Keep tabs on the device’s connectivity with ConnectivityManager. One of the paramount issues to consider in mobile application design is that the network is not always available for use. As people move about, the speeds and capabilities of networks are subject to change. An application that uses network resources should always be able to detect whether those resources are reachable and then notify the user when they are not.

In addition to reachability, ConnectivityManager can provide the application with information about the connection type. This allows you to make decisions such as whether to download a large file because the user is currently roaming and it may cost the user a fortune.

How It Works

Listing 4-38 creates a wrapper method you can place in your code to check for network connectivity.

Listing 4-38. ConnectivityManager Wrapper

public static boolean isNetworkReachable() {
    final ConnectivityManager mManager =
            (ConnectivityManager)context.getSystemService(
                    Context.CONNECTIVITY_SERVICE);
    NetworkInfo current = mManager.getActiveNetworkInfo();
    if(current == null) {
        return false;
    }
    return (current.getState() == NetworkInfo.State.CONNECTED);
}

ConnectivityManager does pretty much all the work in checking the network status, and this wrapper method is more to simplify having to check all possible network paths each time. Note that ConnectivityManager.getActiveNetworkInfo() will return null if there is no active data connection available, so we must check for that case first. If there is an active network, we can inspect its state, which will return one of the following:

  • DISCONNECTED
  • CONNECTING
  • CONNECTED
  • DISCONNECTING

When the state returns as CONNECTED, the network is considered stable and we can utilize it to access remote resources.

Verifying a Route

Mobile devices have multiple connectivity routes (WiFi, 3G/4G, and so forth), and it is common for a device to be connected to a network that doesn’t have a route to the external Web; this is especially common with WiFi networks. ConnectivityManager alone simply notifies you of whether or not your device has associated with a particular network, but says nothing of that network’s ability to access an outside IP address. Add to this the fact that when a device attempts to connect through a network that is “connected” but has no valid route, the time the network stack can take to time out and fail properly can be minutes.

You may find yourself in a situation where it is smarter to check for a valid Internet connection rather than just an association with a network. Listing 4-39 builds on the previous reachability check to do just that.

Listing 4-39. Smarter ConnectivityManager Wrapper

public static boolean hasNetworkConnection(Context context) {
        final ConnectivityManager connectivityManager =
                (ConnectivityManager) context.getSystemService(
                        Context.CONNECTIVITY_SERVICE);
        final NetworkInfo activeNetworkInfo =
                connectivityManager.getActiveNetworkInfo();
 
        //If we aren't even associated with a network, we're done
        boolean connected = (null != activeNetworkInfo)
                && activeNetworkInfo.isConnected();
        if (!connected) return false;
 
        //Check if we can access a remote server
        boolean routeExists;
        try {
            //Check Google Public DNS
            InetAddress host = InetAddress.getByName("8.8.8.8");
 
            Socket s = new Socket();
            s.connect(new InetSocketAddress(host, 53), 5000);
            //It exists if no exception is thrown
            routeExists = true;
            s.close();
        } catch (IOException e) {
            routeExists = false;
        }
 
        return (connected && routeExists);
}

After verifying the same reachability condition as before, Listing 4-39 goes a step further and attempts to open a socket to the well-known standard IPv4 address for the Google Public DNS (8.8.8.8) with a 5-second time-out. If a connection to this host succeeds, we can have a relatively high level of confidence that the device can access any active Internet resource. The advantage to this approach over attempting to fully connect directly to your remote server is that this code will fail faster, forcing up to only a 5-second delay before telling the user they really don’t have the Internet connection they think they do.

It is considered good practice to call a reachability check whenever a network request fails and to notify the user that their request failed because of a lack of connectivity. Listing 4-40 is an example of doing this when a network access fails.

Listing 4-40. Notify User of Connectivity Failure

try {
    //Attempt to access network resource. May throw
    // HttpResponseException or some other IOException on failure
} catch (Exception e) {
    if( !isNetworkReachable() ) {
        AlertDialog.Builder builder =
                new AlertDialog.Builder(context);
        builder.setTitle("No Network Connection");
        builder.setMessage("The Network is unavailable."
                + " Please try your request again later.");
        builder.setPositiveButton("OK",null);
        builder.create().show();
    }
}

Determining Connection Type

In cases where it is also essential to know whether the user is connected to a network that charges for bandwidth, we can call NetworkInfo.getType() on the active network connection (see Listing 4-41).

Listing 4-41. ConnectivityManager Bandwidth Checking

public boolean isWifiReachable() {
    ConnectivityManager mManager =
            (ConnectivityManager)context.getSystemService(
                    Context.CONNECTIVITY_SERVICE);
    NetworkInfo current = mManager.getActiveNetworkInfo();
    if(current == null) {
        return false;
    }
    return (current.getType() == ConnectivityManager.TYPE_WIFI);
}

This modified version of the reachability check determines whether the user is attached to a WiFi connection, typically indicating that the user has a faster connection where bandwidth isn’t tariffed.

4-13. Transferring Data with NFC

Problem

You have an application that must quickly transfer small data packets between two Android devices with minimal setup.

Solution

(API Level 16)

Use the Near field communications (NFC) Beam APIs. NFC communication was originally added to the SDK in Android 2.3 and was expanded in 4.0 to make short-message transfer between devices painless through a process called Android Beam. In Android 4.1, even more was added to make the Beam APIs fully mature for transferring data between two devices.

One of the major additions in 4.1 was the ability to transfer large data over alternate connections. NFC is a great method of discovering devices and setting up an initial connection, but it is low bandwidth and inefficient for sending large data packets such as full-color images. Previously, developers could use NFC to connect two devices but would need to manually negotiate a second connection over WiFi Direct or Bluetooth to transfer the file data. In Android 4.1, the framework now handles that entire process, and any application can share large files over any available connection with a single API call.

How It Works

Depending on the size of the content you wish to push, there are two mechanisms available to transfer data from one device to another.

Beaming with Foreground Push

If you want to send simple content between devices over NFC, you can use the foreground push mechanism to create an NfcMessage containing one or more NfcRecord instances. Listings 4-42 and 4-43 illustrate creating a simple NfcMessage to push to another device.

Listing 4-42. AndroidManifest.xml

<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.examples.nfcbeam"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk
        android:minSdkVersion="16"
        android:targetSdkVersion="16" />
 
    <uses-permission android:name="android.permission.NFC" />
    <application
       android:icon="@drawable/ic_launcher"
       android:label="NfcBeam">
       <activity
          android:name=".NfcActivity"
          android:label="NfcActivity"
          android:launchMode="singleTop">
          <intent-filter>
             <action android:name="android.intent.action.MAIN" />
             <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
          <intent-filter>
             <action android:name="android.nfc.action.NDEF_DISCOVERED" />
             <category android:name="android.intent.category.DEFAULT" />
             <data android:mimeType=
                 "application/com.example.androidrecipes.beamtext"/>
          </intent-filter>
        </activity>
    </application>
</manifest>

First notice that android.permission.NFC is required to work with the NFC service. Second, note the custom <intent-filter> placed on our activity. This is how Android will know which application to launch in response to the content it receives.

Listing 4-43. Activity Generating an NFC Foreground Push

public class NfcActivity extends Activity implements
        CreateNdefMessageCallback, OnNdefPushCompleteCallback {
    private static final String TAG = "NfcBeam";
    private NfcAdapter mNfcAdapter;
    private TextView mDisplay;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mDisplay = new TextView(this);
        setContentView(mDisplay);
        
        // Check for available NFC Adapter
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        if (mNfcAdapter == null) {
          mDisplay.setText("NFC not available on this device.");
        } else {
            // Register callback to set NDEF message. Setting
            // this makes NFC data push active while the Activity
            // is in the foreground.
            mNfcAdapter.setNdefPushMessageCallback(this, this);
            // Register callback for message-sent success
            mNfcAdapter.setOnNdefPushCompleteCallback(this, this);
        }
    }
    
    @Override
    public void onResume() {
        super.onResume();
        // Check to see if a Beam launched this Activity
        if (NfcAdapter.ACTION_NDEF_DISCOVERED
                .equals(getIntent().getAction())) {
            processIntent(getIntent());
        }
    }
 
    @Override
    public void onNewIntent(Intent intent) {
        // onResume gets called after this to handle the intent
        setIntent(intent);
    }
 
    void processIntent(Intent intent) {
        Parcelable[] rawMsgs = intent.getParcelableArrayExtra(
                NfcAdapter.EXTRA_NDEF_MESSAGES);
        // only one message sent during the beam
        NdefMessage msg = (NdefMessage) rawMsgs[0];
        // record 0 contains the MIME type
        mDisplay.setText(new String(
                msg.getRecords()[0].getPayload()) );
    }
    
    @Override
    public NdefMessage createNdefMessage(NfcEvent event) {
        String text = String.format(
                "Sending A Message From Android Recipes at %s",
                DateFormat.getTimeFormat(this)
                        .format(new Date()) );
        NdefMessage msg = new NdefMessage(NdefRecord.createMime(
                "application/com.example.androidrecipes.beamtext",
                text.getBytes()) );
        return msg;
    }
    
    @Override
    public void onNdefPushComplete(NfcEvent event) {
        //This callback happens on a binder thread, don't update
        // the UI directly from this method.
        Log.i(TAG, "Message Sent!");
    }
}

This example application encompasses both the sending and receiving of an NFC push, so the same application should be installed on both devices: the one that is sending and the one that is receiving the data. The activity registers itself for foreground push by using the setNdefPushMessageCallback() method on the NfcAdapter. This call does two things simultaneously. It tells the NFC service to call this activity at the moment a transfer is initiated to receive the message it needs to send, and it also activates NFC push whenever this activity is in the foreground. There is also an alternate version of this called setNdefPushMessage() that takes the message directly rather than implementing a callback.

The callback method constructs an NdefMessage containing a single NFC Data Exchange Format (NDEF) MIME record (created with the NdefRecord.createMime() method). MIME records are simple ways of passing application-specific data. The createMime() method takes both a string for the MIME type and a byte array for the raw data. The information can be anything from a text string to a small image; your application is responsible for packing and unpacking it. Notice that the MIME type here matches the type defined in the manifest’s <intent-filter>.

In order for the push to work, the sending device must have this activity active in the foreground, and the receiving device must not be locked. When the user touches the two devices together, the sending screen shows Android’s Touch to Beam UI, and a tap of the screen sends the message to the other device. As soon as the message is received, the application launches on the receiving device, and the sending device’s onNdefPushComplete() callback is triggered.

On the receiving device, the activity is launched with the ACTION_NDEF_DISCOVERED Intent, so our example will inspect the Intent for the NdefMessage and unpack the payload, turning it back from bytes into a string. This method of using Intent matching to send NFC data is the most flexible, but sometimes you want your application to be explicitly called. This is where Android Application Records come in.

Android Application Records

Your application can provide an additional NdefRecord inside an NdefMessage that directs Android to call a specific package name on the receiving device. To include this in our previous example, we would simply modify the CreateNdefMessageCallback like so:

@Override
public NdefMessage createNdefMessage(NfcEvent event) {
    String text = String.format(
            "Sending A Message From Android Recipes at %s",
            DateFormat.getTimeFormat(this)
                    .format(new Date()) );
    NdefMessage msg = new NdefMessage(NdefRecord.createMime(
            "application/com.example.androidrecipes.beamtext",
            text.getBytes()),
            NdefRecord
                .createApplicationRecord("com.examples.nfcbeam"));
    return msg;
}

With the addition of NdefRecord.createApplicationRecord(), this push message is now guaranteed to launch only our com.examples.nfcbeam package. The text information is still the first record in the message, so our unpacking of the received message remains unchanged.

Beaming Larger Content

We mentioned at the beginning of this recipe that sending large content blobs over NFC is not a great idea. However, Android Beam has the capability to handle that as well. Have a look at Listings 4-44 and 4-45 for examples of sending large image files over Beam.

Listing 4-44. AndroidManifest.xml

<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.examples.nfcbeam"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk
        android:minSdkVersion="16"
        android:targetSdkVersion="16" />
 
    <uses-permission android:name="android.permission.NFC" />
    <application
       android:icon="@drawable/ic_launcher"
       android:label="NfcBeam">
       <activity
          android:name=".BeamActivity"
          android:label="BeamActivity"
          android:launchMode="singleTop">
          <intent-filter>
             <action android:name="android.intent.action.MAIN" />
             <category
                android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
          <intent-filter>
             <action android:name="android.intent.action.VIEW" />
             <data android:mimeType="image/*" />
          </intent-filter>
       </activity>
    </application>
 
</manifest>

Listing 4-45. Activity to Transfer an Image File

public class BeamActivity extends Activity implements
        CreateBeamUrisCallback, OnNdefPushCompleteCallback {
    private static final String TAG = "NfcBeam";
    private static final int PICK_IMAGE = 100;
    
    private NfcAdapter mNfcAdapter;
    private Uri mSelectedImage;
    
    private TextView mUriName;
    private ImageView mPreviewImage;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        mUriName = (TextView) findViewById(R.id.text_uri);
        mPreviewImage =
                (ImageView) findViewById(R.id.image_preview);
        
        // Check for available NFC Adapter
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        if (mNfcAdapter == null) {
            mUriName.setText("NFC not available on this device.");
        } else {
            // Register callback to set NDEF message
            mNfcAdapter.setBeamPushUrisCallback(this, this);
            // Register callback for message-sent success
            mNfcAdapter.setOnNdefPushCompleteCallback(this, this);
        }
    }
    
    @Override
    protected void onActivityResult(int requestCode,
            int resultCode, Intent data) {
        if (requestCode == PICK_IMAGE && resultCode == RESULT_OK
                && data != null) {
            mUriName.setText( data.getData().toString() );
            mSelectedImage = data.getData();
        }
    }
    
    @Override
    public void onResume() {
        super.onResume();
        //Check to see that the Activity started due to
        // an Android Beam
        if (Intent.ACTION_VIEW.equals(getIntent().getAction())) {
            processIntent(getIntent());
        }
    }
 
    @Override
    public void onNewIntent(Intent intent) {
        // onResume gets called after this to handle the intent
        setIntent(intent);
    }
 
    void processIntent(Intent intent) {
        Uri data = intent.getData();
        if(data != null) {
            mPreviewImage.setImageURI(data);
        } else {
            mUriName.setText("Received Invalid Image Uri");
        }
    }
    
    public void onSelectClick(View v) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        startActivityForResult(intent, PICK_IMAGE);
    }
 
    @Override
    public Uri[] createBeamUris(NfcEvent event) {
        if (mSelectedImage == null) {
            return null;
        }
        return new Uri[] {mSelectedImage};
    }
    
    @Override
    public void onNdefPushComplete(NfcEvent event) {
        //This callback happens on a binder thread, don't update
        // the UI directly from this method. This is a good time
        // to tell your user they don't need to hold
        // their phones together anymore!
        Log.i(TAG, "Push Complete!");
    }
}

This example uses CreateBeamUrisCallback, which allows an application to construct an array of Uri instances pointing to content you would like to transmit. Android will do the work of negotiating the initial connection over NFC but will then drop to a more suitable connection such as Bluetooth or WiFi Direct to finish the larger transfers.

In this case, the data on the receiving device is launched using the system’s standard Intent.ACTION_VIEW action, so it is not necessary to load the application on both devices. However, our application does filter for ACTION_VIEW so the receiving device could use it to view the received image content if the user prefers.

Here, the user is asked to select an image from the device to transfer, and then the Uri of that content is displayed once selected. As soon as the user touches that device to another, the same Touch to Beam UI (see Figure 4-4) displays, and the transfer begins when the screen is tapped.

9781430263227_Fig04-04.jpg

Figure 4-4. Activity with Touch to Beam activated

Once the NFC portion of the transfer is complete, the onNdefPushComplete() method is called on the sending device. At this point, the transfer has moved to another connection, so the users don’t need to hold their phones together anymore.

The receiving device will display a progress notification in the system’s window shade while the file is transferring. When the transfer is complete, the user can tap on the notification to view the content. If this application is chosen as the content viewer, the image will be shown in our application’s ImageView. One possible disadvantage to registering your application with such a generic Intent is that every application on the device can then ask your application to view images, so choose your filters wisely!

4-14. Connecting over USB

Problem

Your application needs to communicate with a USB device for the purposes of control or transferring data.

Solution

(API Level 12)

Android has built-in support for devices that contain USB Host circuitry to allow them to enumerate and communicate with connected USB devices. USBManager is the system service that provides applications access to any external devices connected via USB, and we are going to see how you can use that service to establish a connection from your application.

USB Host circuitry is becoming more common on devices, but it is still rare. Initially, only tablet devices had this capability, but it is growing rapidly and may soon become a commonplace interface on commercial Android handsets as well. However, because of this you will certainly want to include the following element in your application manifest:

<uses-feature android:name="android.hardware.usb.host" />

This will limit your application to devices that have the available hardware to do the communications.

The APIs provided by Android are pretty much direct mirrors of the USB specification, without much in the way of higher-level abstraction. This means that if you would like to use them, you will need at least a basic knowledge of USB and how devices communicate.

USB Overview

Before looking at an example of how Android interacts with USB devices, let’s take a moment to define some USB terms:

  • Endpoint: The smallest building block of a USB device. These are what your application eventually connects to for the purpose of sending and receiving data. They can take the form of four main types:
    • Control: Used for configuration and status commands. Every device has at least one control endpoint, called endpoint 0, that is not attached to any interface.
    • Interrupt: Used for small, high-priority control commands.
    • Bulk: Large data transfer. Commonly found in bidirectional pair (1 IN and 1 OUT).
    • Isochronous: Used for real-time data transfer such as audio. Not supported by the latest Android SDK as of this writing.
  • Interface: A collection of endpoints to represent a “logical” device.
    • Physical USB devices can manifest themselves to the host as multiple logical devices, and they do this by exposing multiple interfaces.
  • Configuration: Collection of one or more interfaces. The USB protocol enforces that only one configuration can be active at any one time on a device. In fact, most devices have only one configuration at all. Think of this as the device’s operating mode.

How It Works

Listings 4-46 and 4-47 show examples that use UsbManager to inspect devices connected over USB and then uses control transfers to further query the configuration.

Listing 4-46. res/layout/main.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <Button
        android:id="@+id/button_connect"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Connect"
        android:onClick="onConnectClick" />
    <TextView
        android:id="@+id/text_status"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/text_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
 
</LinearLayout>

Listing 4-47. Activity on USB Host Querying Devices

public class USBActivity extends Activity {
    private static final String TAG = "UsbHost";
 
    TextView mDeviceText, mDisplayText;
    Button mConnectButton;
    
    UsbManager mUsbManager;
    UsbDevice mDevice;
    PendingIntent mPermissionIntent;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        mDeviceText = (TextView) findViewById(R.id.text_status);
        mDisplayText = (TextView) findViewById(R.id.text_data);
        mConnectButton =
            (Button) findViewById(R.id.button_connect);
        
        mUsbManager =
            (UsbManager) getSystemService(Context.USB_SERVICE);
    }
 
    @Override
    protected void onResume() {
        super.onResume();
        mPermissionIntent =
            PendingIntent.getBroadcast(this, 0,
                new Intent(ACTION_USB_PERMISSION), 0);
        IntentFilter filter =
            new IntentFilter(ACTION_USB_PERMISSION);
        registerReceiver(mUsbReceiver, filter);
 
        //Check currently connected devices
        updateDeviceList();
    }
 
    @Override
    protected void onPause() {
        super.onPause();
        unregisterReceiver(mUsbReceiver);
    }
 
    public void onConnectClick(View v) {
        if (mDevice == null) {
            return;
        }
        mDisplayText.setText("---");
        
        //This will either prompt the user with a grant permission
        // dialog, or immediately fire the ACTION_USB_PERMISSION
        // broadcast if the user has already granted it to us.
        mUsbManager.requestPermission(mDevice, mPermissionIntent);
    }
    
    /*
     * Receiver to catch user permission responses, which are
     * required in order to actually interact with a connected
     * device.
     */
    private static final String ACTION_USB_PERMISSION =
            "com.android.recipes.USB_PERMISSION";
    private final BroadcastReceiver mUsbReceiver =
            new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (ACTION_USB_PERMISSION.equals(action)) {
                UsbDevice device =
                    (UsbDevice) intent.getParcelableExtra(
                        UsbManager.EXTRA_DEVICE);
 
                if (intent.getBooleanExtra(
                    UsbManager.EXTRA_PERMISSION_GRANTED, false)
                    && device != null) {
                        //Query the device's descriptor
                        getDeviceStatus(device);
                } else {
                    Log.d(TAG, "permission denied for " + device);
                }
            }
        }
    };
 
    //Type: Indicates whether this is a read or write
    // Matches USB_ENDPOINT_DIR_MASK for either IN or OUT
    private static final int REQUEST_TYPE = 0x80;
    //Request: GET_CONFIGURATION_DESCRIPTOR = 0x06
    private static final int REQUEST = 0x06;
    //Value: Descriptor Type (High) and Index (Low)
    // Configuration Descriptor = 0x2
    // Index = 0x0 (First configuration)
    private static final int REQ_VALUE = 0x200;
    private static final int REQ_INDEX = 0x00;
    private static final int LENGTH = 64;
 
    /*
     * Initiate a control transfer to request the first
     * configuration descriptor of the device.
     */
    private void getDeviceStatus(UsbDevice device) {
        UsbDeviceConnection connection =
                mUsbManager.openDevice(device);
        //Create a sufficiently large buffer for incoming data
        byte[] buffer = new byte[LENGTH];
        connection.controlTransfer(REQUEST_TYPE, REQUEST,
                REQ_VALUE, REQ_INDEX, buffer, LENGTH, 2000);
        //Parse received data into a description
        String description = parseConfigDescriptor(buffer);
        
        mDisplayText.setText(description);
        connection.close();
    }
    
    /*
     * Parse the USB configuration descriptor response per the
     * USB Specification.  Return a printable description of
     * the connected device.
     */
    private static final int DESC_SIZE_CONFIG = 9;
    private String parseConfigDescriptor(byte[] buffer) {
        StringBuilder sb = new StringBuilder();
        //Parse configuration descriptor header
        int totalLength = (buffer[3] &0xFF) << 8;
        totalLength += (buffer[2] & 0xFF);
        //Interface count
        int numInterfaces = (buffer[5] & 0xFF);
        //Configuration attributes
        int attributes = (buffer[7] & 0xFF);
        //Power is given in 2mA increments
        int maxPower = (buffer[8] & 0xFF) * 2;
        
        sb.append("Configuration Descriptor: ");
        sb.append("Length: " + totalLength + " bytes ");
        sb.append(numInterfaces + " Interfaces ");
        sb.append(String.format("Attributes:%s%s%s ",
            (attributes & 0x80) == 0x80 ? " BusPowered" : "",
            (attributes & 0x40) == 0x40 ? " SelfPowered" : "",
            (attributes & 0x20) == 0x20 ? " RemoteWakeup" : ""));
        sb.append("Max Power: " + maxPower + "mA ");
        
        //The rest of the descriptor is interfaces and endpoints
        int index = DESC_SIZE_CONFIG;
        while (index < totalLength) {
            //Read length and type
            int len = (buffer[index] & 0xFF);
            int type = (buffer[index+1] & 0xFF);
            switch (type) {
            case 0x04: //Interface Descriptor
                int intfNumber = (buffer[index+2] & 0xFF);
                int numEndpoints = (buffer[index+4] & 0xFF);
                int intfClass = (buffer[index+5] & 0xFF);
                
                sb.append( String.format(
                        "- Interface %d, %s, %d Endpoints ",
                        intfNumber,
                        nameForClass(intfClass),
                        numEndpoints) );
                break;
            case 0x05: //Endpoint Descriptor
                int endpointAddr = ((buffer[index+2] & 0xFF));
                //Number is lower 4 bits
                int endpointNum = (endpointAddr & 0x0F);
                //Direction is high bit
                int direction = (endpointAddr & 0x80);
                
                int endpointAttrs = (buffer[index+3] & 0xFF);
                //Type is the lower two bits
                int endpointType = (endpointAttrs & 0x3);
                
                sb.append(String.format("-- Endpoint %d, %s %s ",
                        endpointNum,
                        nameForEndpointType(endpointType),
                        nameForDirection(direction) ));
                break;
            }
            //Advance to next descriptor
            index += len;
        }
        
        return sb.toString();
    }
    
    private void updateDeviceList() {
        HashMap<String, UsbDevice> connectedDevices =
                mUsbManager.getDeviceList();
        if (connectedDevices.isEmpty()) {
            mDevice = null;
            mDeviceText.setText("No Devices Currently Connected");
            mConnectButton.setEnabled(false);
        } else {
            StringBuilder builder = new StringBuilder();
            for (UsbDevice device : connectedDevices.values()) {
                //Use the last device detected (if multiple)
                // to open
                mDevice = device;
                builder.append(readDevice(device));
                builder.append(" ");
            }
            mDeviceText.setText(builder.toString());
            mConnectButton.setEnabled(true);
        }
    }
 
    /*
     * Enumerate the endpoints and interfaces on the connected
     * device. We do not need permission to do anything here, it
     * is all "publicly available" until we try to connect to
     * an actual device.
     */
    private String readDevice(UsbDevice device) {
        StringBuilder sb = new StringBuilder();
        sb.append("Device Name: " + device.getDeviceName()
                + " ");
        sb.append( String.format(
                "Device Class: %s -> Subclass: 0x%02x -> "
                    + "Protocol: 0x%02x ",
                nameForClass(device.getDeviceClass()),
                device.getDeviceSubclass(),
                device.getDeviceProtocol())
        );
 
        for (int i = 0; i < device.getInterfaceCount(); i++) {
            UsbInterface intf = device.getInterface(i);
            sb.append( String.format(
                    "+--Interface %d Class: %s -> "
                       + "Subclass: 0x%02x -> Protocol: 0x%02x ",
                    intf.getId(),
                    nameForClass(intf.getInterfaceClass()),
                    intf.getInterfaceSubclass(),
                    intf.getInterfaceProtocol())
            );
 
            for (int j = 0; j < intf.getEndpointCount(); j++) {
                UsbEndpoint endpoint = intf.getEndpoint(j);
                sb.append( String.format(
                        "  +---Endpoint %d: %s %s ",
                        endpoint.getEndpointNumber(),
                        nameForEndpointType(endpoint.getType()),
                        nameForDirection(endpoint.getDirection()))
                );
            }
        }
 
        return sb.toString();
    }
 
    /* Helper Methods to Provide Readable Names for USB Constants
     */
    
    private String nameForClass(int classType) {
        switch (classType) {
        case UsbConstants.USB_CLASS_APP_SPEC:
            return String.format(
                    "Application Specific 0x%02x", classType);
        case UsbConstants.USB_CLASS_AUDIO:
            return "Audio";
        case UsbConstants.USB_CLASS_CDC_DATA:
            return "CDC Control";
        case UsbConstants.USB_CLASS_COMM:
            return "Communications";
        case UsbConstants.USB_CLASS_CONTENT_SEC:
            return "Content Security";
        case UsbConstants.USB_CLASS_CSCID:
            return "Content Smart Card";
        case UsbConstants.USB_CLASS_HID:
            return "Human Interface Device";
        case UsbConstants.USB_CLASS_HUB:
            return "Hub";
        case UsbConstants.USB_CLASS_MASS_STORAGE:
            return "Mass Storage";
        case UsbConstants.USB_CLASS_MISC:
            return "Wireless Miscellaneous";
        case UsbConstants.USB_CLASS_PER_INTERFACE:
            return "(Defined Per Interface)";
        case UsbConstants.USB_CLASS_PHYSICA:
            return "Physical";
        case UsbConstants.USB_CLASS_PRINTER:
            return "Printer";
        case UsbConstants.USB_CLASS_STILL_IMAGE:
            return "Still Image";
        case UsbConstants.USB_CLASS_VENDOR_SPEC:
            return String.format(
                    "Vendor Specific 0x%02x", classType);
        case UsbConstants.USB_CLASS_VIDEO:
            return "Video";
        case UsbConstants.USB_CLASS_WIRELESS_CONTROLLER:
            return "Wireless Controller";
        default:
            return String.format("0x%02x", classType);
        }
    }
 
    private String nameForEndpointType(int type) {
        switch (type) {
        case UsbConstants.USB_ENDPOINT_XFER_BULK:
            return "Bulk";
        case UsbConstants.USB_ENDPOINT_XFER_CONTROL:
            return "Control";
        case UsbConstants.USB_ENDPOINT_XFER_INT:
            return "Interrupt";
        case UsbConstants.USB_ENDPOINT_XFER_ISOC:
            return "Isochronous";
        default:
            return "Unknown Type";
        }
    }
 
    private String nameForDirection(int direction) {
        switch (direction) {
        case UsbConstants.USB_DIR_IN:
            return "IN";
        case UsbConstants.USB_DIR_OUT:
            return "OUT";
        default:
            return "Unknown Direction";
        }
    }
}

When the activity first comes into the foreground, it registers a BroadcastReceiver with a custom action (which we’ll discuss in more detail shortly), and it queries the list of currently connected devices by using UsbManager.getDeviceList(), which returns a HashMap of UsbDevice items that we can iterate over and interrogate. For each device connected, we query each interface and endpoint, building a description string to print to the user about what this device is. We then print all that data to the user interface.

Note  This application, as it stands, does not require any manifest permissions. We do not need to declare a permission simply to query information about devices connected to the host.

You can see that UsbManager provides APIs to inspect just about every piece of information you would need to discover if a connected device is the one you are interested in communicating with. All standard definitions for device classes, endpoint types, and transfer directions are also defined in UsbConstants, so you can match the types you want without defining all of this yourself.

So, what about that BroadcastReceiver we registered? The remainder of this example code takes action when the user presses the Connect button on the screen. At this point, we would like to talk to the connected device, which is an operation that does require user permission. Here, when the user clicks the button, we call UsbManager.requestPermission() to ask the user if we can connect. If permission has not yet been granted, the user will see a dialog box asking him or her to grant permission to connect.

Upon saying yes, the PendingIntent passed along to the method will get fired. In our example, that Intent was a broadcast with a custom action string we defined, so this will trigger onReceive() in that BroadcastReceiver; any subsequent calls to requestPermission() will immediately trigger the receiver as well. Inside the receiver, we check to make sure that the result was a permission-granted response, and we attempt to open a connection to the device with UsbManager.openDevice(), which returns a UsbDeviceConnection instance when successful.

With a valid connection made, we request some more detailed information about the device by requesting its configuration descriptor via a control transfer. Control transfers are requests always made on endpoint 0 of the device. A configuration descriptor contains information about the configuration as well as each interface and endpoint, so its length is variable. We allocate a decent-sized buffer to ensure we capture everything.

Upon returning from controlTransfer(), the buffer is filled with the response data. Our application then processes the bytes, determining some more information about the device, including its maximum power draw and whether the device is configured to be powered from the USB post (bus-powered) or by an external source (self-powered). This example parses out only a fraction of the useful information that can be found inside these descriptors. Once again, all the parsed data is put into a string report and displayed to the user interface.

Much of the data read in the first section from the framework APIs and in the second section directly from the device is the same and should match up 1:1 between the two text reports displayed on the screen. One thing to note is that this application works only if the device is already connected when the application runs: it will not be notified if a connection happens while it is in the foreground. We will look at how to handle that scenario in the next section.

Getting Notified of Device Connections

In order for Android to notify your application when a particular device is connected, you need to register the device types you are interested in with an <intent-filter> in the manifest. Take a look at Listings 4-48 and 4-49 to see how this is done.

Listing 4-48. Partial AndroidManifest.xml

<activity
   android:name=".USBActivity"
   android:label="@string/title_activity_usb" >
   <intent-filter>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
   <intent-filter>
      <action android:name=
         "android.hardware.usb.action.USB_DEVICE_ATTACHED" />
   </intent-filter>
 
   <meta-data android:name=
      "android.hardware.usb.action.USB_DEVICE_ATTACHED"
      android:resource="@xml/device_filter" />
</activity>

Listing 4-49. res/xml/device_filter.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-device vendor-id="5432" product-id="9876" />
</resources>

The activity you want to launch with a connection has a filter added to it with the USB_DEVICE_ATTACHED action string and with some XML metadata describing the devices you are interested in. There are several device attribute fields you can place into <usb-device> to filter which connection events notify your application:

  • vendor-id
  • product-id
  • class
  • subclass
  • protocol

You can define as many of these as necessary to fit your application. For example, if you want to communicate with only one specific device, you might define both vendor-id and product-id as the example code did. If you are more interested in all devices of a given type (say, all mass-storage devices), you might define only the class attribute. It is even allowable to define no attributes, and have your application match on any device connected!

Summary

Connecting an Android application to the Web and web services is a great way to add user value in today’s connected world. Android’s framework for connecting to the Web and other remote hosts makes adding this functionality straightforward. We’ve explored how to bring the standards of the Web into your application, using HTML and JavaScript to interact with the user, but within a native context. You also saw how to use Android to download content from remote servers and consume it in your application. We also showed that a web server is not the only host worth connecting to, by using Bluetooth, NFC, and SMS to communicate directly from one device to another. In the next chapter, we will look at using the tools that Android provides to interact with a device’s hardware resources.

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

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