Bluetooth, NFC, Networks, and Wi-Fi - Professional Android 4 Application Development (2012)

Professional Android 4 Application Development (2012)

Chapter 16. Bluetooth, NFC, Networks, and Wi-Fi

What's in this Chapter?

Managing Bluetooth devices and discoverability mode

Discovering remote Bluetooth devices

Communicating over Bluetooth

Monitoring Internet connectivity

Monitoring Wi-Fi and network details

Configuring Wi-Fi and scanning for access points

Transferring data using Wi-Fi Direct

Scanning NFC tags

Transferring data using Android Beam

This chapter begins to explore Android's hardware communications APIs by examining the Bluetooth, network, Wi-Fi, and Near Field Communication (NFC) packages.

Android offers APIs to manage and monitor your Bluetooth settings: to control discoverability, to discover nearby Bluetooth devices, and to use Bluetooth as a proximity-based, peer-to-peer transport layer for your applications.

A full network and Wi-Fi package is also available. Using these APIs, you can scan for hotspots, create and modify Wi-Fi configuration settings, monitor your Internet connectivity, and control and monitor Internet settings and preferences. The introduction of Wi-Fi Direct offers a peer-to-peer solution for communicating between devices using Wi-Fi.

Android 2.3 (API level 9) introduced support for NFC, including the support for reading smart tags, and Android 4.0 (API level 14) added the ability to communicate with other NFC-enabled Android devices using Android Beam.

Using Bluetooth

Bluetooth is a communications protocol designed for short-range, low-bandwidth peer-to-peer communications.

Using the Bluetooth APIs, you can search for, and connect to, other Bluetooth devices within range. By initiating a communications link using Bluetooth Sockets, you can then transmit and receive streams of data between devices from within your applications.

2.1

At the time of writing, only encrypted communication is supported between devices, meaning that you can form connections only between devices that have been paired.

Managing the Local Bluetooth Device Adapter

The local Bluetooth device is controlled via the BluetoothAdapter class, which represents the host Android device on which your application is running.

To access the default Bluetooth Adapter, call getDefaultAdapter, as shown in Listing 16.1. Some Android devices feature multiple Bluetooth adapters, though it is currently only possible to access the default device.

2.11

Listing 16.1: Accessing the default Bluetooth Adapter

BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();

code snippet PA4AD_Ch16_Bluetooth/src/BluetoothActivity.java

To read any of the local Bluetooth Adapter properties, initiate discovery, or find bonded devices, you need to include the BLUETOOTH permission in your application manifest. To modify any of the local device properties, the BLUETOOTH_ADMIN permission is also required:

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

The Bluetooth Adapter offers methods for reading and setting properties of the local Bluetooth hardware.

2.1

The Bluetooth Adapter properties can be read and changed only if the Bluetooth Adapter is currently turned on—that is, if its device state is enabled. If the device is off, these methods will return null.

Use the isEnabled method to confirm the device is enabled, after which you can access the Bluetooth Adapter's friendly name (an arbitrary string that users can set to identify a particular device) and hardware address, using the getName and getAddress methods, respectively:

if (bluetooth.isEnabled()) {
  String address = bluetooth.getAddress();
  String name = bluetooth.getName();
}

If you have the BLUETOOTH_ADMIN permission, you can change the friendly name of the Bluetooth Adapter using the setName method:

bluetooth.setName("Blackfang");

To find a more detailed description of the current Bluetooth Adapter state, use the getState method, which will return one of the following BluetoothAdapter constants:

· STATE_TURNING_ON

· STATE_ON

· STATE_TURNING_OFF

· STATE_OFF

To conserve battery life and optimize security, most users will keep Bluetooth disabled until they plan to use it.

To enable the Bluetooth Adapter, you can start a system Preference Activity using the BluetoothAdapter.ACTION_REQUEST_ENABLE static constant as a startActivityForResult action string:

startActivityForResult(
  new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), 0);

Figure 16.1 shows the resulting Preference Activity.

Figure 16.1

16.1

It prompts the user to turn on Bluetooth and asks for confirmation. If the user agrees, the sub-Activity will close and return to the calling Activity when the Bluetooth Adapter has turned on (or has encountered an error). If the user selects no, the sub-Activity will close and return immediately. Use the result code parameter returned in the onActivityResult handler to determine the success of this operation, as shown in Listing 16.2.

2.11

Listing 16.2: Enabling Bluetooth

private static final int ENABLE_BLUETOOTH = 1;
 
private void initBluetooth() {
  if (!bluetooth.isEnabled()) { 
    // Bluetooth isn't enabled, prompt the user to turn it on.
    Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(intent, ENABLE_BLUETOOTH);
  } else {
    // Bluetooth is enabled, initialize the UI.
    initBluetoothUI();
  }
}
 
protected void onActivityResult(int requestCode,
                                int resultCode, Intent data) {
  if (requestCode == ENABLE_BLUETOOTH)
    if (resultCode == RESULT_OK) {
      // Bluetooth has been enabled, initialize the UI.
      initBluetoothUI();
    }
}

code snippet PA4AD_Ch16_Bluetooth/src/BluetoothActivity.java

Enabling and disabling the Bluetooth Adapter are somewhat time-consuming, asynchronous operations. Rather than polling the Bluetooth Adapter, your application should register a Broadcast Receiver that listens for ACTION_STATE_CHANGED. The Broadcast Intent will include two extras,EXTRA_STATE and EXTRA_PREVIOUS_STATE, which indicate the current and previous Bluetooth Adapter states, respectively:

BroadcastReceiver bluetoothState = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    String prevStateExtra = BluetoothAdapter.EXTRA_PREVIOUS_STATE;
    String stateExtra = BluetoothAdapter.EXTRA_STATE;
    int state = intent.getIntExtra(stateExtra, –1);
    int previousState = intent.getIntExtra(prevStateExtra, –1);
 
    String tt = "";
    switch (state) {
      case (BluetoothAdapter.STATE_TURNING_ON) :
        tt = "Bluetooth turning on"; break;
      case (BluetoothAdapter.STATE_ON) :
        tt = "Bluetooth on"; break;
      case (BluetoothAdapter.STATE_TURNING_OFF) :
        tt = "Bluetooth turning off"; break;
      case (BluetoothAdapter.STATE_OFF) :
        tt = "Bluetooth off"; break;
      default: break;
    }
    Log.d(TAG, tt);
  }
};
 
String actionStateChanged = BluetoothAdapter.ACTION_STATE_CHANGED;
registerReceiver(bluetoothState,
                 new IntentFilter(actionStateChanged));

2.1

You can also turn the Bluetooth Adapter on and off directly, using the enable and disable methods, respectively, if you include the BLUETOOTH_ADMIN permission in your manifest.

This should be done only when absolutely necessary, and the user should always be notified if you are manually changing the Bluetooth Adapter status on the user's behalf. In most cases you should use the Intent mechanism described earlier.

Being Discoverable and Remote Device Discovery

The process of two devices finding each other to connect is called discovery. Before you can establish a Bluetooth Socket for communications, the local Bluetooth Adapter must bond with the remote device. Before two devices can bond and connect, they first need to discover each other.

2.1

Although the Bluetooth protocol supports ad-hoc connections for data transfer, this mechanism is not currently available in Android. Android Bluetooth communication is currently supported only between bonded devices.

Managing Device Discoverability

For an Android device to find your local Bluetooth Adapter during a discovery scan, you need to ensure that it's discoverable. The Bluetooth Adapter's discoverability is indicated by its scan mode, found using the getScanMode method on the BluetoothAdapter object.

It will return one of the following BluetoothAdapter constants:

· SCAN_MODE_CONNECTABLE_DISCOVERABLE—Inquiry scan and page scan are both enabled, meaning that the device is discoverable from any Bluetooth device performing a discovery scan.

