Building Apps with Connectivity & the Cloud
These classes teach you how to connect your app to the world beyond the user‘s device. You‘ll learn how to connect to other devices in the area, connect to the Internet, backup and sync your app‘s data, and more.
-
Connecting Devices Wirelessly
How to find and connect to local devices using Network Service Discovery and how to create peer-to-peer connections with Wi-Fi.
-
Performing Network Operations
How to create a network connection, monitor the connection for changes in connectivity, and perform transactions with XML data.
-
Transferring Data Without Draining the Battery
How to minimize your app‘s impact on the battery when performing downloads and other network transactions.
-
Syncing to the Cloud
How to sync and back up app and user data to remote web services in the cloud and how to restore the data back to multiple devices.
-
Resolving Cloud Save Conflicts
How to design a robust conflict resolution strategy for apps that save data to the cloud.
-
Transferring Data Using Sync Adapters
How to transfer data between the cloud and the device using the Android sync adapter framework
-
Transmitting Network Data Using Volley
How to perform fast, scalable UI operations over the network using Volley
Connecting Devices Wirelessly
DEPENDENCIES AND PREREQUISITES
- Android 4.1 or higher
YOU SHOULD ALSO READ
Besides enabling communication with the cloud, Android‘s wireless APIs also enable communication with other devices on the same local network, and even devices which are not on a network, but are physically nearby. The addition of Network Service Discovery (NSD) takes this further by allowing an application to seek out a nearby device running services with which it can communicate. Integrating this functionality into your application helps you provide a wide range of features, such as playing games with users in the same room, pulling images from a networked NSD-enabled webcam, or remotely logging into other machines on the same network.
This class describes the key APIs for finding and connecting to other devices from your application. Specifically, it describes the NSD API for discovering available services and the Wi-Fi Peer-to-Peer (P2P) API for doing peer-to-peer wireless connections. This class also shows you how to use NSD and Wi-Fi P2P in combination to detect the services offered by a device and connect to the device when neither device is connected to a network.
Lessons
- Using Network Service Discovery
- Learn how to broadcast services offered by your own application, discover services offered on the local network, and use NSD to determine the connection details for the service you wish to connect to.
- Creating P2P Connections with Wi-Fi
- Learn how to fetch a list of nearby peer devices, create an access point for legacy devices, and connect to other devices capable of Wi-Fi P2P connections.
- Using Wi-Fi P2P for Service Discovery
Learn how to discover services published by nearby devices without being on the same network, using Wi-Fi
Using Network Service Discovery
THIS LESSON TEACHES YOU HOW TO
- Register Your Service on the Network
- Discover Services on the Network
- Connect to Services on the Network
- Unregister Your Service on Application Close
TRY IT OUT
NsdChat.zip
Adding Network Service Discovery (NSD) to your app allows your users to identify other devices on the local network that support the services your app requests. This is useful for a variety of peer-to-peer applications such as file sharing or multi-player gaming. Android‘s NSD APIs simplify the effort required for you to implement such features.
This lesson shows you how to build an application that can broadcast its name and connection information to the local network and scan for information from other applications doing the same. Finally, this lesson shows you how to connect to the same application running on another device.
Register Your Service on the Network
Note: This step is optional. If you don‘t care about broadcasting your app‘s services over the local network, you can skip forward to the next section, Discover Services on the Network.
To register your service on the local network, first create a NsdServiceInfo
object. This object provides the information that other devices on the network use when they‘re deciding whether to connect to your service.
public void registerService(int port) { // Create the NsdServiceInfo object, and populate it. NsdServiceInfo serviceInfo = new NsdServiceInfo(); // The name is subject to change based on conflicts // with other services advertised on the same network. serviceInfo.setServiceName("NsdChat"); serviceInfo.setServiceType("_http._tcp."); serviceInfo.setPort(port); ....}
This code snippet sets the service name to "NsdChat". The name is visible to any device on the network that is using NSD to look for local services. Keep in mind that the name must be unique for any service on the network, and Android automatically handles conflict resolution. If two devices on the network both have the NsdChat application installed, one of them changes the service name automatically, to something like "NsdChat (1)".
The second parameter sets the service type, specifies which protocol and transport layer the application uses. The syntax is "_<protocol>._<transportlayer>". In the code snippet, the service uses HTTP protocol running over TCP. An application offering a printer service (for instance, a network printer) would set the service type to "_ipp._tcp".
Note: The International Assigned Numbers Authority (IANA) manages a centralized, authoritative list of service types used by service discovery protocols such as NSD and Bonjour. You can download the list from the IANA list of service names and port numbers. If you intend to use a new service type, you should reserve it by filling out the IANA Ports and Service registration form.
When setting the port for your service, avoid hardcoding it as this conflicts with other applications. For instance, assuming that your application always uses port 1337 puts it in potential conflict with other installed applications that use the same port. Instead, use the device‘s next available port. Because this information is provided to other apps by a service broadcast, there‘s no need for the port your application uses to be known by other applications at compile-time. Instead, the applications can get this information from your service broadcast, right before connecting to your service.
If you‘re working with sockets, here‘s how you can initialize a socket to any available port simply by setting it to 0.
public void initializeServerSocket() { // Initialize a server socket on the next available port. mServerSocket = new ServerSocket(0); // Store the chosen port. mLocalPort = mServerSocket.getLocalPort(); ...}
Now that you‘ve defined the NsdServiceInfo
object, you need to implement the RegistrationListener
interface. This interface contains callbacks used by Android to alert your application of the success or failure of service registration and unregistration.
public void initializeRegistrationListener() { mRegistrationListener = new NsdManager.RegistrationListener() { @Override public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) { // Save the service name. Android may have changed it in order to // resolve a conflict, so update the name you initially requested // with the name Android actually used. mServiceName = NsdServiceInfo.getServiceName(); } @Override public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) { // Registration failed! Put debugging code here to determine why. } @Override public void onServiceUnregistered(NsdServiceInfo arg0) { // Service has been unregistered. This only happens when you call // NsdManager.unregisterService() and pass in this listener. } @Override public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) { // Unregistration failed. Put debugging code here to determine why. } };}
Now you have all the pieces to register your service. Call the method registerService()
.
Note that this method is asynchronous, so any code that needs to run after the service has been registered must go in the onServiceRegistered()
method.
public void registerService(int port) { NsdServiceInfo serviceInfo = new NsdServiceInfo(); serviceInfo.setServiceName("NsdChat"); serviceInfo.setServiceType("_http._tcp."); serviceInfo.setPort(port); mNsdManager = Context.getSystemService(Context.NSD_SERVICE); mNsdManager.registerService( serviceInfo, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);}
Discover Services on the Network
The network is teeming with life, from the beastly network printers to the docile network webcams, to the brutal, fiery battles of nearby tic-tac-toe players. The key to letting your application see this vibrant ecosystem of functionality is service discovery. Your application needs to listen to service broadcasts on the network to see what services are available, and filter out anything the application can‘t work with.
Service discovery, like service registration, has two steps: setting up a discovery listener with the relevant callbacks, and making a single asynchronous API call to discoverServices()
.
First, instantiate an anonymous class that implements NsdManager.DiscoveryListener
. The following snippet shows a simple example:
public void initializeDiscoveryListener() { // Instantiate a new DiscoveryListener mDiscoveryListener = new NsdManager.DiscoveryListener() { // Called as soon as service discovery begins. @Override public void onDiscoveryStarted(String regType) { Log.d(TAG, "Service discovery started"); } @Override public void onServiceFound(NsdServiceInfo service) { // A service was found! Do something with it. Log.d(TAG, "Service discovery success" + service); if (!service.getServiceType().equals(SERVICE_TYPE)) { // Service type is the string containing the protocol and // transport layer for this service. Log.d(TAG, "Unknown Service Type: " + service.getServiceType()); } else if (service.getServiceName().equals(mServiceName)) { // The name of the service tells the user what they‘d be // connecting to. It could be "Bob‘s Chat App". Log.d(TAG, "Same machine: " + mServiceName); } else if (service.getServiceName().contains("NsdChat")){ mNsdManager.resolveService(service, mResolveListener); } } @Override public void onServiceLost(NsdServiceInfo service) { // When the network service is no longer available. // Internal bookkeeping code goes here. Log.e(TAG, "service lost" + service); } @Override public void onDiscoveryStopped(String serviceType) { Log.i(TAG, "Discovery stopped: " + serviceType); } @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { Log.e(TAG, "Discovery failed: Error code:" + errorCode); mNsdManager.stopServiceDiscovery(this); } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { Log.e(TAG, "Discovery failed: Error code:" + errorCode); mNsdManager.stopServiceDiscovery(this); } };}
The NSD API uses the methods in this interface to inform your application when discovery is started, when it fails, and when services are found and lost (lost means "is no longer available"). Notice that this snippet does several checks when a service is found.
- The service name of the found service is compared to the service name of the local service to determine if the device just picked up its own broadcast (which is valid).
- The service type is checked, to verify it‘s a type of service your application can connect to.
- The service name is checked to verify connection to the correct application.
Checking the service name isn‘t always necessary, and is only relevant if you want to connect to a specific application. For instance, the application might only want to connect to instances of itself running on other devices. However, if the application wants to connect to a network printer, it‘s enough to see that the service type is "_ipp._tcp".
After setting up the listener, call discoverServices()
, passing in the service type your application should look for, the discovery protocol to use, and the listener you just created.
mNsdManager.discoverServices( SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
Connect to Services on the Network
When your application finds a service on the network to connect to, it must first determine the connection information for that service, using the
resolveService()
method. Implement a NsdManager.ResolveListener
to pass into this method, and use it to get a NsdServiceInfo
containing the connection information.
public void initializeResolveListener() { mResolveListener = new NsdManager.ResolveListener() { @Override public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { // Called when the resolve fails. Use the error code to debug. Log.e(TAG, "Resolve failed" + errorCode); } @Override public void onServiceResolved(NsdServiceInfo serviceInfo) { Log.e(TAG, "Resolve Succeeded. " + serviceInfo); if (serviceInfo.getServiceName().equals(mServiceName)) { Log.d(TAG, "Same IP."); return; } mService = serviceInfo; int port = mService.getPort(); InetAddress host = mService.getHost(); } };}
Once the service is resolved, your application receives detailed service information including an IP address and port number. This is everything you need to create your own network connection to the service.
Unregister Your Service on Application Close
It‘s important to enable and disable NSD functionality as appropriate during the application‘s lifecycle. Unregistering your application when it closes down helps prevent other applications from thinking it‘s still active and attempting to connect to it. Also, service discovery is an expensive operation, and should be stopped when the parent Activity is paused, and re-enabled when the Activity is resumed. Override the lifecycle methods of your main Activity and insert code to start and stop service broadcast and discovery as appropriate.
//In your application‘s Activity @Override protected void onPause() { if (mNsdHelper != null) { mNsdHelper.tearDown(); } super.onPause(); } @Override protected void onResume() { super.onResume(); if (mNsdHelper != null) { mNsdHelper.registerService(mConnection.getLocalPort()); mNsdHelper.discoverServices(); } } @Override protected void onDestroy() { mNsdHelper.tearDown(); mConnection.tearDown(); super.onDestroy(); } // NsdHelper‘s tearDown method public void tearDown() { mNsdManager.unregisterService(mRegistrationListener); mNsdManager.stopServiceDiscovery(mDiscoveryListener); }
Creating P2P Connections with Wi-Fi
THIS LESSON TEACHES YOU HOW TO
- Set Up Application Permissions
- Set Up the Broadcast Receiver and Peer-to-Peer Manager
- Initiate Peer Discovery
- Fetch the List of Peers
- Connect to a Peer
YOU SHOULD ALSO READ
The Wi-Fi peer-to-peer (P2P) APIs allow applications to connect to nearby devices without needing to connect to a network or hotspot (Android‘s Wi-Fi P2P framework complies with the Wi-Fi Direct™ certification program). Wi-Fi P2P allows your application to quickly find and interact with nearby devices, at a range beyond the capabilities of Bluetooth.
This lesson shows you how to find and connect to nearby devices using Wi-Fi P2P.
Set Up Application Permissions
In order to use Wi-Fi P2P, add the
CHANGE_WIFI_STATE
,ACCESS_WIFI_STATE
, and INTERNET
permissions to your manifest. Wi-Fi P2P doesn‘t require an internet connection, but it does use standard Java sockets, which require the INTERNET
permission. So you need the following permissions to use Wi-Fi P2P.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.nsdchat" ... <uses-permission android:required="true" android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:required="true" android:name="android.permission.CHANGE_WIFI_STATE"/> <uses-permission android:required="true" android:name="android.permission.INTERNET"/> ...
Set Up a Broadcast Receiver and Peer-to-Peer Manager
To use Wi-Fi P2P, you need to listen for broadcast intents that tell your application when certain events have occurred. In your application, instantiate an
IntentFilter
and set it to listen for the following:
WIFI_P2P_STATE_CHANGED_ACTION
- Indicates whether Wi-Fi P2P is enabled
WIFI_P2P_PEERS_CHANGED_ACTION
- Indicates that the available peer list has changed.
WIFI_P2P_CONNECTION_CHANGED_ACTION
- Indicates the state of Wi-Fi P2P connectivity has changed.
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
- Indicates this device‘s configuration details have changed.
private final IntentFilter intentFilter = new IntentFilter();...@Overridepublic void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Indicates a change in the Wi-Fi P2P status. intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION); // Indicates a change in the list of available peers. intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION); // Indicates the state of Wi-Fi P2P connectivity has changed. intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION); // Indicates this device‘s details have changed. intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION); ...}
At the end of the onCreate()
method, get an instance of the WifiP2pManager
, and call its initialize()
method. This method returns a WifiP2pManager.Channel
object, which you‘ll use later to connect your app to the Wi-Fi P2P framework.
@Override Channel mChannel; public void onCreate(Bundle savedInstanceState) { .... mManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE); mChannel = mManager.initialize(this, getMainLooper(), null);}
Now create a new BroadcastReceiver
class that you‘ll use to listen for changes to the System‘s Wi-Fi P2P state. In the onReceive()
method, add a condition to handle each P2P state change listed above.
@Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) { // Determine if Wifi P2P mode is enabled or not, alert // the Activity. int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1); if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) { activity.setIsWifiP2pEnabled(true); } else { activity.setIsWifiP2pEnabled(false); } } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) { // The peer list has changed! We should probably do something about // that. } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) { // Connection state changed! We should probably do something about // that. } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) { DeviceListFragment fragment = (DeviceListFragment) activity.getFragmentManager() .findFragmentById(R.id.frag_list); fragment.updateThisDevice((WifiP2pDevice) intent.getParcelableExtra( WifiP2pManager.EXTRA_WIFI_P2P_DEVICE)); } }
Finally, add code to register the intent filter and broadcast receiver when your main activity is active, and unregister them when the activity is paused. The best place to do this is the onResume()
and onPause()
methods.
/** register the BroadcastReceiver with the intent values to be matched */ @Override public void onResume() { super.onResume(); receiver = new WiFiDirectBroadcastReceiver(mManager, mChannel, this); registerReceiver(receiver, intentFilter); } @Override public void onPause() { super.onPause(); unregisterReceiver(receiver); }
Initiate Peer Discovery
To start searching for nearby devices with Wi-Fi P2P, call
discoverPeers()
. This method takes the following arguments:
- The
WifiP2pManager.Channel
you received back when you initialized the peer-to-peer mManager - An implementation of
WifiP2pManager.ActionListener
with methods the system invokes for successful and unsuccessful discovery.
mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() { @Override public void onSuccess() { // Code for when the discovery initiation is successful goes here. // No services have actually been discovered yet, so this method // can often be left blank. Code for peer discovery goes in the // onReceive method, detailed below. } @Override public void onFailure(int reasonCode) { // Code for when the discovery initiation fails goes here. // Alert the user that something went wrong. }});
Keep in mind that this only initiates peer discovery. The discoverPeers()
method starts the discovery process and then immediately returns. The system notifies you if the peer discovery process is successfully initiated by calling methods in the provided action listener. Also, discovery will remain active until a connection is initiated or a P2P group is formed.
Fetch the List of Peers
Now write the code that fetches and processes the list of peers. First implement the
WifiP2pManager.PeerListListener
interface, which provides information about the peers that Wi-Fi P2P has detected. The following code snippet illustrates this.
private List peers = new ArrayList(); ... private PeerListListener peerListListener = new PeerListListener() { @Override public void onPeersAvailable(WifiP2pDeviceList peerList) { // Out with the old, in with the new. peers.clear(); peers.addAll(peerList.getDeviceList()); // If an AdapterView is backed by this data, notify it // of the change. For instance, if you have a ListView of available // peers, trigger an update. ((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged(); if (peers.size() == 0) { Log.d(WiFiDirectActivity.TAG, "No devices found"); return; } } }
Now modify your broadcast receiver‘s onReceive()
method to call requestPeers()
when an intent with the actionWIFI_P2P_PEERS_CHANGED_ACTION
is received. You need to pass this listener into the receiver somehow. One way is to send it as an argument to the broadcast receiver‘s constructor.
public void onReceive(Context context, Intent intent) { ... else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) { // Request available peers from the wifi p2p manager. This is an // asynchronous call and the calling activity is notified with a // callback on PeerListListener.onPeersAvailable() if (mManager != null) { mManager.requestPeers(mChannel, peerListListener); } Log.d(WiFiDirectActivity.TAG, "P2P peers changed"); }...}
Now, an intent with the action WIFI_P2P_PEERS_CHANGED_ACTION
intent will trigger a request for an updated peer list.
Connect to a Peer
In order to connect to a peer, create a new
WifiP2pConfig
object, and copy data into it from the WifiP2pDevice
representing the device you want to connect to. Then call the connect()
method.
@Override public void connect() { // Picking the first device found on the network. WifiP2pDevice device = peers.get(0); WifiP2pConfig config = new WifiP2pConfig(); config.deviceAddress = device.deviceAddress; config.wps.setup = WpsInfo.PBC; mManager.connect(mChannel, config, new ActionListener() { @Override public void onSuccess() { // WiFiDirectBroadcastReceiver will notify us. Ignore for now. } @Override public void onFailure(int reason) { Toast.makeText(WiFiDirectActivity.this, "Connect failed. Retry.", Toast.LENGTH_SHORT).show(); } }); }
The WifiP2pManager.ActionListener
implemented in this snippet only notifies you when the initiation succeeds or fails. To listen for changes in connection state, implement the WifiP2pManager.ConnectionInfoListener
interface. Its onConnectionInfoAvailable()
callback will notify you when the state of the connection changes. In cases where multiple devices are going to be connected to a single device (like a game with 3 or more players, or a chat app), one device will be designated the "group owner".
@Override public void onConnectionInfoAvailable(final WifiP2pInfo info) { // InetAddress from WifiP2pInfo struct. InetAddress groupOwnerAddress = info.groupOwnerAddress.getHostAddress()); // After the group negotiation, we can determine the group owner. if (info.groupFormed && info.isGroupOwner) { // Do whatever tasks are specific to the group owner. // One common case is creating a server thread and accepting // incoming connections. } else if (info.groupFormed) { // The other device acts as the client. In this case, // you‘ll want to create a client thread that connects to the group // owner. } }
Now go back to the onReceive()
method of the broadcast receiver, and modify the section that listens for aWIFI_P2P_CONNECTION_CHANGED_ACTION
intent. When this intent is received, call requestConnectionInfo()
. This is an asynchronous call, so results will be received by the connection info listener you provide as a parameter.
... } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) { if (mManager == null) { return; } NetworkInfo networkInfo = (NetworkInfo) intent .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO); if (networkInfo.isConnected()) { // We are connected with the other device, request connection // info to find group owner IP mManager.requestConnectionInfo(mChannel, connectionListener); } ...
Using Wi-Fi P2P for Service Discovery
THIS LESSON TEACHES YOU TO
The first lesson in this class, Using Network Service Discovery, showed you how to discover services that are connected to a local network. However, using Wi-Fi Peer-to-Peer (P2P) Service Discovery allows you to discover the services of nearby devices directly, without being connected to a network. You can also advertise the services running on your device. These capabilities help you communicate between apps, even when no local network or hotspot is available.
While this set of APIs is similar in purpose to the Network Service Discovery APIs outlined in a previous lesson, implementing them in code is very different. This lesson shows you how to discover services available from other devices, using Wi-Fi P2P. The lesson assumes that you‘re already familiar with theWi-Fi P2P API.
Set Up the Manifest
In order to use Wi-Fi P2P, add the
CHANGE_WIFI_STATE
, ACCESS_WIFI_STATE
, and INTERNET
permissions to your manifest. Even though Wi-Fi P2P doesn‘t require an Internet connection, it uses standard Java sockets, and using these in Android requires the requested permissions.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.nsdchat" ... <uses-permission android:required="true" android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:required="true" android:name="android.permission.CHANGE_WIFI_STATE"/> <uses-permission android:required="true" android:name="android.permission.INTERNET"/> ...
Add a Local Service
If you‘re providing a local service, you need to register it for service discovery. Once your local service is registered, the framework automatically responds to service discovery requests from peers.
To create a local service:
- Create a
WifiP2pServiceInfo
object. - Populate it with information about your service.
- Call
addLocalService()
to register the local service for service discovery.
private void startRegistration() { // Create a string map containing information about your service. Map<string,string> record = new HashMap<string,string>(); record.put("listenport", String.valueOf(SERVER_PORT)); record.put("buddyname", "John Doe" + (int) (Math.random() * 1000)); record.put("available", "visible"); // Service information. Pass it an instance name, service type // _protocol._transportlayer , and the map containing // information other devices will want once they connect to this one. WifiP2pDnsSdServiceInfo serviceInfo = WifiP2pDnsSdServiceInfo.newInstance("_test", "_presence._tcp", record); // Add the local service, sending the service info, network channel, // and listener that will be used to indicate success or failure of // the request. mManager.addLocalService(channel, serviceInfo, new ActionListener() { @Override public void onSuccess() { // Command successful! Code isn‘t necessarily needed here, // Unless you want to update the UI or add logging statements. } @Override public void onFailure(int arg0) { // Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY } }); }
Discover Nearby Services
Android uses callback methods to notify your application of available services, so the first thing to do is set those up. Create a
WifiP2pManager.DnsSdTxtRecordListener
to listen for incoming records. This record can optionally be broadcast by other devices. When one comes in, copy the device address and any other relevant information you want into a data structure external to the current method, so you can access it later. The following example assumes that the record contains a "buddyname" field, populated with the user‘s identity.
final HashMap<String, String> buddies = new HashMap<String, String>();...private void discoverService() { DnsSdTxtRecordListener txtListener = new DnsSdTxtRecordListener() { @Override /* Callback includes: * fullDomain: full domain name: e.g "printer._ipp._tcp.local." * record: TXT record dta as a map of key/value pairs. * device: The device running the advertised service. */ public void onDnsSdTxtRecordAvailable( String fullDomain, Map<string,string> record, WifiP2pDevice device) { Log.d(TAG, "DnsSdTxtRecord available -" + record.toString()); buddies.put(device.deviceAddress, record.get("buddyname")); } }; ...}
To get the service information, create a WifiP2pManager.DnsSdServiceResponseListener
. This receives the actual description and connection information. The previous code snippet implemented a Map
object to pair a device address with the buddy name. The service response listener uses this to link the DNS record with the corresponding service information. Once both listeners are implemented, add them to the WifiP2pManager
using the setDnsSdResponseListeners()
method.
private void discoverService() {... DnsSdServiceResponseListener servListener = new DnsSdServiceResponseListener() { @Override public void onDnsSdServiceAvailable(String instanceName, String registrationType, WifiP2pDevice resourceType) { // Update the device name with the human-friendly version from // the DnsTxtRecord, assuming one arrived. resourceType.deviceName = buddies .containsKey(resourceType.deviceAddress) ? buddies .get(resourceType.deviceAddress) : resourceType.deviceName; // Add to the custom adapter defined specifically for showing // wifi devices. WiFiDirectServicesList fragment = (WiFiDirectServicesList) getFragmentManager() .findFragmentById(R.id.frag_peerlist); WiFiDevicesAdapter adapter = ((WiFiDevicesAdapter) fragment .getListAdapter()); adapter.add(resourceType); adapter.notifyDataSetChanged(); Log.d(TAG, "onBonjourServiceAvailable " + instanceName); } }; mManager.setDnsSdResponseListeners(channel, servListener, txtListener); ...}
Now create a service request and call addServiceRequest()
. This method also takes a listener to report success or failure.
serviceRequest = WifiP2pDnsSdServiceRequest.newInstance(); mManager.addServiceRequest(channel, serviceRequest, new ActionListener() { @Override public void onSuccess() { // Success! } @Override public void onFailure(int code) { // Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY } });
Finally, make the call to discoverServices()
.
mManager.discoverServices(channel, new ActionListener() { @Override public void onSuccess() { // Success! } @Override public void onFailure(int code) { // Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY if (code == WifiP2pManager.P2P_UNSUPPORTED) { Log.d(TAG, "P2P isn‘t supported on this device."); else if(...) ... } });
If all goes well, hooray, you‘re done! If you encounter problems, remember that the asynchronous calls you‘ve made take an WifiP2pManager.ActionListener
as an argument, and this provides you with callbacks indicating success or failure. To diagnose problems, put debugging code in onFailure()
. The error code provided by the method hints at the problem. Here are the possible error values and what they mean
P2P_UNSUPPORTED
- Wi-Fi P2P isn‘t supported on the device running the app.
BUSY
- The system is to busy to process the request.
ERROR
- The operation failed due to an internal error.
Performing Network Operations
DEPENDENCIES AND PREREQUISITES
- Android 1.6 (API level 4) or higher
- A device that is able to connect to mobile and Wi-Fi networks
YOU SHOULD ALSO READ
- Optimizing Battery Life
- Transferring Data Without Draining the Battery
- Web Apps Overview
- Transmitting Network Data Using Volley
TRY IT OUT
NetworkUsage.zip
This class explains the basic tasks involved in connecting to the network, monitoring the network connection (including connection changes), and giving users control over an app‘s network usage. It also describes how to parse and consume XML data.
This class includes a sample application that illustrates how to perform common network operations. You can download the sample (to the right) and use it as a source of reusable code for your own application.
By going through these lessons, you‘ll have the fundamental building blocks for creating Android applications that download content and parse data efficiently, while minimizing network traffic.
Note: See the class Transmitting Network Data Using Volleyfor information on Volley, an HTTP library that makes networking for Android apps easier and faster. Volley is available through the open AOSP repository. Volley may be able to help you streamline and improve the performance of your app‘s network operations.
Lessons
- Connecting to the Network
- Learn how to connect to the network, choose an HTTP client, and perform network operations outside of the UI thread.
- Managing Network Usage
- Learn how to check a device‘s network connection, create a preferences UI for controlling network usage, and respond to connection changes.
- Parsing XML Data
- Learn how to parse and consume XML data.
Connecting to the Network
THIS LESSON TEACHES YOU TO
- Choose an HTTP Client
- Check the Network Connection
- Perform Network Operations on a Separate Thread
- Connect and Download Data
- Convert the InputStream to a String
YOU SHOULD ALSO READ
- Transmitting Network Data Using Volley
- Optimizing Battery Life
- Transferring Data Without Draining the Battery
- Web Apps Overview
- Application Fundamentals
This lesson shows you how to implement a simple application that connects to the network. It explains some of the best practices you should follow in creating even the simplest network-connected app.
Note that to perform the network operations described in this lesson, your application manifest must include the following permissions:
<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Choose an HTTP Client
Most network-connected Android apps use HTTP to send and receive data. Android includes two HTTP clients:
HttpURLConnection
and Apache HttpClient
. Both support HTTPS, streaming uploads and downloads, configurable timeouts, IPv6, and connection pooling. We recommend usingHttpURLConnection
for applications targeted at Gingerbread and higher. For more discussion of this topic, see the blog postAndroid‘s HTTP Clients.
Check the Network Connection
Before your app attempts to connect to the network, it should check to see whether a network connection is available using
getActiveNetworkInfo()
and isConnected()
. Remember, the device may be out of range of a network, or the user may have disabled both Wi-Fi and mobile data access. For more discussion of this topic, see the lesson Managing Network Usage.
public void myClickHandler(View view) { ... ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); if (networkInfo != null && networkInfo.isConnected()) { // fetch data } else { // display error } ...}
Perform Network Operations on a Separate Thread
Network operations can involve unpredictable delays. To prevent this from causing a poor user experience, always perform network operations on a separate thread from the UI. The
AsyncTask
class provides one of the simplest ways to fire off a new task from the UI thread. For more discussion of this topic, see the blog postMultithreading For Performance.
In the following snippet, the myClickHandler()
method invokes new DownloadWebpageTask().execute(stringUrl)
. The DownloadWebpageTask
class is a subclass of AsyncTask
.DownloadWebpageTask
implements the following AsyncTask
methods:
doInBackground()
executes the methoddownloadUrl()
. It passes the web page URL as a parameter. The methoddownloadUrl()
fetches and processes the web page content. When it finishes, it passes back a result string.onPostExecute()
takes the returned string and displays it in the UI.
public class HttpExampleActivity extends Activity { private static final String DEBUG_TAG = "HttpExample"; private EditText urlText; private TextView textView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); urlText = (EditText) findViewById(R.id.myUrl); textView = (TextView) findViewById(R.id.myText); } // When user clicks button, calls AsyncTask. // Before attempting to fetch the URL, makes sure that there is a network connection. public void myClickHandler(View view) { // Gets the URL from the UI‘s text field. String stringUrl = urlText.getText().toString(); ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); if (networkInfo != null && networkInfo.isConnected()) { new DownloadWebpageTask().execute(stringUrl); } else { textView.setText("No network connection available."); } } // Uses AsyncTask to create a task away from the main UI thread. This task takes a // URL string and uses it to create an HttpUrlConnection. Once the connection // has been established, the AsyncTask downloads the contents of the webpage as // an InputStream. Finally, the InputStream is converted into a string, which is // displayed in the UI by the AsyncTask‘s onPostExecute method. private class DownloadWebpageTask extends AsyncTask<String, Void, String> { @Override protected String doInBackground(String... urls) { // params comes from the execute() call: params[0] is the url. try { return downloadUrl(urls[0]); } catch (IOException e) { return "Unable to retrieve web page. URL may be invalid."; } } // onPostExecute displays the results of the AsyncTask. @Override protected void onPostExecute(String result) { textView.setText(result); } } ...}
The sequence of events in this snippet is as follows:
- When users click the button that invokes
myClickHandler()
, the app passes the specified URL to theAsyncTask
subclassDownloadWebpageTask
. - The
AsyncTask
methoddoInBackground()
calls thedownloadUrl()
method. - The
downloadUrl()
method takes a URL string as a parameter and uses it to create aURL
object. - The
URL
object is used to establish anHttpURLConnection
. - Once the connection has been established, the
HttpURLConnection
object fetches the web page content as anInputStream
. - The
InputStream
is passed to thereadIt()
method, which converts the stream to a string. - Finally, the
AsyncTask
‘sonPostExecute()
method displays the string in the main activity‘s UI.
Connect and Download Data
In your thread that performs your network transactions, you can use
HttpURLConnection
to perform a GET
and download your data. After you call connect()
, you can get an InputStream
of the data by callinggetInputStream()
.
In the following snippet, the doInBackground()
method calls the method downloadUrl()
. The downloadUrl()
method takes the given URL and uses it to connect to the network via HttpURLConnection
. Once a connection has been established, the app uses the method getInputStream()
to retrieve the data as an InputStream
.
// Given a URL, establishes an HttpUrlConnection and retrieves// the web page content as a InputStream, which it returns as// a string.private String downloadUrl(String myurl) throws IOException { InputStream is = null; // Only display the first 500 characters of the retrieved // web page content. int len = 500; try { URL url = new URL(myurl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(10000 /* milliseconds */); conn.setConnectTimeout(15000 /* milliseconds */); conn.setRequestMethod("GET"); conn.setDoInput(true); // Starts the query conn.connect(); int response = conn.getResponseCode(); Log.d(DEBUG_TAG, "The response is: " + response); is = conn.getInputStream(); // Convert the InputStream into a string String contentAsString = readIt(is, len); return contentAsString; // Makes sure that the InputStream is closed after the app is // finished using it. } finally { if (is != null) { is.close(); } }}
Note that the method getResponseCode()
returns the connection‘s status code. This is a useful way of getting additional information about the connection. A status code of 200 indicates success.
Convert the InputStream to a String
An
InputStream
is a readable source of bytes. Once you get an InputStream
, it‘s common to decode or convert it into a target data type. For example, if you were downloading image data, you might decode and display it like this:
InputStream is = null;...Bitmap bitmap = BitmapFactory.decodeStream(is);ImageView imageView = (ImageView) findViewById(R.id.image_view);imageView.setImageBitmap(bitmap);
In the example shown above, the InputStream
represents the text of a web page. This is how the example converts the InputStream
to a string so that the activity can display it in the UI:
// Reads an InputStream and converts it to a String.public String readIt(InputStream stream, int len) throws IOException, UnsupportedEncodingException { Reader reader = null; reader = new InputStreamReader(stream, "UTF-8"); char[] buffer = new char[len]; reader.read(buffer); return new String(buffer);}
Managing Network Usage
THIS LESSON TEACHES YOU TO
- Check a Device‘s Network Connection
- Manage Network Usage
- Implement a Preferences Activity
- Respond to Preference Changes
- Detect Connection Changes
YOU SHOULD ALSO READ
TRY IT OUT
NetworkUsage.zip
This lesson describes how to write applications that have fine-grained control over their usage of network resources. If your application performs a lot of network operations, you should provide user settings that allow users to control your app’s data habits, such as how often your app syncs data, whether to perform uploads/downloads only when on Wi-Fi, whether to use data while roaming, and so on. With these controls available to them, users are much less likely to disable your app’s access to background data when they approach their limits, because they can instead precisely control how much data your app uses.
For general guidelines on how to write apps that minimize the battery life impact of downloads and network connections, seeOptimizing Battery Life and Transferring Data Without Draining the Battery.
Check a Device‘s Network Connection
A device can have various types of network connections. This lesson focuses on using either a Wi-Fi or a mobile network connection. For the full list of possible network types, see
ConnectivityManager
.
Wi-Fi is typically faster. Also, mobile data is often metered, which can get expensive. A common strategy for apps is to only fetch large data if a Wi-Fi network is available.
Before you perform network operations, it‘s good practice to check the state of network connectivity. Among other things, this could prevent your app from inadvertently using the wrong radio. If a network connection is unavailable, your application should respond gracefully. To check the network connection, you typically use the following classes:
ConnectivityManager
: Answers queries about the state of network connectivity. It also notifies applications when network connectivity changes.NetworkInfo
: Describes the status of a network interface of a given type (currently either Mobile or Wi-Fi).
This code snippet tests network connectivity for Wi-Fi and mobile. It determines whether these network interfaces are available (that is, whether network connectivity is possible) and/or connected (that is, whether network connectivity exists and if it is possible to establish sockets and pass data):
private static final String DEBUG_TAG = "NetworkStatusExample";... ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI); boolean isWifiConn = networkInfo.isConnected();networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);boolean isMobileConn = networkInfo.isConnected();Log.d(DEBUG_TAG, "Wifi connected: " + isWifiConn);Log.d(DEBUG_TAG, "Mobile connected: " + isMobileConn);
Note that you should not base decisions on whether a network is "available." You should always checkisConnected()
before performing network operations, since isConnected()
handles cases like flaky mobile networks, airplane mode, and restricted background data.
A more concise way of checking whether a network interface is available is as follows. The methodgetActiveNetworkInfo()
returns a NetworkInfo
instance representing the first connected network interface it can find, or null
if none of the interfaces is connected (meaning that an internet connection is not available):
public boolean isOnline() { ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return (networkInfo != null && networkInfo.isConnected());}
To query more fine-grained state you can use NetworkInfo.DetailedState
, but this should seldom be necessary.
Manage Network Usage
You can implement a preferences activity that gives users explicit control over your app‘s usage of network resources. For example:
- You might allow users to upload videos only when the device is connected to a Wi-Fi network.
- You might sync (or not) depending on specific criteria such as network availability, time interval, and so on.
To write an app that supports network access and managing network usage, your manifest must have the right permissions and intent filters.
- The manifest excerpted below includes the following permissions:
android.permission.INTERNET
—Allows applications to open network sockets.android.permission.ACCESS_NETWORK_STATE
—Allows applications to access information about networks.
- You can declare the intent filter for the
ACTION_MANAGE_NETWORK_USAGE
action (introduced in Android 4.0) to indicate that your application defines an activity that offers options to control data usage.ACTION_MANAGE_NETWORK_USAGE
shows settings for managing the network data usage of a specific application. When your app has a settings activity that allows users to control network usage, you should declare this intent filter for that activity. In the sample application, this action is handled by the classSettingsActivity
, which displays a preferences UI to let users decide when to download a feed.
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.networkusage" ...> <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="14" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <application ...> ... <activity android:label="SettingsActivity" android:name=".SettingsActivity"> <intent-filter> <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> </application></manifest>
Implement a Preferences Activity
As you can see in the manifest excerpt above, the sample app‘s activity
SettingsActivity
has an intent filter for the ACTION_MANAGE_NETWORK_USAGE
action. SettingsActivity
is a subclass of PreferenceActivity
. It displays a preferences screen (shown in figure 1) that lets users specify the following:
- Whether to display summaries for each XML feed entry, or just a link for each entry.
- Whether to download the XML feed if any network connection is available, or only if Wi-Fi is available.
Figure 1. Preferences activity.
Here is SettingsActivity
. Note that it implements OnSharedPreferenceChangeListener
. When a user changes a preference, it fires onSharedPreferenceChanged()
, which sets refreshDisplay
to true. This causes the display to refresh when the user returns to the main activity:
public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Loads the XML preferences file addPreferencesFromResource(R.xml.preferences); } @Override protected void onResume() { super.onResume(); // Registers a listener whenever a key changes getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); } @Override protected void onPause() { super.onPause(); // Unregisters the listener set in onResume(). // It‘s best practice to unregister listeners when your app isn‘t using them to cut down on // unnecessary system overhead. You do this in onPause(). getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); } // When the user changes the preferences selection, // onSharedPreferenceChanged() restarts the main activity as a new // task. Sets the refreshDisplay flag to "true" to indicate that // the main activity should update its display. // The main activity queries the PreferenceManager to get the latest settings. @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { // Sets refreshDisplay to true so that when the user returns to the main // activity, the display refreshes to reflect the new settings. NetworkActivity.refreshDisplay = true; }}
Respond to Preference Changes
When the user changes preferences in the settings screen, it typically has consequences for the app‘s behavior. In this snippet, the app checks the preferences settings in
onStart()
. if there is a match between the setting and the device‘s network connection (for example, if the setting is "Wi-Fi"
and the device has a Wi-Fi connection), the app downloads the feed and refreshes the display.
public class NetworkActivity extends Activity { public static final String WIFI = "Wi-Fi"; public static final String ANY = "Any"; private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest"; // Whether there is a Wi-Fi connection. private static boolean wifiConnected = false; // Whether there is a mobile connection. private static boolean mobileConnected = false; // Whether the display should be refreshed. public static boolean refreshDisplay = true; // The user‘s current network preference setting. public static String sPref = null; // The BroadcastReceiver that tracks network connectivity changes. private NetworkReceiver receiver = new NetworkReceiver(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Registers BroadcastReceiver to track network connection changes. IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); receiver = new NetworkReceiver(); this.registerReceiver(receiver, filter); } @Override public void onDestroy() { super.onDestroy(); // Unregisters BroadcastReceiver when app is destroyed. if (receiver != null) { this.unregisterReceiver(receiver); } } // Refreshes the display if the network connection and the // pref settings allow it. @Override public void onStart () { super.onStart(); // Gets the user‘s network preference settings SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); // Retrieves a string value for the preferences. The second parameter // is the default value to use if a preference value is not found. sPref = sharedPrefs.getString("listPref", "Wi-Fi"); updateConnectedFlags(); if(refreshDisplay){ loadPage(); } } // Checks the network connection and sets the wifiConnected and mobileConnected // variables accordingly. public void updateConnectedFlags() { ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeInfo = connMgr.getActiveNetworkInfo(); if (activeInfo != null && activeInfo.isConnected()) { wifiConnected = activeInfo.getType() == ConnectivityManager.TYPE_WIFI; mobileConnected = activeInfo.getType() == ConnectivityManager.TYPE_MOBILE; } else { wifiConnected = false; mobileConnected = false; } } // Uses AsyncTask subclass to download the XML feed from stackoverflow.com. public void loadPage() { if (((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) || ((sPref.equals(WIFI)) && (wifiConnected))) { // AsyncTask subclass new DownloadXmlTask().execute(URL); } else { showErrorPage(); } }... }
Detect Connection Changes
The final piece of the puzzle is the
BroadcastReceiver
subclass, NetworkReceiver
. When the device‘s network connection changes, NetworkReceiver
intercepts the action CONNECTIVITY_ACTION
, determines what the network connection status is, and sets the flags wifiConnected
and mobileConnected
to true/false accordingly. The upshot is that the next time the user returns to the app, the app will only download the latest feed and update the display if NetworkActivity.refreshDisplay
is set to true
.
Setting up a BroadcastReceiver that gets called unnecessarily can be a drain on system resources. The sample application registers the BroadcastReceiver
NetworkReceiver
in onCreate()
, and it unregisters it in onDestroy()
. This is more lightweight than declaring a <receiver>
in the manifest. When you declare a <receiver>
in the manifest, it can wake up your app at any time, even if you haven‘t run it for weeks. By registering and unregistering NetworkReceiver
within the main activity, you ensure that the app won‘t be woken up after the user leaves the app. If you do declare a <receiver>
in the manifest and you know exactly where you need it, you can use setComponentEnabledSetting()
to enable and disable it as appropriate.
Here is NetworkReceiver
:
public class NetworkReceiver extends BroadcastReceiver { @Overridepublic void onReceive(Context context, Intent intent) { ConnectivityManager conn = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = conn.getActiveNetworkInfo(); // Checks the user prefs and the network connection. Based on the result, decides whether // to refresh the display or keep the current display. // If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection. if (WIFI.equals(sPref) && networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { // If device has its Wi-Fi connection, sets refreshDisplay // to true. This causes the display to be refreshed when the user // returns to the app. refreshDisplay = true; Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show(); // If the setting is ANY network and there is a network connection // (which by process of elimination would be mobile), sets refreshDisplay to true. } else if (ANY.equals(sPref) && networkInfo != null) { refreshDisplay = true; // Otherwise, the app can‘t download content--either because there is no network // connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there // is no Wi-Fi connection. // Sets refreshDisplay to false. } else { refreshDisplay = false; Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show(); }}
Parsing XML Data
THIS LESSON TEACHES YOU TO
- Choose a Parser
- Analyze the Feed
- Instantiate the Parser
- Read the Feed
- Parse XML
- Skip Tags You Don‘t Care About
- Consume XML Data
YOU SHOULD ALSO READ
TRY IT OUT
NetworkUsage.zip
Extensible Markup Language (XML) is a set of rules for encoding documents in machine-readable form. XML is a popular format for sharing data on the internet. Websites that frequently update their content, such as news sites or blogs, often provide an XML feed so that external programs can keep abreast of content changes. Uploading and parsing XML data is a common task for network-connected apps. This lesson explains how to parse XML documents and use their data.
Choose a Parser
We recommend
XmlPullParser
, which is an efficient and maintainable way to parse XML on Android. Historically Android has had two implementations of this interface:
KXmlParser
viaXmlPullParserFactory.newPullParser()
.ExpatPullParser
, viaXml.newPullParser()
.
Either choice is fine. The example in this section usesExpatPullParser
, via Xml.newPullParser()
.
Analyze the Feed
The first step in parsing a feed is to decide which fields you‘re interested in. The parser extracts data for those fields and ignores the rest.
Here is an excerpt from the feed that‘s being parsed in the sample app. Each post to StackOverflow.com appears in the feed as an entry
tag that contains several nested tags:
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" ..."> <title type="text">newest questions tagged android - Stack Overflow</title>... <entry> ... </entry> <entry> <id>http://stackoverflow.com/q/9439999</id> <re:rank scheme="http://stackoverflow.com">0</re:rank> <title type="text">Where is my data file?</title> <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="android"/> <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="file"/> <author> <name>cliff2310</name> <uri>http://stackoverflow.com/users/1128925</uri> </author> <link rel="alternate" href="http://stackoverflow.com/questions/9439999/where-is-my-data-file" /> <published>2012-02-25T00:30:54Z</published> <updated>2012-02-25T00:30:54Z</updated> <summary type="html"> <p>I have an Application that requires a data file...</p> </summary> </entry> <entry> ... </entry>...</feed>
The sample app extracts data for the entry
tag and its nested tags title
, link
, and summary
.
Instantiate the Parser
The next step is to instantiate a parser and kick off the parsing process. In this snippet, a parser is initialized to not process namespaces, and to use the provided
InputStream
as its input. It starts the parsing process with a call to nextTag()
and invokes the readFeed()
method, which extracts and processes the data the app is interested in:
public class StackOverflowXmlParser { // We don‘t use namespaces private static final String ns = null; public List parse(InputStream in) throws XmlPullParserException, IOException { try { XmlPullParser parser = Xml.newPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(in, null); parser.nextTag(); return readFeed(parser); } finally { in.close(); } } ... }
Read the Feed
The
readFeed()
method does the actual work of processing the feed. It looks for elements tagged "entry" as a starting point for recursively processing the feed. If a tag isn‘t an entry
tag, it skips it. Once the whole feed has been recursively processed, readFeed()
returns a List
containing the entries (including nested data members) it extracted from the feed. This List
is then returned by the parser.
private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException { List entries = new ArrayList(); parser.require(XmlPullParser.START_TAG, ns, "feed"); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); // Starts by looking for the entry tag if (name.equals("entry")) { entries.add(readEntry(parser)); } else { skip(parser); } } return entries;}
Parse XML
The steps for parsing an XML feed are as follows:
- As described in Analyze the Feed, identify the tags you want to include in your app. This example extracts data for the
entry
tag and its nested tagstitle
,link
, andsummary
. - Create the following methods:
- A "read" method for each tag you‘re interested in. For example,
readEntry()
,readTitle()
, and so on. The parser reads tags from the input stream. When it encounters a tag namedentry
,title
,link
orsummary
, it calls the appropriate method for that tag. Otherwise, it skips the tag. - Methods to extract data for each different type of tag and to advance the parser to the next tag. For example:
- For the
title
andsummary
tags, the parser callsreadText()
. This method extracts data for these tags by callingparser.getText()
. - For the
link
tag, the parser extracts data for links by first determining if the link is the kind it‘s interested in. Then it usesparser.getAttributeValue()
to extract the link‘s value. - For the
entry
tag, the parser callsreadEntry()
. This method parses the entry‘s nested tags and returns anEntry
object with the data memberstitle
,link
, andsummary
.
- For the
- A helper
skip()
method that‘s recursive. For more discussion of this topic, see Skip Tags You Don‘t Care About.
- A "read" method for each tag you‘re interested in. For example,
This snippet shows how the parser parses entries, titles, links, and summaries.
public static class Entry { public final String title; public final String link; public final String summary; private Entry(String title, String summary, String link) { this.title = title; this.summary = summary; this.link = link; }} // Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off// to their respective "read" methods for processing. Otherwise, skips the tag.private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException { parser.require(XmlPullParser.START_TAG, ns, "entry"); String title = null; String summary = null; String link = null; while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if (name.equals("title")) { title = readTitle(parser); } else if (name.equals("summary")) { summary = readSummary(parser); } else if (name.equals("link")) { link = readLink(parser); } else { skip(parser); } } return new Entry(title, summary, link);} // Processes title tags in the feed.private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_TAG, ns, "title"); String title = readText(parser); parser.require(XmlPullParser.END_TAG, ns, "title"); return title;} // Processes link tags in the feed.private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException { String link = ""; parser.require(XmlPullParser.START_TAG, ns, "link"); String tag = parser.getName(); String relType = parser.getAttributeValue(null, "rel"); if (tag.equals("link")) { if (relType.equals("alternate")){ link = parser.getAttributeValue(null, "href"); parser.nextTag(); } } parser.require(XmlPullParser.END_TAG, ns, "link"); return link;} // Processes summary tags in the feed.private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_TAG, ns, "summary"); String summary = readText(parser); parser.require(XmlPullParser.END_TAG, ns,"summary"); return summary;} // For the tags title and summary, extracts their text values.privateString readText(XmlPullParser parser)throwsIOException,XmlPullParserException{ String result =""; if(parser.next()==XmlPullParser.TEXT){ result = parser.getText(); parser.nextTag(); } return result;} ...}
Skip Tags You Don‘t Care About
One of the steps in the XML parsing described above is for the parser to skip tags it‘s not interested in. Here is the parser‘s
skip()
method:
private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { if (parser.getEventType() != XmlPullParser.START_TAG) { throw new IllegalStateException(); } int depth = 1; while (depth != 0) { switch (parser.next()) { case XmlPullParser.END_TAG: depth--; break; case XmlPullParser.START_TAG: depth++; break; } } }
This is how it works:
- It throws an exception if the current event isn‘t a
START_TAG
. - It consumes the
START_TAG
, and all events up to and including the matchingEND_TAG
. - To make sure that it stops at the correct
END_TAG
and not at the first tag it encounters after the originalSTART_TAG
, it keeps track of the nesting depth.
Thus if the current element has nested elements, the value of depth
won‘t be 0 until the parser has consumed all events between the original START_TAG
and its matching END_TAG
. For example, consider how the parser skips the<author>
element, which has 2 nested elements, <name>
and <uri>
:
- The first time through the
while
loop, the next tag the parser encounters after<author>
is theSTART_TAG
for<name>
. The value fordepth
is incremented to 2. - The second time through the
while
loop, the next tag the parser encounters is theEND_TAG
</name>
. The value fordepth
is decremented to 1. - The third time through the
while
loop, the next tag the parser encounters is theSTART_TAG
<uri>
. The value fordepth
is incremented to 2. - The fourth time through the
while
loop, the next tag the parser encounters is theEND_TAG
</uri>
. The value fordepth
is decremented to 1. - The fifth time and final time through the
while
loop, the next tag the parser encounters is theEND_TAG
</author>
. The value fordepth
is decremented to 0, indicating that the<author>
element has been successfully skipped.
Consume XML Data
The example application fetches and parses the XML feed within an
AsyncTask
. This takes the processing off the main UI thread. When processing is complete, the app updates the UI in the main activity (NetworkActivity
).
In the excerpt shown below, the loadPage()
method does the following:
- Initializes a string variable with the URL for the XML feed.
- If the user‘s settings and the network connection allow it, invokes
new DownloadXmlTask().execute(url)
. This instantiates a newDownloadXmlTask
object (AsyncTask
subclass) and runs itsexecute()
method, which downloads and parses the feed and returns a string result to be displayed in the UI.
public class NetworkActivity extends Activity { public static final String WIFI = "Wi-Fi"; public static final String ANY = "Any"; private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest"; // Whether there is a Wi-Fi connection. private static boolean wifiConnected = false; // Whether there is a mobile connection. private static boolean mobileConnected = false; // Whether the display should be refreshed. public static boolean refreshDisplay = true; public static String sPref = null; ... // Uses AsyncTask to download the XML feed from stackoverflow.com. public void loadPage() { if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) { new DownloadXmlTask().execute(URL); } else if ((sPref.equals(WIFI)) && (wifiConnected)) { new DownloadXmlTask().execute(URL); } else { // show error } }
The AsyncTask
subclass shown below, DownloadXmlTask
, implements the following AsyncTask
methods:
doInBackground()
executes the methodloadXmlFromNetwork()
. It passes the feed URL as a parameter. The methodloadXmlFromNetwork()
fetches and processes the feed. When it finishes, it passes back a result string.onPostExecute()
takes the returned string and displays it in the UI.
// Implementation of AsyncTask used to download XML feed from stackoverflow.com.private class DownloadXmlTask extends AsyncTask<String, Void, String> { @Override protected String doInBackground(String... urls) { try { return loadXmlFromNetwork(urls[0]); } catch (IOException e) { return getResources().getString(R.string.connection_error); } catch (XmlPullParserException e) { return getResources().getString(R.string.xml_error); } } @Override protected void onPostExecute(String result) { setContentView(R.layout.main); // Displays the HTML string in the UI via a WebView WebView myWebView = (WebView) findViewById(R.id.webview); myWebView.loadData(result, "text/html", null); }}
Below is the method loadXmlFromNetwork()
that is invoked from DownloadXmlTask
. It does the following:
- Instantiates a
StackOverflowXmlParser
. It also creates variables for aList
ofEntry
objects (entries
), andtitle
,url
, andsummary
, to hold the values extracted from the XML feed for those fields. - Calls
downloadUrl()
, which fetches the feed and returns it as anInputStream
. - Uses
StackOverflowXmlParser
to parse theInputStream
.StackOverflowXmlParser
populates aList
ofentries
with data from the feed. - Processes the
entries
List
, and combines the feed data with HTML markup. - Returns an HTML string that is displayed in the main activity UI by the
AsyncTask
methodonPostExecute()
.
// Uploads XML from stackoverflow.com, parses it, and combines it with// HTML markup. Returns HTML string.private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException { InputStream stream = null; // Instantiate the parser StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser(); List<Entry> entries = null; String title = null; String url = null; String summary = null; Calendar rightNow = Calendar.getInstance(); DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa"); // Checks whether the user set the preference to include summary text SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); boolean pref = sharedPrefs.getBoolean("summaryPref", false); StringBuilder htmlString = new StringBuilder(); htmlString.append("<h3>" + getResources().getString(R.string.page_title) + "</h3>"); htmlString.append("<em>" + getResources().getString(R.string.updated) + " " + formatter.format(rightNow.getTime()) + "</em>"); try { stream = downloadUrl(urlString); entries = stackOverflowXmlParser.parse(stream); // Makes sure that the InputStream is closed after the app is // finished using it. } finally { if (stream != null) { stream.close(); } } // StackOverflowXmlParser returns a List (called "entries") of Entry objects. // Each Entry object represents a single post in the XML feed. // This section processes the entries list to combine each entry with HTML markup. // Each entry is displayed in the UI as a link that optionally includes // a text summary. for (Entry entry : entries) { htmlString.append("<p><a href=‘"); htmlString.append(entry.link); htmlString.append("‘>" + entry.title + "</a></p>"); // If the user set the preference to include summary text, // adds it to the display. if (pref) { htmlString.append(entry.summary); } } return htmlString.toString();} // Given a string representation of a URL, sets up a connection and gets// an input stream.private InputStream downloadUrl(String urlString) throws IOException { URL url = new URL(urlString); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(10000 /* milliseconds */); conn.setConnectTimeout(15000 /* milliseconds */); conn.setRequestMethod("GET"); conn.setDoInput(true); // Starts the query conn.connect(); return conn.getInputStream();}