Android开发训练之第五章——Building Apps with Connectivity & the Cloud

时间:2023-03-08 17:46:07

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.

  1. 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.

    1. Using Network Service Discovery
    2. Creating P2P Connections with Wi-Fi
    3. Using Wi-Fi P2P for Service Discovery
  2. Performing Network Operations

    How to create a network connection, monitor the connection for changes in connectivity, and perform transactions with XML data.

    1. Connecting to the Network
    2. Managing Network Usage
    3. Parsing XML Data
  3. Transferring Data Without Draining the Battery

    How to minimize your app's impact on the battery when performing downloads and other network transactions.

    1. Optimizing Downloads for Efficient Network Access
    2. Minimizing the Effect of Regular Updates
    3. Redundant Downloads are Redundant
    4. Modifying Patterns Based on the Connectivity Type
  4. 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.

    1. Using the Backup API
    2. Making the Most of Google Cloud Messaging
  5. Resolving Cloud Save Conflicts

    How to design a robust conflict resolution strategy for apps that save data to the cloud.

  6. Transferring Data Using Sync Adapters

    How to transfer data between the cloud and the device using the Android sync adapter framework

    1. Creating a Stub Authenticator
    2. Creating a Stub Content Provider
    3. Creating a Sync Adapter
    4. Running a Sync Adapter
  7. Transmitting Network Data Using Volley

    How to perform fast, scalable UI operations over the network using Volley

    1. Sending a Simple Request
    2. Setting Up a RequestQueue
    3. Making a Standard Request
    4. Implementing a Custom Request

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

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.

  1. 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).
  2. The service type is checked, to verify it's a type of service your application can connect to.
  3. 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

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();
...
@Override
public 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:

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 theWifiP2pManager.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 WifiP2pDevicerepresenting 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

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_STATEACCESS_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:

  1. Create a WifiP2pServiceInfo object.
  2. Populate it with information about your service.
  3. 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

TRY IT OUT

Download the sample

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 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 method downloadUrl(). It passes the web page URL as a parameter. The method downloadUrl() 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:

  1. When users click the button that invokes myClickHandler(), the app passes the specified URL to the AsyncTasksubclass DownloadWebpageTask.
  2. The AsyncTask method doInBackground() calls the downloadUrl() method.
  3. The downloadUrl() method takes a URL string as a parameter and uses it to create a URL object.
  4. The URL object is used to establish an HttpURLConnection.
  5. Once the connection has been established, the HttpURLConnection object fetches the web page content as anInputStream.
  6. The InputStream is passed to the readIt() method, which converts the stream to a string.
  7. Finally, the AsyncTask's onPostExecute() 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 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, seeConnectivityManager.

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:
  • 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 class SettingsActivity, 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.

Android开发训练之第五章——Building Apps with Connectivity & the Cloud Android开发训练之第五章——Building Apps with Connectivity & the Cloud

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://*.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 *.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 {   
     
@Override
public 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

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:

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 *.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://*.com/q/9439999</id>
        <re:rank scheme="http://*.com">0</re:rank>
        <title type="text">Where is my data file?</title>
        <category scheme="http://*.com/feeds/tag?tagnames=android&sort=newest/tags" term="android"/>
        <category scheme="http://*.com/feeds/tag?tagnames=android&sort=newest/tags" term="file"/>
        <author>
            <name>cliff2310</name>
            <uri>http://*.com/users/1128925</uri>
        </author>
        <link rel="alternate" href="http://*.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 titlelink, 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 *XmlParser {
    // 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:

  1. 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 tags titlelink, and summary.
  2. 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 named entrytitlelink or summary, 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 and summary tags, the parser calls readText(). This method extracts data for these tags by calling parser.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 uses parser.getAttributeValue() to extract the link's value.
      • For the entry tag, the parser calls readEntry(). This method parses the entry's nested tags and returns an Entry object with the data members titlelink, and summary.
    • A helper skip() method that's recursive. For more discussion of this topic, see Skip Tags You Don't Care About.

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 matching END_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 the START_TAG for<name>. The value for depth is incremented to 2.
  • The second time through the while loop, the next tag the parser encounters is the END_TAG </name>. The value for depth is decremented to 1.
  • The third time through the while loop, the next tag the parser encounters is the START_TAG <uri>. The value fordepth is incremented to 2.
  • The fourth time through the while loop, the next tag the parser encounters is the END_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 the END_TAG</author>. The value for depth 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 new DownloadXmlTask object (AsyncTask subclass) and runs its execute() 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://*.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 *.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 method loadXmlFromNetwork(). It passes the feed URL as a parameter. The method loadXmlFromNetwork() 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 *.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:

  1. Instantiates a *XmlParser. It also creates variables for a List of Entry objects (entries), andtitleurl, and summary, to hold the values extracted from the XML feed for those fields.
  2. Calls downloadUrl(), which fetches the feed and returns it as an InputStream.
  3. Uses *XmlParser to parse the InputStream*XmlParser populates a List of entrieswith data from the feed.
  4. Processes the entries List, and combines the feed data with HTML markup.
  5. Returns an HTML string that is displayed in the main activity UI by the AsyncTask method onPostExecute().
// Uploads XML from *.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
    *XmlParser *XmlParser = new *XmlParser();
    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 = *XmlParser.parse(stream);
    // Makes sure that the InputStream is closed after the app is
    // finished using it.
    } finally {
        if (stream != null) {
            stream.close();
        }
     }
   
    // *XmlParser 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();
}