· SCAN_MODE_CONNECTABLE—Page scan is enabled but inquiry scan is not. This means that devices that have previously connected and bonded to the local device can find it during discovery, but new devices can't.

· SCAN_MODE_NONE—Discoverability is turned off. No remote devices can find the local Bluetooth Adapter during discovery.

For privacy reasons, Android devices will default to having discoverability disabled. To turn on discovery, you need to obtain explicit permission from the user; you do this by starting a new Activity using the ACTION_REQUEST_DISCOVERABLE action, as shown in Listing 16.3.

2.11

Listing 16.3: Enabling discoverability

startActivityForResult(
  new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE),
             DISCOVERY_REQUEST);

code snippet PA4AD_Ch16_Bluetooth/src/BluetoothActivity.java

By default, discoverability will be enabled for 2 minutes. You can modify this setting by adding an EXTRA_DISCOVERABLE_DURATION extra to the launch Intent, specifying the number of seconds you want discoverability to last.

When the Intent is broadcast, the user will be prompted by the dialog, as shown in Figure 16.2, to turn on discoverability for the specified duration.

Figure 16.2

16.2

To learn if the user has allowed or rejected your discovery request, override the onActivityResult handler, as shown in Listing 16.4. The returned resultCode parameter indicates the duration of discoverability, or a negative number if the user has rejected your request.

Listing 16.4: Monitoring discoverability request approval

@Override
protected void onActivityResult(int requestCode,
                                int resultCode, Intent data) {
  if (requestCode == DISCOVERY_REQUEST) {
    if (resultCode == RESULT_CANCELED) {
      Log.d(TAG, "Discovery canceled by user");
    }
  }
}

code snippet PA4AD_Ch16_Bluetooth/src/BluetoothActivity.java

Alternatively, you can monitor changes in discoverability by receiving the ACTION_SCAN_MODE_CHANGED broadcast action. The Broadcast Intent includes the current and previous scan modes as extras:

registerReceiver(new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    String prevScanMode = BluetoothAdapter.EXTRA_PREVIOUS_SCAN_MODE;
    String scanMode = BluetoothAdapter.EXTRA_SCAN_MODE;
 
    int currentScanMode = intent.getIntExtra(scanMode, –1);
    int prevMode = intent.getIntExtra(prevScanMode, –1);
 
    Log.d(TAG, "Scan Mode: " + currentScanMode  + 
             ". Previous: " + prevMode);
  }
},
new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));

Discovering Remote Devices

In this section you'll learn how to initiate discovery from your local Bluetooth Adapter to find discoverable devices nearby.

2.1

The discovery process can take some time to complete (up to 12 seconds). During this time, performance of your Bluetooth Adapter communications will be seriously degraded. Use the techniques in this section to check and monitor the discovery status of the Bluetooth Adapter, and avoid doing high-bandwidth operations (including connecting to a new remote Bluetooth Device) while discovery is in progress.

You can check if the local Bluetooth Adapter is already performing a discovery scan by using the isDiscovering method.

To initiate the discovery process, call startDiscovery on the Bluetooth Adapter:

if (bluetooth.isEnabled())
  bluetooth.startDiscovery();

To cancel a discovery in progress, call cancelDiscovery.

The discovery process is asynchronous. Android uses broadcast Intents to notify you of the start and end of discovery as well as remote devices discovered during the scan.

You can monitor changes in the discovery process by creating Broadcast Receivers to listen for the ACTION_DISCOVERY_STARTED and ACTION_DISCOVERY_FINISHED Broadcast Intents:

BroadcastReceiver discoveryMonitor = new BroadcastReceiver() {
 
  String dStarted = BluetoothAdapter.ACTION_DISCOVERY_STARTED;
  String dFinished = BluetoothAdapter.ACTION_DISCOVERY_FINISHED;
 
  @Override
  public void onReceive(Context context, Intent intent) {
    if (dStarted.equals(intent.getAction())) {
      // Discovery has started.
      Log.d(TAG, "Discovery Started...");
    }
    else if (dFinished.equals(intent.getAction())) {
      // Discovery has completed.
      Log.d(TAG, "Discovery Complete.");
    }
  }
};
 
registerReceiver(discoveryMonitor,
                 new IntentFilter(dStarted));
registerReceiver(discoveryMonitor,
                 new IntentFilter(dFinished));

Discovered Bluetooth Devices are returned via Broadcast Intents by means of the ACTION_FOUND broadcast action.

As shown in Listing 16.5, each Broadcast Intent includes the name of the remote device in an extra indexed as BluetoothDevice.EXTRA_NAME, and an immutable representation of the remote Bluetooth device as a BluetoothDevice parcelable object stored under the BluetoothDevice.EXTRA_DEVICEextra.

2.11

Listing 16.5: Discovering remote Bluetooth Devices

private ArrayList<BluetoothDevice> deviceList = 
  new ArrayList<BluetoothDevice>();
 
private void startDiscovery() {
  registerReceiver(discoveryResult,
                   new IntentFilter(BluetoothDevice.ACTION_FOUND));
 
  if (bluetooth.isEnabled() && !bluetooth.isDiscovering())
    deviceList.clear();
    bluetooth.startDiscovery();
}
 
BroadcastReceiver discoveryResult = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    String remoteDeviceName = 
      intent.getStringExtra(BluetoothDevice.EXTRA_NAME);
 
    BluetoothDevice remoteDevice =  
      intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
 
    deviceList.add(remoteDevice);
 
    Log.d(TAG, "Discovered " + remoteDeviceName);
  }
};

code snippet PA4AD_Ch16_Bluetooth/src/BluetoothActivity.java

Each BluetoothDevice object returned through the discovery broadcasts represents a remote Bluetooth Device discovered.

In the following sections you will use Bluetooth Device objects to create a connection, bond, and ultimately transfer data between the local Bluetooth Adapter and remote Bluetooth Devices.

Bluetooth Communications

The Android Bluetooth communications APIs are wrappers around RFCOMM, the Bluetooth radio frequency communications protocol. RFCOMM supports RS232 serial communication over the Logical Link Control and Adaptation Protocol (L2CAP) layer.

In practice, this alphabet soup provides a mechanism for opening communication sockets between two paired Bluetooth devices.

2.1

Before your application can communicate between devices, the devices must be paired (bonded). If users attempt to connect two unpaired devices, they will be prompted to pair them before the connection is established.

You can establish an RFCOMM communication channel for bidirectional communications using the following classes.

· BluetoothServerSocket—Used to establish a listening socket for initiating a link between devices. To establish a handshake, one device acts as a server to listen for, and accept, incoming connection requests.

· BluetoothSocket—Used to create a new client to connect to a listening Bluetooth Server Socket. Also returned by the Bluetooth Server Socket after a connection is established. Once a connection is established, Bluetooth Sockets are used by both the server and client to transfer data streams.

When creating an application that uses Bluetooth as a peer-to-peer transport layer, you'll need to implement both a Bluetooth Server Socket to listen for connections and a Bluetooth Socket to initiate a new channel and handle communications.

When connected, the Bluetooth Server Socket returns a Bluetooth Socket that's then used by the server device to send and receive data. This server-side Bluetooth Socket is used in exactly the same way as the client socket. The designations of server and client are relevant only to how the connection is established; they don't affect how data flows after that connection is made.

Opening a Bluetooth Server Socket Listener

A Bluetooth Server Socket is used to listen for incoming Bluetooth Socket connection requests from remote Bluetooth Devices. In order for two Bluetooth devices to be connected, one must act as a server (listening for and accepting incoming requests) and the other as a client (initiating the request to connect to the server). After the two are connected, the communications between the server and host device are handled through a Bluetooth Socket at both ends.

To have your Bluetooth Adapter act as a server, call its listenUsingRfcommWithServiceRecord method to listen for incoming connection requests. Pass in a name to identify your server and a universally unique identifier (UUID). The method will return a BluetoothServerSocket object—note that the client Bluetooth Socket that connects to this listener will need to know its UUID in order to connect.

Call accept on the Server Socket, optionally passing in a timeout duration, to have it start listening for connections. The Server Socket will now block until a remote Bluetooth Socket client with a matching UUID attempts to connect.

If a connection request is made from a remote device that is not yet paired with the local Bluetooth Adapter, the user will be prompted to accept a pairing request before the accept call returns. This prompt is made via a Notification, or a Dialog, as shown in Figure 16.3.

Figure 16.3

16.3

If an incoming connection request is successful, accept will return a Bluetooth Socket connected to the client device. You can use this socket to transfer data, as shown later in this section.

2.1

Note that accept is a blocking operation, so it's best practice to listen for incoming connection requests on a background thread rather than block the UI thread until a connection has been made.

It's also important to note that your Bluetooth Adapter must be discoverable for remote Bluetooth Devices to connect to it. Listing 16.6 shows some typical skeleton code that uses the ACTION_REQUEST_DISCOVERABLE broadcast to request that the device be made discoverable, before listening for incoming connection requests for the returned discoverability duration.

2.11

Listing 16.6: Listening for Bluetooth Socket connection requests

private BluetoothSocket transferSocket;
 
private UUID startServerSocket(BluetoothAdapter bluetooth) {
  UUID uuid = UUID.fromString("a60f35f0-b93a-11de-8a39-08002009c666");
  String name = "bluetoothserver";
 
  try {
    final BluetoothServerSocket btserver = 
      bluetooth.listenUsingRfcommWithServiceRecord(name, uuid);
 
    Thread acceptThread = new Thread(new Runnable() {
      public void run() {
        try {
          // Block until client connection established.
          BluetoothSocket serverSocket = btserver.accept();
          // Start listening for messages.
          listenForMessages(serverSocket);
          // Add a reference to the socket used to send messages.
          transferSocket = serverSocket;
        } catch (IOException e) {
          Log.e("BLUETOOTH", "Server connection IO Exception", e);
        }
      }
    });
    acceptThread.start();
  } catch (IOException e) {
    Log.e("BLUETOOTH", "Socket listener IO Exception", e);
  }
  return uuid;
}

code snippet PA4AD_Ch16_Bluetooth/src/BluetoothActivity.java

Selecting Remote Bluetooth Devices for Communications

The BluetoothSocket class can be used on the client device to initiate a communications channel from within your application to a listening Bluetooth Server Socket. It is also returned by the Bluetooth Server Socket Listener after a connection to a client device has been established.

Create a client-side Bluetooth Socket by calling createRfcommSocketToServiceRecord on a BluetoothDevice object that represents the target remote server device. The target device should have a Bluetooth Server Socket listening for connection requests (as described in the previous section).

There are a number of ways to obtain a reference to a remote Bluetooth Device, and some important caveats regarding the devices with which you can create a communications link.

Bluetooth Device Connection Requirements

In order for a Bluetooth Socket to establish a connection to a remote Bluetooth Device, the following conditions must be true:

· The remote device must be discoverable.

· The remote device must be accepting connections through a Bluetooth Server Socket.

· The local and remote devices must be paired (bonded). If the devices are not paired, the users of each device will be prompted to pair them when the connection request is initiated.

Finding a Bluetooth Device to Connect To

Bluetooth Device objects are used to represents remote devices. You can query them for the properties of each remote device, and to initiate Bluetooth Socket connections.

There are several ways for you to obtain BluetoothDevices in code. In each case you should check to ensure that the device you intend to connect to is discoverable and (optionally) determine whether you are bonded to it. If you can't discover the remote device, you should prompt the user to enable discoverability on it.

You learned one technique for finding discoverable Bluetooth Devices earlier in this section. Using the startDiscovery method and monitoring ACTION_FOUND broadcasts allows you to receive Broadcast Intents that include a BluetoothDevice.EXTRA_DEVICE extra containing the discovered Bluetooth Device.

You can also use the getRemoteDevice method on your local Bluetooth Adapter, specifying the hardware address of the remote Bluetooth Device you want to connect to:

BluetoothDevice device = bluetooth.getRemoteDevice("01:23:77:35:2F:AA");

This is particularly useful when you know the hardware address of the target device, such as when using a technology such as Android Beam to share this information between devices.

To find the set of currently paired devices, call getBondedDevices on the local Bluetooth Adapter. You can query the returned set to find out if the target Bluetooth Device is already paired with the local Bluetooth Adapter.

final BluetoothDevice knownDevice = 
  bluetooth.getRemoteDevice("01:23:77:35:2F:AA");
 
Set<BluetoothDevice> bondedDevices = bluetooth.getBondedDevices();
 
if (bondedDevices.contains(knownDevice))
  // TODO Target device is bonded / paired with the local device.

Opening a Client Bluetooth Socket Connection

To initiate a communications channel to a remote device, create a Bluetooth Socket using the BluetoothDevice object that represents it.

To create a new connection, call createRfcommSocketToServiceRecord on the Bluetooth Device object representing the target device. Pass in the UUID of its open Bluetooth Server Socket listener.

The returned Bluetooth Socket can then be used to initiate the connection with a call to connect, as shown in Listing 16.7.

2.1

Note that connect is a blocking operation, so it's best practice to initiate connection requests on a background thread rather than block the UI thread until a connection has been made.

2.11

Listing 16.7: Creating a Bluetooth client socket

private void connectToServerSocket(BluetoothDevice device, UUID uuid) {
  try{
    BluetoothSocket clientSocket 
      = device.createRfcommSocketToServiceRecord(uuid);
 
    // Block until server connection accepted.
    clientSocket.connect();
 
    // Start listening for messages.
    listenForMessages(clientSocket);
 
    // Add a reference to the socket used to send messages.
    transferSocket = clientSocket;
 
  } catch (IOException e) {
    Log.e("BLUETOOTH", "Bluetooth client I/O Exception", e);
  }
}

code snippet PA4AD_Ch16_Bluetooth/src/BluetoothActivity.java

If users attempt to connect to a Bluetooth Device that has not yet been paired (bonded) with the host device, they will be prompted to accept the pairing before the connect call completes. The users must accept the pairing request on both the host and remote devices for the connection to be established.

Transmitting Data Using Bluetooth Sockets

After a connection has been established, you will have a Bluetooth Socket on both the client and the server devices. From this point onward there is no significant distinction between them; you can send and receive data using the Bluetooth Socket on both devices.

Data transfer across Bluetooth Sockets is handled via standard Java InputStream and OutputStream objects, which you can obtain from a Bluetooth Socket using the appropriately named getInputStream and getOutputStream methods, respectively.

Listing 16.8 shows two simple skeleton methods—the first used to send a string to a remote device using an Output Stream, and the second to listen for incoming strings using an Input Stream. The same technique can be used to transfer any streamable data.

2.11

Listing 16.8: Sending and receiving strings using Bluetooth Sockets

private void listenForMessages(BluetoothSocket socket, 
                                   StringBuilder incoming) {
      listening = true;
      int bufferSize = 1024;
      byte[] buffer = new byte[bufferSize];
      try {
        InputStream instream = socket.getInputStream();
        int bytesRead = -1;
        while (listening) {
          bytesRead = instream.read(buffer);
          if (bytesRead != -1) {
            String result = "";
            while ((bytesRead == bufferSize) &&
                   (buffer[bufferSize-1] != 0)){
              result = result + new String(buffer, 0, bytesRead - 1);
              bytesRead = instream.read(buffer);
            }
            result = result + new String(buffer, 0, bytesRead - 1);
            incoming.append(result);
          }
          socket.close();
        }
      } catch (IOException e) {
        Log.e(TAG, "Message received failed.", e);
      }
      finally {
      }
    }

code snippet PA4AD_Ch16_Bluetooth/src/BluetoothActivity.java

Managing Network and Internet Connectivity

With the speed, reliability, and cost of Internet connectivity being dependent on the network technology used (Wi-Fi, GPRS, 3G, LTE, and so on), letting your applications know and manage these connections can help to ensure they run efficiently and responsively.

Android broadcasts Intents that allow you to monitor changes in network connectivity and offers APIs that provide control over network settings and connections.

Android networking is principally handled via the ConnectivityManager, a Service that lets you monitor the connectivity state, set your preferred network connection, and manage connectivity failover.

The section “Managing Wi-Fi” describes how to use the WifiManager to monitor and control the device's Wi-Fi connectivity specifically. The WifiManager lets you create new Wi-Fi configurations, monitor and modify the existing Wi-Fi network settings, manage the active connection, and perform access point scans.

Introducing the Connectivity Manager

The ConnectivityManager represents the Network Connectivity Service. It's used to monitor the state of network connections, configure failover settings, and control the network radios.

To use the Connectivity Manager, your application needs read and write network state access permissions:

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

To access the Connectivity Manager, use getSystemService, passing in Context.CONNECTIVITY_SERVICE as the service name, as shown in Listing 16.9.

2.11

Listing 16.9: Accessing the Connectivity Manager

String service = Context.CONNECTIVITY_SERVICE;
 
ConnectivityManager connectivity = 
  (ConnectivityManager)getSystemService(service);

code snippet PA4AD_Ch16_Data_Transfer/src/MyActivity.java

Supporting User Preferences for Background Data Transfers

Until Android 4.0 (API level 14), user preferences for background data transfers were enforced at the application level—meaning that for pre-Android 4.0 platforms, you are responsible for adhering to the user's preference for allowing background data transfers.

To obtain the background data setting, call the getBackgroundDataSetting method on the Connectivity Manager object:

boolean backgroundEnabled = connectivity.getBackgroundDataSetting();

If the background data setting is disabled, your application should transfer data only when it is active and in the foreground. By turning off this value, the user explicitly requests that your application does not transfer data when it is not visible and in the foreground.

If your application requires background data transfer to function, it's best practice to notify users of this requirement and offer to take them to the settings page to alter their preference.

If the user does change the background data preference, the system will send a Broadcast Intent with the Connectivity Manager's ACTION_BACKGROUND_DATA_SETTING_CHANGED action.

To monitor changes in the background data setting, create and register a new Broadcast Receiver that listens for this Broadcast Intent, as shown in Listing 16.10.

2.11

Listing 16.10: Monitoring the background data setting

registerReceiver(
  new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
      boolean backgroundEnabled = 
        connectivity.getBackgroundDataSetting();
      setBackgroundData(backgroundEnabled);
    }
  },
  new IntentFilter(
        ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED)
);

code snippet PA4AD_Ch16_Data_Transfer/src/MyActivity.java

In Android 4.0 and above, getBackgroundDataSetting has been deprecated and will always return true. Users now have much more control over the network data usage of applications, including setting individual data limits and restricting background data.

These preferences are now enforced at the system level, meaning that if data transfer is unavailable for your application, attempts to transfer data or check the network connectivity status will fail, with the device appearing to be offline.

The best way to prevent users from limiting or disabling your applications data transfer is to:

· Minimize the data you transfer

· Modify your data usage based on the connection type (as described in the next section)

· Provide user preferences for modifying your data usage (for example, background update frequency)

If you create a Preference Activity to allow users to modify your application's data usage, you can make it available from within the system settings when a user inspects your application's data usage.

Add a MANAGE_NETWORK_USAGE Intent Filter to the Preference Activity's manifest node, as shown in Listing 16.11.

2.11

Listing 16.11: Making your application's data usage preferences available from system settings

<activity android:name=".MyPreferences" 
          android:label="@string/preference_title">
  <intent-filter>
    <action 
      android:name="android.intent.action.MANAGE_NETWORK_USAGE"
    />
    <category android:name="android.intent.category.DEFAULT" />   
  </intent-filter>
</activity>

code snippet PA4AD_Ch16_Data_Transfer/AndroidManifest.xml

Once set, the View Application Settings button in the system settings will launch your Preference Activity, allowing users to refine your application's data usage rather than restricting or disabling it.

Finding and Monitoring Network Connectivity

The Connectivity Manager provides a high-level view of the available network connections. The getActiveNetworkInfo method returns a NetworkInfo object that includes details on the currently active network:

// Get the active network information.
NetworkInfo activeNetwork = connectivity.getActiveNetworkInfo();

You can also use the getNetworkInfo method to find details on an inactive network of the type specified.

Use the returned NetworkInfo to find the connection status, network type, and detailed state information of the returned network.

Before attempting to transfer data, configure a repeating alarm, or schedule a background service that performs data transfer, use the Connectivity Manager to check that you're actually connected to the Internet, and if so, to verify which type of connection is in place, as shown in Listing 16.12.

Listing 16.12: Determining connectivity

NetworkInfo activeNetwork = connectivity.getActiveNetworkInfo();
 
boolean isConnected = ((activeNetwork  != null) &&   
                       (activeNetwork.isConnectedOrConnecting()));
 
boolean isWiFi = activeNetwork.getType() ==
                 ConnectivityManager.TYPE_WIFI;

code snippet PA4AD_Ch16_Data_Transfer/src/MyActivity.java

By querying the connectivity status and network type, you can temporarily disable downloads and updates, alter your refresh frequency, or defer large downloads based on the bandwidth available.

2.1

Mobile data costs, and the impact of data transfer on battery life, tend to be significantly higher than Wi-Fi, so it's good practice to lower your application's update rate on mobile connections and to defer downloads of significant size until you have a Wi-Fi connection.

To monitor network connectivity, create a Broadcast Receiver that listens for ConnectivityManager.CONNECTIVITY_ACTION Broadcast Intents, as shown in Listing 16.13.

2.11

Listing 16.13: Monitoring connectivity

<receiver android:name=".ConnectivityChangedReceiver" >
  <intent-filter >
    <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
  </intent-filter>
</receiver> 

code snippet PA4AD_Ch16_Data_Transfer/AndroidManifest.xml

These Intents include extras that provide additional details on the change to the connectivity state. You can access each extra using one of the static constants available from the ConnectivityManager class. Most usefully, the EXTRA_NO_CONNECTIVITY extra contains a Boolean that returns true if the device is not connected to any network. Where EXTRA_NO_CONNECTIVITY is false (meaning there is an active connection), it's good practice to use getActiveNetworkInfo to find further details about the new connectivity status and modify your download schedule, as appropriate.

Managing Wi-Fi

The WifiManager, which represents the Android Wi-Fi Connectivity Service, can be used to configure Wi-Fi network connections, manage the current Wi-Fi connection, scan for access points, and monitor changes in Wi-Fi connectivity.

To use the Wi-Fi Manager, your application must have uses-permissions for accessing and changing the Wi-Fi state included in its manifest:

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

Access the Wi-Fi Manager using the getSystemService method, passing in the Context.WIFI_SERVICE constant, as shown in Listing 16.14.

2.11

Listing 16.14: Accessing the Wi-Fi Manager

String service = Context.WIFI_SERVICE;
WifiManager wifi = (WifiManager)getSystemService(service);

code snippet PA4AD_Ch16_WiFi/src/MyActivity.java

You can use the Wi-Fi Manager to enable or disable your Wi-Fi hardware using the setWifiEnabled method, or to request the current Wi-Fi state using the getWifiState or isWifiEnabled methods, as shown in Listing 16.15.

Listing 16.15: Monitoring and changing Wi-Fi state

if (!wifi.isWifiEnabled())
  if (wifi.getWifiState() != WifiManager.WIFI_STATE_ENABLING)
    wifi.setWifiEnabled(true);

code snippet PA4AD_Ch16_WiFi/src/MyActivity.java

The following sections begin with tracking the current Wi-Fi connection status and monitoring changes in signal strength. Later you'll also learn how to scan for and connect to specific access points.

Monitoring Wi-Fi Connectivity

In most cases it's best practice to use the Connectivity Manager to monitor changes in Wi-Fi connectivity; however, the Wifi Manager does broadcast Intents whenever the connectivity status of the Wi-Fi network changes, using an action from one of the following constants defined in theWifiManager class:

· WIFI_STATE_CHANGED_ACTION—Indicates that the Wi-Fi hardware status has changed, moving between enabling, enabled, disabling, disabled, and unknown. It includes two extra values keyed on EXTRA_WIFI_STATE and EXTRA_PREVIOUS_STATE that provide the new and previous Wi-Fi states, respectively.

· SUPPLICANT_CONNECTION_CHANGE_ACTION—This Intent is broadcast whenever the connection state with the active supplicant (access point) changes. It is fired when a new connection is established or an existing connection is lost, using the EXTRA_NEW_STATE Boolean extra, which returns true in the former case.

· NETWORK_STATE_CHANGED_ACTION—Fired whenever the Wi-Fi connectivity state changes. This Intent includes two extras: the first, EXTRA_NETWORK_INFO, includes a NetworkInfo object that details the current network state, whereas the second, EXTRA_BSSID, includes the BSSID of the access point you're connected to.

· RSSI_CHANGED_ACTION—You can monitor the current signal strength of the connected Wi-Fi network by listening for the RSSI_CHANGED_ACTION Intent. This Broadcast Intent includes an integer extra, EXTRA_NEW_RSSI, that holds the current signal strength. To use this signal strength, you should use the calculateSignalLevel static method on the Wi-Fi Manager to convert it to an integer value on a scale you specify.

Monitoring Active Wi-Fi Connection Details

When an active Wi-Fi connection has been established, you can use the getConnectionInfo method on the Wi-Fi Manager to find information on the connection's status. The returned WifiInfo object includes the SSID, BSSID, MAC address, and IP address of the current access point, as well as the current link speed and signal strength, as shown in Listing 16.16.

2.11

Listing 16.16: Querying the active network connection

WifiInfo info = wifi.getConnectionInfo();
if (info.getBSSID() != null) {
  int strength = WifiManager.calculateSignalLevel(info.getRssi(), 5);
  int speed = info.getLinkSpeed();
  String units = WifiInfo.LINK_SPEED_UNITS;
  String ssid = info.getSSID();
 
  String cSummary = String.format("Connected to %s at %s%s. 
                                  Strength %s/5",
                                  ssid, speed, units, strength);
  Log.d(TAG, cSummary);
}

code snippet PA4AD_Ch16_WiFi/src/MyActivity.java

Scanning for Hotspots

You can also use the Wi-Fi Manager to conduct access point scans using the startScan method. An Intent with the SCAN_RESULTS_AVAILABLE_ACTION action will be broadcast to asynchronously announce that the scan is complete and results are available.

Call getScanResults to get those results as a list of ScanResult objects. Each Scan Result includes the details retrieved for each access point detected, including link speed, signal strength, SSID, and the authentication techniques supported.

Listing 16.17 shows how to initiate a scan for access points that displays a Toast indicating the total number of access points found and the name of the access point with the strongest signal.

Listing 16.17: Conducting a scan for Wi-Fi access points

// Register a broadcast receiver that listens for scan results.
registerReceiver(new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    List<ScanResult> results = wifi.getScanResults();
    ScanResult bestSignal = null;
    for (ScanResult result : results) {
      if (bestSignal == null ||
          WifiManager.compareSignalLevel(
            bestSignal.level,result.level) < 0)
        bestSignal = result;
    }
 
    String connSummary = String.format("%s networks found. %s is
                                       the strongest.",
                                       results.size(),
                                       bestSignal.SSID);
 
    Toast.makeText(MyActivity.this, 
                   connSummary, Toast.LENGTH_LONG).show();
  }
}, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));
 
// Initiate a scan.
wifi.startScan();

code snippet PA4AD_Ch16_WiFi/src/MyActivity.java

Managing Wi-Fi Configurations

You can use the Wifi Manager to manage the configured network settings and control which networks to connect to. When connected, you can interrogate the active network connection to get additional details of its configuration and settings.

Get a list of the current network configurations using getConfiguredNetworks. The list of WifiConfiguration objects returned includes the network ID, SSID, and other details for each configuration.

To use a particular network configuration, use the enableNetwork method, passing in the network ID to use and specifying true for the disableAllOthers parameter:

// Get a list of available configurations
List<WifiConfiguration> configurations = wifi.getConfiguredNetworks();
// Get the network ID for the first one.
if (configurations.size() > 0) {
  int netID = configurations.get(0).networkId;
  // Enable that network.
  boolean disableAllOthers = true;
  wifi.enableNetwork(netID, disableAllOthers);
}

Creating Wi-Fi Network Configurations

To connect to a Wi-Fi network, you need to create and register a configuration. Normally, your users would do this using the native Wi-Fi configuration settings, but there's no reason you can't expose the same functionality within your own applications or, for that matter, replace the native Wi-Fi configuration Activity entirely.

Network configurations are stored as WifiConfiguration objects. The following is a nonexhaustive list of some of the public fields available for each Wi-Fi configuration:

· BSSID—The BSSID for an access point

· SSID—The SSID for a particular network

· networkId—A unique identifier used to identify this network configuration on the current device

· priority—The network configuration's priority to use when ordering the list of potential access points to connect to

· status—The current status of this network connection, which will be one of the following: WifiConfiguration.Status.ENABLED, WifiConfiguration.Status.DISABLED, or WifiConfiguration.Status.CURRENT

The Wifi Configuration object also contains the supported authentication techniques, as well as the keys used previously to authenticate with this access point.

The addNetwork method lets you specify a new configuration to add to the current list; similarly, updateNetwork lets you update a network configuration by passing in a WifiConfiguration that's sparsely populated with a network ID and the values you want to change.

You can also use removeNetwork, passing in a network ID, to remove a configuration.

To persist any changes made to the network configurations, you must call saveConfiguration.

Transferring Data Using Wi-Fi Direct

Wi-Fi Direct is a communications protocol designed for medium-range, high-bandwidth peer-to-peer communications. Support for Wi-Fi Direct was added to Android 4.0 (API level 14). Compared to Bluetooth, Wi-Fi Direct is faster and more reliable, and works over greater distances.

Using the Wi-Fi Direct APIs, you can search for, and connect to, other Wi-Fi Direct devices within range. By initiating a communications link using sockets, you can then transmit and receive streams of data between supported devices (including some printers, scanners, cameras, and televisions) and between instances of your application running on different devices.

As a high bandwidth alternative to Bluetooth, Wi-Fi Direct is particularly suitable for operations such as media sharing and live media streaming.

Initializing the Wi-Fi Direct Framework

To use Wi-Fi Direct, your application requires the ACCESS_WIFI_STATE, CHANGE_WIFI_STATE, and INTERNET permissions:

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

Wi-Fi Direct connections are initiated and managed using the WifiP2pManager system service, which you can access using the getSystemService method, passing in the Context.WIFI_P2P_SERVICE constant:

  wifiP2pManager = 
    (WifiP2pManager)getSystemService(Context.WIFI_P2P_SERVICE);

Before you can use the WiFi P2P Manager, you must create a channel to the Wi-Fi Direct framework using the Wifi P2P Manager's initialize method. Pass in the current Context, the Looper on which to receive Wi-Fi Direct events, and a ChannelListener to listen for the loss of your channel connection, as shown in Listing 16.18.

2.11

Listing 16.18: Initializing Wi-Fi Direct

private WifiP2pManager wifiP2pManager;
private Channel wifiDirectChannel;
 
private void initializeWiFiDirect() {
  wifiP2pManager = 
    (WifiP2pManager)getSystemService(Context.WIFI_P2P_SERVICE);
 
  wifiDirectChannel = wifiP2pManager.initialize(this, getMainLooper(), 
    new ChannelListener() {
      public void onChannelDisconnected() {
       initializeWiFiDirect();
      }
    }
  );
}

code snippet PA4AD_Ch16_WiFiDirect/src/WiFiDirectActivity.java

You will use this Channel whenever you interact with the Wi-Fi Direct framework, so initializing the WiFi P2P Manager will typically be done within the onCreate handler of your Activity.

Most actions performed using the WiFi P2P Manager (such as peer discovery and connection attempts) will immediately indicate their success (or failure) using an ActionListener, as shown in Listing 16.19. When successful, the return values associated with those actions are obtained by receiving Broadcast Intents, as described in the following sections.

Listing 16.19: Creating a WiFi P2P Manager Action Listener

private ActionListener actionListener = new ActionListener() {
  public void onFailure(int reason) {
    String errorMessage = "WiFi Direct Failed: ";
    switch (reason) {
      case WifiP2pManager.BUSY : 
        errorMessage += "Framework busy."; break;
      case WifiP2pManager.ERROR : 
        errorMessage += "Internal error."; break;
      case WifiP2pManager.P2P_UNSUPPORTED : 
        errorMessage += "Unsupported."; break;
      default: 
        errorMessage += "Unknown error."; break;
    }
    Log.d(TAG, errorMessage);
  }
  
  public void onSuccess() {
    // Success! 
    // Return values will be returned using a Broadcast Intent
  }
};

code snippet PA4AD_Ch16_WiFiDirect/src/WiFiDirectActivity.java

Enabling Wi-Fi Direct and Monitoring Its Status

For an Android Device to find other Wi-Fi Direct devices, or to be discoverable by them, users must first enable Wi-Fi Direct. You can launch the settings screen, for users to change this setting, by starting a new Activity using the android.provider.Settings.ACTION_WIRELESS_SETTINGS class, as shown in Listing 16.20.

2.11

Listing 16.20: Enabling Wi-Fi Direct on a device

Intent intent = new Intent(
  android.provider.Settings.ACTION_WIRELESS_SETTINGS);
 
startActivity(intent);

code snippet PA4AD_Ch16_WiFiDirect/src/WiFiDirectActivity.java

Wi-Fi Direct will remain enabled only until you have made a connection and transferred data. It will be disabled automatically after a short period of inactivity.

You will be able to perform Wi-Fi Direct operations only while Wi-Fi Direct is enabled on the device, so it's important to listen for changes in its status, modifying your UI to disable actions that aren't available.

You can monitor the Wi-Fi Direct status by registering a Broadcast Receiver that receives the WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION action:

IntentFilter p2pEnabledFilter = new
   IntentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
 
registerReceiver(p2pStatusReceiver, p2pEnabledFilter);

The Intent received by the associated Broadcast Receiver, as shown in Listing 16.21, will include a WifiP2pManager.EXTRA_WIFI_STATE extra that will be set to either WIFI_P2P_STATE_ENABLED or WIFI_P2P_STATE_DISABLED.

Listing 16.21: Receiving a Wi-Fi Direct status change

BroadcastReceiver p2pStatusReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    int state = intent.getIntExtra(
      WifiP2pManager.EXTRA_WIFI_STATE,
      WifiP2pManager.WIFI_P2P_STATE_DISABLED);
    
    switch (state) {
      case (WifiP2pManager.WIFI_P2P_STATE_ENABLED): 
        buttonDiscover.setEnabled(true);
        break;
      default: 
        buttonDiscover.setEnabled(false);
    }
  }
};

code snippet PA4AD_Ch16_WiFiDirect/src/WiFiDirectActivity.java

Within the onReceive handler, you can modify your UI accordingly based on the change in state.

After creating a channel to the Wi-Fi Direct framework and enabling Wi-Fi Direct on the host and its peer device(s), you can begin the process of discovering and connecting to peers.

Discovering Peers

To initiate a scan for peers, call the WiFi P2P Manager's discoverPeers method, passing in the active channel and an Action Listener. Changes to the peer list will be broadcast as an Intent using the WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION action. Peer discovery will remain active until a connection is established or a group is created.

When you receive an Intent notifying you of a change to the peer list, you can request the current list of discovered peers using the WifiP2pManager.requestPeers method, as shown in Listing 16.22.

2.11

Listing 16.22: Discovering Wi-Fi Direct peers

private void discoverPeers() {
  wifiP2pManager.discoverPeers(wifiDirectChannel, actionListener);
}
 
BroadcastReceiver peerDiscoveryReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    wifiP2pManager.requestPeers(wifiDirectChannel, 
      new PeerListListener() {
        public void onPeersAvailable(WifiP2pDeviceList peers) {
          deviceList.clear();
          deviceList.addAll(peers.getDeviceList());
          aa.notifyDataSetChanged();
        }
      });
  }
};

code snippet PA4AD_Ch16_WiFiDirect/src/WiFiDirectActivity.java

The requestPeers method accepts a PeerListListener whose onPeersAvailable handler will execute when the peer list has been retrieved. The list of peers will be available as a WifiP2pDeviceList, which you can then query to find the name and address of all the available peer devices.

Connecting with Peers

To form a Wi-Fi Direct connection with a peer device, use the WiFi P2P Manager's connect method, passing in the active channel, an Action Listener, and a WifiP2pConfig object that specifies the address of the peer to connect to, as shown in Listing 16.23.

2.11

Listing 16.23: Requesting a connection to a Wi-Fi Direct peer

private void connectTo(WifiP2pDevice device) {
  WifiP2pConfig config = new WifiP2pConfig();
  config.deviceAddress = device.deviceAddress;
 
  wifiP2pManager.connect(wifiDirectChannel, config, actionListener);
}

code snippet PA4AD_Ch16_WiFiDirect/src/WiFiDirectActivity.java

When you attempt to establish a connection, the remote device will be prompted to accept it. On Android devices this requires the user to manually accept the connection request using the dialog shown in Figure 16.4.

Figure 16.4

16.4

If the device accepts the connection request, the successful connection is broadcast on both devices using the WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION Intent action.

The Broadcast Intent will include a NetworkInfo object parceled within the WifiP2pManager.EXTRA_NETWORK_INFO extra. You can query the Network Info to confirm whether the change in connection status represents a new connection or a disconnection:

boolean connected = networkInfo.isConnected();

In the former case, you can request further details on the connection using the WifiP2pManager.requestConnectionInfo method, passing in the active channel and a ConnectionInfoListener, as shown in Listing 16.24.

2.11

Listing 16.24: Connecting to a Wi-Fi Direct peer

BroadcastReceiver connectionChangedReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    
    // Extract the NetworkInfo
    String extraKey = WifiP2pManager.EXTRA_NETWORK_INFO;
    NetworkInfo networkInfo = 
      (NetworkInfo)intent.getParcelableExtra(extraKey);
 
    // Check if we're connected
    if (networkInfo.isConnected()) {
      wifiP2pManager.requestConnectionInfo(wifiDirectChannel, 
        new ConnectionInfoListener() {
          public void onConnectionInfoAvailable(WifiP2pInfo info) { 
            // If the connection is established
            if (info.groupFormed) {
              // If we're the server
              if (info.isGroupOwner) {
                 // TODO Initiate server socket.
              }
              // If we're the client
              else if (info.groupFormed) {
                // TODO Initiate client socket.
              }
            }
          }
        });
    } else {
      Log.d(TAG, "Wi-Fi Direct Disconnected");
    }
  }
};

code snippet PA4AD_Ch16_WiFiDirect/src/WiFiDirectActivity.java

The ConnectionInfoListener will fire its onConnectionInfoAvailable handler when the connection details become available, passing in a WifiP2pInfo object that includes those details.

When a collection is established, a group consisting of the peers connected is formed. The initiator of the connection will be returned as the group owner and would typically (but not necessarily) take on the role of server for further communications.

2.1

Each P2P connection is regarded as a group, even if that connection is exclusively between two peers. If you need to connect with legacy devices that don't support Wi-Fi Direct, you can manually create groups to create a virtual access point to which they can connect.

Having established a connection, you can use standard TCP/IP sockets to transmit data between devices, as described later in the next section.

Transferring Data Between Peers

Although the specifics of any particular data transfer implementation is beyond the scope of this book, this section describes the basic process of transmitting data between connected devices using standard Java sockets.

To establish a socket connection, one device must create a ServerSocket that listens for connection requests, and the other device must create a client Socket that makes connection requests. This distinction is relevant only in terms of establishing the connection—after that connection is established, the data can flow in either direction.

Create a new server-side socket using the ServerSocket class, specifying a port on which to listen for requests. Call its accept method to listen for incoming requests, as shown in Listing 16.25.

2.11

Listing 16.25: Creating a Server Socket

ServerSocket serverSocket = new ServerSocket(8666);
Socket serverClient = serverSocket.accept();

code snippet PA4AD_Ch16_WiFiDirect/src/WiFiDirectActivity.java

To request a connection from the client device, create a new Socket and use its connect method, specifying the host address of the target device, a port to connect on, and a timeout for the connection request, as shown in Listing 16.26.

Listing 16.26: Creating a client Socket

int timeout = 10000;
int port = 8666;
 
InetSocketAddress socketAddress 
  = new InetSocketAddress(hostAddress, port);
 
try {
  Socket socket = new Socket();
  socket.bind(null);
  socket.connect(socketAddress, timeout);
} catch (IOException e) {
  Log.e(TAG, "IO Exception.", e);
}

code snippet PA4AD_Ch16_WiFiDirect/src/WiFiDirectActivity.java

Like the Server Socket call to accept, the call to connect is a blocking call that will return after the Socket connection has been established.

After the sockets have been established, you can create Input Streams and Output Streams on either the server- or client-side sockets to transmit and receive data bidirectionally.

2.1

Network communications such as those described here should always be handled on a background thread to avoid blocking the UI thread. This is particularly the case when establishing the network connection because both the server- and client-side logic includes blocking calls that will disrupt the UI.

Near Field Communication

Android 2.3 (API level 9) introduced Near Field Communication (NFC) APIs. NFC is a contactless technology used to transmit small amounts of data across short distances (typically less than 4 centimeters).

NFC transfers can occur between two NFC-enabled devices, or between a device and an NFC “tag.” Tags can range from passive tags that transmit a URL when scanned to complex systems such as those used in NFC payment solutions, such as Google Wallet.

NFC messages in Android are handled using the NFC Data Exchange Format (NDEF).

In order to read, write, or broadcast NFC messages, your application requires the NFC manifest permission:

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

Reading NFC Tags

When an Android device is used to scan an NFC tag, the system will decode the incoming payload using its own tag dispatch system, which analyzes the tag, categorizes the data, and uses an Intent to launch an application to receive the data.

For an application to receive NFC data, you need to add an Activity Intent Filter that listens for one of the following Intent actions:

· NfcAdapter.ACTION_NDEF_DISCOVERED—The highest priority, and most specific, of the NFC messages. Intents using this action include MIME types and/or URI data. It's best practice to listen for this broadcast whenever possible because the extra data allows you to be more specific in defining which tags to respond to.

· NfcAdapter.ACTION_TECH_DISCOVERED—This action is broadcast when the NFC technology is known but the tag contains no data—or contains data that can't be mapped to a MIME type or URI.

· NfcAdapter.ACTION_TAG_DISCOVERED—If a tag is received from an unknown technology, it will be broadcast using this action.

Listing 16.27 shows how to register an Activity that will respond only to NFC tags that correspond to a URI that points to my blog.

2.11

Listing 16.27: Listening for NFC tags

<activity android:name=".BlogViewer">
  <intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <data android:scheme="http"
          android:host="blog.radioactiveyak.com"/>
  </intent-filter>
</activity>

code snippet PA4AD_Ch16_NFC/AndoridManifest.xml

It's good practice to make your NFC Intent Filters as specific as possible to minimize the number of applications available to respond to a given NFC tag and provide the best, fastest user experience.

In many cases the Intent data/URI and MIME type are sufficient for your application to respond accordingly. However, if required, the payload delivered from an NFC message is available through extras within the Intent that started your Activity.

The NfcAdapter.EXTRA_TAG extra includes a raw Tag object that represents the scanned tag. The NfcAdapter.EXTRA_TNDEF_MESSAGES extra contains an array of NDEF Messages, as shown in Listing 16.28.

Listing 16.28: Extracting NFC tag payloads

String action = getIntent().getAction();
 
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
  Parcelable[] messages = 
    intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
 
  for (int i = 0; i < messages.length; i++) {
    NdefMessage message = (NdefMessage)messages[i];
    NdefRecord[] records = message.getRecords();
 
    for (int j = 0; j < records.length; j++) {
      NdefRecord record = records[j];
      // TODO Process the individual records.
    }
  }
}

code snippet PA4AD_Ch16_NFC/src/BeamerActivity.java

Using the Foreground Dispatch System

By default, the tag dispatch system will determine which application should receive a particular tag based on the standard process of Intent resolution. In that process, the foreground Activity has no priority over other applications; so, if several applications are all registered to receive a tag of the type scanned, the user will be prompted to select which to use, even if your application is in the foreground at the time.

Using the foreground dispatch system, you can specify a particular Activity as having priority, allowing it to become the default receiver when it is in the foreground. Foreground dispatching can be toggled using the enable/disableForegroundDispatch methods on the NFC Adapter. Foreground dispatching can be used only while an Activity is in the foreground, so it should be enabled and disabled from within your onResume and onPause handlers, respectively, as shown in Listing 16.29. The parameters to enableForegroundDispatch are described following the example.

2.11

Listing 16.29: Using the foreground dispatch system

public void onPause() {
  super.onPause();
 
  nfcAdapter.disableForegroundDispatch(this);
}
 
@Override
public void onResume() {
  super.onResume();
  nfcAdapter.enableForegroundDispatch(
    this,
    // Intent that will be used to package the Tag Intent.
    nfcPendingIntent,
    // Array of Intent Filters used to declare the Intents you
    // wish to intercept.
    intentFiltersArray,
    // Array of Tag technologies you wish to handle.
    techListsArray);
 
  String action = getIntent().getAction();
  if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
    processIntent(getIntent());
  }
}

code snippet PA4AD_Ch16_NFC/src/BeamerActivity.java

The Intent Filters array should declare the URIs or MIME types you want to intercept—any received tags that don't match these criteria will be handled using the standard tag dispatching system. To ensure a good user experience, it's important that you specify only the tag content your application handles.

You can further refine the tags you receive by explicitly indicating the technologies you want to handle, typically represented by adding the NfcF class.

Finally, the Pending Intent will be populated by the NFC Adapter to transmit the received tag directly to your application.

Listing 16.30 shows the Pending Intent, MIME type array, and technologies array used to enable the foreground dispatching in Listing 16.29.

2.11

Listing 16.30: Configuring foreground dispatching parameters

PendingIntent nfcPendingIntent;
IntentFilter[] intentFiltersArray;
String[][] techListsArray;
 
@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  
  [... Existing onCreate logic ...]
 
  // Create the Pending Intent.  
  int requestCode = 0;
  int flags = 0;
 
  Intent nfcIntent = new Intent(this, getClass());
  nfcIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
   
  nfcPendingIntent = 
    PendingIntent.getActivity(this, requestCode, nfcIntent, flags);
  
  // Create an Intent Filter limited to the URI or MIME type to
  // intercept TAG scans from.
  IntentFilter tagIntentFilter = 
    new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
  tagIntentFilter.addDataScheme("http");
  tagIntentFilter.addDataAuthority("blog.radioactiveyak.com", null);
  intentFiltersArray = new IntentFilter[] { tagIntentFilter };
 
  // Create an array of technologies to handle.
  techListsArray = new String[][] {
    new String[] { 
      NfcF.class.getName() 
    } 
  };
}

code snippet PA4AD_Ch16_NFC/src/BeamerActivity.java

Introducing Android Beam

Android Beam, introduced in Android 4.0 (API level 14), provides a simple API for an application to transmit data between two devices using NFC, simply by placing them back-to-back. For example, the native contacts, browser, and YouTube applications use Android Beam to share the currently viewed contact, web page, and video, respectively, with other devices.

2.1

To beam messages, your application must be in the foreground and the device receiving the data must not be locked.

Android Beam is initiated by tapping two NFC-enabled Android devices together. Users are presented with a “touch to beam” UI, at which point they can choose to “beam” the foreground application to the other device.

Android Beam uses NFC to push NDEF messages between devices when they are physically placed together.

By enabling Android Beam within your application, you can define the payload of the beamed message. If you don't customize the message, the default action for your application will be to launch it on the target device. If your application isn't installed on the target device, the Google Play Store will launch and display your application's details page.

To define the message your application beams, you need to request the NFC permission in the manifest:

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

The process to define your own custom payload is described as follows:

1. Create an NdefMessage object that contains an NdefRecord that contains your message payload.

2. Assign your Ndef Message to the NFC Adapter as your Android Beam payload.

3. Configure your application to listen for incoming Android Beam messages.

Creating Android Beam Messages

To create a new Ndef Message, create a new NdefMessage object that contains at least one NdefRecord containing the payload you want to beam to your application on the target device.

When creating a new Ndef Record, you must specify the type of record it represents, a MIME type, an ID, and a payload. There are several common types of Ndef Record that can be used to transmit data using Android Beam; note that they should always be the first record added to each beamed message.

Using the NdefRecord.TNF_MIME_MEDIA type, you can transmit an absolute URI:

NdefRecord uriRecord = new NdefRecord(
  NdefRecord.TNF_ABSOLUTE_URI,
  "http://blog.radioactiveyak.com".getBytes(Charset.forName("US-ASCII")),
  new byte[0], new byte[0]);

This is the most common Ndef Record transmitted using Android Beam because the received Intent will be of the same form as any Intent used to start an Activity. The Intent Filter used to decide which NFC messages a particular Activity should receive can use the scheme, host, andpathPrefix attributes.

If you need to transmit messages that contain information that can't be easily interpreted as a URI, the NdefRecord.TNF_MIME_MEDIA type supports the creation of an application-specific MIME type and the inclusion of associated payload data:

byte[] mimeType = "application/com.paad.nfcbeam".getBytes(Charset.forName("US-ASCII"));
byte[] tagId = new byte[0];
byte[] payload = "Not a URI".getBytes(Charset.forName("US-ASCII"));
 
NdefRecord mimeRecord = new NdefRecord(
  NdefRecord.TNF_MIME_MEDIA,
  mimeType,
  tagId, 
  payload);

A more complete examination of the available NDEF record types and how to use them can be found in the Android Developer Guide (http://developer.android.com/guide/topics/nfc/nfc.html#creating-records).

It's good practice to also include an Ndef Record in the form of an Android Application Record (AAR). This guarantees that your application will be launched on the target device, and that if your application isn't installed, the Google Play Store will be lunched for the user to install it.

To create an AAR Ndef Record, use the createApplicationRecord static method on the Ndef Record class, specifying the package name of your application, as shown in Listing 16.31.

2.11

Listing 16.31: Creating an Android Beam NDEF message

String payload = "Two to beam across";
 
String mimeType = "application/com.paad.nfcbeam";
byte[] mimeBytes = mimeType.getBytes(Charset.forName("US-ASCII"));
 
NdefMessage nfcMessage = new NdefMessage(new NdefRecord[] { 
  // Create the NFC payload.
  new NdefRecord(
    NdefRecord.TNF_MIME_MEDIA, 
    mimeBytes, 
    new byte[0],    
    payload.getBytes()),
 
  // Add the AAR (Android Application Record)
  NdefRecord.createApplicationRecord("com.paad.nfcbeam")
});

code snippet PA4AD_Ch16_NFCBeam/src/BeamerActivity.java

Assigning the Android Beam Payload

You specify your Android Beam payload using the NFC adapter. You can access the default NFC adapter using the static getDefaultAdapter method on the NfcAdapter class:

NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);

There are two alternatives for specifying the NDEF Message created in Listing 16.31 as your application's Android Beam payload. The simplest way is to use the setNdefPushMessage method to assign a message that should always be sent from the current Activity if Android Beam is initiated. You would typically make this assignment once, from within your Activity's onResume method:

nfcAdapter.setNdefPushMessage(nfcMessage, this);

A better alternative is to use the setNdefPushMessageCallback method. This handler will fire immediately before your message is beamed, allowing you to dynamically set the payload content based on the application's current context—for example, which video is being watched, which web page is being browsed, or which map coordinates are centered, as shown in Listing 16.32.

2.11

Listing 16.32: Setting your Android Beam message dynamically

nfcAdapter.setNdefPushMessageCallback(new CreateNdefMessageCallback() {
  public NdefMessage createNdefMessage(NfcEvent event) {
    String payload = "Beam me up, Android!\n\n" +
        "Beam Time: " + System.currentTimeMillis();
    
    NdefMessage message = createMessage(payload);
    
    return message;
  }
}, this);

code snippet PA4AD_Ch16_NFCBeam/src/BeamerActivity.java

If you set both a static message and a dynamic message using the callback hander, only the latter will be transmitted.

Receiving Android Beam Messages

Android Beam messages are received much like NFC tags, as described earlier in this chapter. To receive the payload you packaged in the previous section, start by adding a new Intent Filter to your Activity, as shown in Listing 16.33.

2.11

Listing 16.33: Android Beam Intent Filter

<intent-filter>
  <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
  <category android:name="android.intent.category.DEFAULT"/>
  <data android:mimeType="application/com.paad.nfcbeam"/>
</intent-filter>

code snippet PA4AD_Ch16_NFCBeam/AndroidManifest.xml

The Activity will be launched on the recipient device when an Android Beam has been initiated, or, if your application isn't installed, the Google Play Store will be launched to allow the user to download it.

The beam data will be delivered to your Activity using an Intent with the NfcAdapter.ACTION_NDEF_DISCOVERED action and the payload available as an array of NdfMessages stored against the NfcAdapter.EXTRA_NDEF_MESSAGES extra, as shown in Listing 16.34.

Listing 16.34: Extracting the Android Beam payload

Parcelable[] messages = intent.getParcelableArrayExtra(
  NfcAdapter.EXTRA_NDEF_MESSAGES);
 
NdefMessage message = (NdefMessage)messages[0];
NdefRecord record = message.getRecords()[0];
 
String payload = new String(record.getPayload());

code snippet PA4AD_Ch16_NFCBeam/src/BeamerActivity.java

Typically, the payload string will be in the form of a URI, allowing you to extract and handle it as you would the data encapsulated within an Intent to display the appropriate video, web page, or map coordinates.