Chapter     4

Communications and Networking

The key to many successful mobile applications is their ability to connect and interact with remote data sources. Web services and APIs are abundant in today’s world, allowing an application to interact with just about any service, from weather forecasts to personal financial information. Bringing this data into the palm of a user’s hand and making it accessible from anywhere is one of the greatest powers of the mobile platform. Android builds on the web foundations that Google is known for and provides a rich toolset for communicating with the outside world.

4-1. Displaying Web Information

Problem

HTML or image data from the Web needs to be presented in the application without any modification or processing.

Solution

(API Level 1)

Display the information in a WebView. WebView is a view widget that can be embedded in any layout to display web content, both local and remote, in your application. WebView is based on the same open source WebKit technology that powers the Android Browser application, affording applications the same level of power and capability.

How It Works

WebView has some very desirable properties when displaying assets downloaded from the Web, not the least of which are two-dimensional scrolling (horizontal and vertical at the same time) and zoom controls. A WebView can be the perfect place to house a large image, such as a stadium map, in which the user may want to pan and zoom around. Here we will discuss how to do this with both local and remote assets.

Display a URL

The simplest case is displaying an HTML page or image by supplying the URL of the resource to the WebView. The following are a handful of practical uses for this technique in your applications:

  • Provide access to your corporate site without leaving the application.
  • Display a page of live content from a web server, such as an FAQ section, that can be changed without requiring an upgrade to the application.
  • Display a large image resource that the user would want to interact with using pan/zoom.

Let’s take a look at a simple example that loads a very popular web page inside the content view of an activity instead of within the browser (see Listings 4-1 and 4-2).

Listing 4-1. Activity Containing a WebView

public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        WebView webview = new WebView(this);
        //Enable JavaScript support
        webview.getSettings().setJavaScriptEnabled(true);
        webview.loadUrl("http://www.google.com/");
 
        setContentView(webview);
    }
}

Note  By default, WebView has JavaScript support disabled. Be sure to enable JavaScript in the WebView.WebSettings object if the content you are displaying requires it.

Listing 4-2. AndroidManifest.xml Setting the Required Permissions

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

Important  If the content you are loading into WebView is remote, AndroidManifest.xml must declare that it uses the android.permission.INTERNET permission.

The result displays the HTML page in your activity (see Figure 4-1).

9781430263227_Fig04-01.jpg

Figure 4-1. HTML page in a WebView

Local Assets

WebView is also quite useful in displaying local content to take advantage of either HTML/CSS formatting or the pan/zoom behavior it provides to its contents. You may use the assets directory of your Android project to store resources you would like to display in a WebView, such as large images or HTML files. To better organize the assets, you may also create subdirectories under assets to store files in.

WebView.loadUrl() can display files stored under assets by using the file:///android_asset/<resource path> URL schema. For example, if the file android.jpg was placed into the assets directory, it could be loaded into a WebView using the following URL:

file:///android_asset/android.jpg

If that same file were placed in a directory named images under assets, WebView could load it with the following URL:

file:///android_asset/images/android.jpg

In addition, WebView.loadData() will load raw HTML stored in a string resource or variable into the view. Using this technique, preformatted HTML text could be stored in res/values/strings.xml or downloaded from a remote API and displayed in the application.

Listings 4-3 and 4-4 show an example activity with two WebView widgets stacked vertically on top of one another. The upper view is displaying a large image file stored in the assets directory, and the lower view is displaying an HTML string stored in the application’s string resources.

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

<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">
  <WebView
    android:id="@+id/upperview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
  />
  <WebView
    android:id="@+id/lowerview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
  />
</LinearLayout>

Listing 4-4. Activity to Display Local Web Content

public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        WebView upperView = (WebView)findViewById(R.id.upperview);
        //Zoom feature must be enabled
        upperView.getSettings().setBuiltInZoomControls(true);
        upperView.loadUrl("file:///android_asset/android.jpg");
 
        WebView lowerView = (WebView)findViewById(R.id.lowerview);
        String htmlString =
                "<h1>Header</h1><p>This is HTML text<br />"
                + "<i>Formatted in italics</i></p>";
        lowerView.loadData(htmlString, "text/html", "utf-8");
    }
}

When the activity is displayed, each WebView occupies half of the screen’s vertical space. The HTML string is formatted as expected, while the large image can be scrolled both horizontally and vertically; the user may even zoom in or out (see Figure 4-2).

9781430263227_Fig04-02.jpg

Figure 4-2. Two WebViews displaying local resources

4-2. Intercepting WebView Events

Problem

Your application is using a WebView to display content, but it also needs to listen and respond to users clicking links on the page.

Solution

(API Level 1)

Implement a WebViewClient and attach it to the WebView. WebViewClient and WebChromeClient are two WebKit classes that allow an application to get event callbacks and customize the behavior of the WebView. By default, WebView will pass a URL to the ActivityManager to be handled if no WebViewClient is present, which usually results in any clicked link loading in the Browser application instead of the current WebView.

How It Works

In Listing 4-5, we create an activity with a WebView that will handle its own URL loading.

Listing 4-5. Activity with a WebView That Handles URLs

public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        WebView webview = new WebView(this);
        webview.getSettings().setJavaScriptEnabled(true);
        //Add a client to the view
        webview.setWebViewClient(new WebViewClient());
        webview.loadUrl("http://www.google.com");
        setContentView(webview);
    }
}

In this example, simply providing a plain vanilla WebViewClient to WebView allows it to handle any URL requests itself, instead of passing them up to the ActivityManager, so clicking a link will load the requested page inside the same view. This is because the default implementation simply returns false for shouldOverrideUrlLoading(), which tells the client to pass the URL to the WebView and not to the application.

In this next case, we will take advantage of the WebViewClient.shouldOverrideUrlLoading() callback to intercept and monitor user activity (see Listing 4-6).

Listing 4-6. Activity That Intercepts WebView URLs

public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        WebView webview = new WebView(this);
        webview.getSettings().setJavaScriptEnabled(true);
        //Add a client to the view
        webview.setWebViewClient(mClient);
        webview.loadUrl("http://www.google.com");
        setContentView(webview);
    }
 
    private WebViewClient mClient = new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view,
                String url) {
            Uri request = Uri.parse(url);
 
            if(TextUtils.equals(request.getAuthority(),
                    "www.google.com")) {
                //Allow the load
                return false;
            }
 
            Toast.makeText(MyActivity.this,
                "Sorry, buddy",
                Toast.LENGTH_SHORT).show();
            return true;
        }
    };
}

In this example, shouldOverrideUrlLoading() determines whether to load the content back in this WebView based on the URL it was passed, keeping the user from leaving Google’s site. Uri.getAuthority() returns the hostname portion of a URL, and we use that to check whether the link the user clicked is on Google’s domain (www.google.com). If we can verify that the link is to another Google page, returning false allows the WebView to load the content. If not, we notify the user and, returning true, tell the WebViewClient that the application has taken care of this URL and not to allow the WebView to load it.

This technique can be more sophisticated, enabling the application to actually handle the URL by doing something interesting. A custom schema could even be developed to create a full interface between your application and the WebView content.

4-3. Accessing WebView with JavaScript

Problem

Your application needs access to the raw HTML of the current contents displayed in a WebView, either to read or modify specific values.

Solution

(API Level 1)

Create a JavaScript interface to bridge between the WebView and application code.

How It Works

WebView.addJavascriptInterface() binds a Java object to JavaScript so that its methods can then be called within the WebView. Using this interface, JavaScript can be used to marshal data between your application code and the WebView’s HTML.

Caution  Allowing JavaScript to control your application can inherently present a security threat, allowing remote execution of application code. This interface should be utilized with that possibility in mind.

Let’s look at an example of this in action. Listing 4-7 presents a simple HTML form to be loaded into the WebView from the local assets directory. Listing 4-8 is an activity that uses two JavaScript functions to exchange data between the activity preferences and content in the WebView.

Listing 4-7. assets/form.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
    "http://www.w3.org/TR/html4/strict.dtd">
<html>
 
<form name="input" action="form.html" method="get">
Enter Email: <input type="text" id="emailAddress" />
<input type="submit" value="Submit" />
</form>
 
</html>

Listing 4-8. Activity with JavaScript Bridge Interface

public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        WebView webview = new WebView(this);
        //JavaScript is not enabled by default
        webview.getSettings().setJavaScriptEnabled(true);
        webview.setWebViewClient(mClient);
        //Attach the custom interface to the view
        webview.addJavascriptInterface(new MyJavaScriptInterface(), "BRIDGE");
 
        setContentView(webview);
        
        webview.loadUrl("file:///android_asset/form.html");
    }
 
    private static final String JS_SETELEMENT =
            "javascript:document.getElementById('%s').value='%s'";
    private static final String JS_GETELEMENT =
            "javascript:window.BRIDGE"
            + ".storeElement('%s',document.getElementById('%s').value)";
    private static final String ELEMENTID = "emailAddress";
 
    private WebViewClient mClient = new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            //Before leaving the page, attempt to retrieve the email
            // using JavaScript
            executeJavascript(view,
                    String.format(JS_GETELEMENT, ELEMENTID, ELEMENTID) );
            return false;
        }
 
        @Override
        public void onPageFinished(WebView view, String url) {
            //When page loads, inject address into page using JavaScript
            SharedPreferences prefs = getPreferences(Activity.MODE_PRIVATE);
            executeJavascript(view, String.format(JS_SETELEMENT, ELEMENTID,
                    prefs.getString(ELEMENTID, "")) );
        }
    };
 
    private void executeJavascript(WebView view, String script) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            view.evaluateJavascript(script, null);
        } else {
            view.loadUrl(script);
        }
    }
    
    private class MyJavaScriptInterface {
        //Store an element in preferences
        @JavascriptInterface
        public void storeElement(String id, String element) {
            SharedPreferences.Editor edit =
                    getPreferences(Activity.MODE_PRIVATE).edit();
            edit.putString(id, element);
            edit.commit();
            //If element is valid, raise a Toast
            if(!TextUtils.isEmpty(element)) {
                Toast.makeText(MyActivity.this, element, Toast.LENGTH_SHORT)
                        .show();
            }
        }
    }
}

In this somewhat contrived example, a single element form is created in HTML and displayed in a WebView. In the activity code, we look for a form value in the WebView with the ID of emailAddress, and its value is saved to SharedPreferences every time a link is clicked on the page (in this case, the Submit button of the form) through the shouldOverrideUrlLoading() callback. Whenever the page finished loading (that is, onPageFinished() is called), we attempt to inject the current value from SharedPreferences back into the web form.

Note  JavaScript is not enabled by default in WebView. In order to inject or even simply render JavaScript, we must call WebSettings.setJavaScriptEnabled(true) when initializing the view.

A Java class is created called MyJavaScriptInterface, which defines the method storeElement(). When the view is created, we call the WebView.addJavascriptInterface() method to attach this object to the view and give it the name BRIDGE. When calling this method, the string parameter is a name used to reference the interface inside JavaScript code.

We have defined two JavaScript methods as constant strings here: JS_GETELEMENT and JS_SETELEMENT. Prior to Android 4.4, we executed these methods on the WebView by calling the same loadUrl() method we’ve seen before. However, in API Level 19 and beyond, we have a new method on WebView named evaluateJavascript() for this purpose. The example code verifies the API level currently in use and calls the appropriate method.

Notice that JS_GETELEMENT is a reference to calling our custom interface function (referenced as BRIDGE.storeElement), which will call that method on MyJavaScriptInterface and store the form element’s value in preferences. If the value retrieved from the form is not blank, a Toast will also be raised.

Any JavaScript may be executed on the WebView in this manner, and it does not need to be a method included as part of the custom interface. JS_SETELEMENT, for example, uses pure JavaScript to set the value of the form element on the page.

One popular application of this technique is to remember form data that a user may need to enter in the application, but the form must be web based, such as a reservation form or payment form for a web application that doesn’t have a lower-level API to access.

4-4. Downloading an Image File

Problem

Your application needs to download and display an image from the Web or another remote server.

Solution

(API Level 3)

Use AsyncTask to download the data in a background thread. AsyncTask is a wrapper class that makes threading long-running operations into the background painless and simple; it also manages concurrency with an internal thread pool. In addition to handling the background threading, callback methods are provided before, during, and after the operation executes, allowing you to make any updates required on the main UI thread.

How It Works

In the context of downloading an image, let’s create a subclass of ImageView called WebImageView, which will lazily load an image from a remote source and display it as soon as it is available. The downloading will be performed inside an AsyncTask operation (see Listing 4-9).

Listing 4-9. WebImageView

public class WebImageView extends ImageView {
 
    private Drawable mPlaceholder, mImage;
 
    public WebImageView(Context context) {
        this(context, null);
    }
 
    public WebImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
 
    public WebImageView(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defaultStyle);
    }
 
    public void setPlaceholderImage(Drawable drawable) {
        mPlaceholder = drawable;
        if(mImage == null) {
            setImageDrawable(mPlaceholder);
        }
    }
 
    public void setPlaceholderImage(int resid) {
        mPlaceholder = getResources().getDrawable(resid);
        if(mImage == null) {
            setImageDrawable(mPlaceholder);
        }
    }
 
    public void setImageUrl(String url) {
        DownloadTask task = new DownloadTask();
        task.execute(url);
    }
 
    private class DownloadTask extends
            AsyncTask<String, Void, Bitmap> {
        @Override
        protected Bitmap doInBackground(String... params) {
            String url = params[0];
            try {
                URLConnection connection =
                        (new URL(url)).openConnection();
                InputStream is = connection.getInputStream();
                BufferedInputStream bis =
                        new BufferedInputStream(is);
 
                ByteArrayBuffer baf = new ByteArrayBuffer(50);
                int current = 0;
                while ((current = bis.read()) != -1) {
                    baf.append((byte)current);
                }
                byte[] imageData = baf.toByteArray();
                return BitmapFactory.decodeByteArray(imageData, 0,
                        imageData.length);
            } catch (Exception exc) {
                return null;
            }
        }
 
       @Override
        protected void onPostExecute(Bitmap result) {
            mImage = new BitmapDrawable(result);
            if(mImage != null) {
                setImageDrawable(mImage);
            }
        }
    };
}

As you can see, WebImageView is a simple extension of the Android ImageView widget. The setPlaceholderImage() methods allow a local drawable to be set as the display image until the remote content is finished downloading. The bulk of the interesting work begins after the view has been given a remote URL using setImageUrl(), at which point the custom AsyncTask begins work.

Notice that an AsyncTask is strongly typed with three values for the input parameter, progress value, and result. In this case, a string is passed to the task’s execute() method and the background operation should return a Bitmap. The middle value, the progress, we are not using in this example, so it is set as Void. When extending AsyncTask, the only required method to implement is doInBackground(), which defines the chunk of work to be run on a background thread. In the previous example, this is where a connection is made to the remote URL provided, and the image is downloaded. Upon completion, we attempt to create a Bitmap from the downloaded data. If an error occurs at any point, the operation will abort and return null.

The other callback methods defined in AsyncTask, such as onPreExecute(), onPostExecute(), and onProgressUpdate(), are called on the main thread for the purposes of updating the user interface. In the previous example, onPostExecute() is used to update the view’s image with the result data.

Important  Android UI classes are not thread-safe. Be sure to use one of the callback methods that occur on the main thread to make any updates to the UI. Do not update views from within doInBackground().

Listings 4-10 and 4-11 show simple examples of using this class in an activity. Because this class is not part of the android.widget or android.view packages, we must write the fully qualified package name when using it in XML.

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.examples.WebImageView
        android:id="@+id/webImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>

Listing 4-11. Example Activity

public class WebImageActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        WebImageView imageView =
                (WebImageView) findViewById(R.id.webImage);
        imageView.setPlaceholderImage(R.drawable.icon);
        imageView.setImageUrl(
          "http://apress.com/resource/weblogo/Apress_120x90.gif");
    }
}

In this example, we first set a local image (the application icon) as the WebImageView placeholder. This image is displayed immediately to the user. We then tell the view to fetch an image of the Apress logo from the Web. As noted previously, this downloads the image in the background and, when it is complete, replaces the placeholder image in the view. It is this simplicity in creating background operations that has led the Android team to refer to AsyncTask as painless threading.

4-5. Downloading Completely in the Background

Problem

The application must download a large resource to the device, such as a movie file, that must not require the user to keep the application active.

Solution

(API Level 9)

Use the DownloadManager API. The DownloadManager is a service added to the SDK with API Level 9 that allows a long-running download to be handed off and managed completely by the system. The primary advantage of using this service is that DownloadManager will continue attempting to download the resource despite failures, connection changes, and even device reboots.

How It Works

Listing 4-12 is a sample activity that uses DownloadManager to handle the download of a large image file. When complete, the image is displayed in an ImageView. Whenever you utilize DownloadManager to access content from the Web, be sure to declare you are using the android.permission.INTERNET in the application’s manifest.

Listing 4-12. DownloadManager Sample Activity

public class DownloadActivity extends Activity {
 
    private static final String DL_ID = "downloadId";
    private SharedPreferences prefs;
 
    private DownloadManager dm;
    private ImageView imageView;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        imageView = new ImageView(this);
        setContentView(imageView);
 
        prefs =
            PreferenceManager.getDefaultSharedPreferences(this);
        dm = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
    }
 
    @Override
    public void onResume() {
        super.onResume();
 
        if(!prefs.contains(DL_ID)) {
            //Start the download
            Uri resource = Uri.parse(
                    "http://www.bigfoto.com/dog-animal.jpg");
            DownloadManager.Request request =
                    new DownloadManager.Request(resource);
            //Set allowed connections to process download
            request.setAllowedNetworkTypes(Request.NETWORK_MOBILE
                    | Request.NETWORK_WIFI);
            request.setAllowedOverRoaming(false);
 
            //Display in the notification bar
            request.setTitle("Download Sample");
            long id = dm.enqueue(request);
            //Save the unique id
            prefs.edit().putLong(DL_ID, id).commit();
        } else {
            //Download already started, check status
            queryDownloadStatus();
        }
 
        registerReceiver(receiver, new IntentFilter(
                DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    }
 
    @Override
    public void onPause() {
        super.onPause();
        unregisterReceiver(receiver);
    }
 
    private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            queryDownloadStatus();
        }
    };
 
    private void queryDownloadStatus() {
        DownloadManager.Query query = new DownloadManager.Query();
        query.setFilterById(prefs.getLong(DL_ID, 0));
        Cursor c = dm.query(query);
 
        if(c.moveToFirst()) {
            int status = c.getInt(
                c.getColumnIndex(DownloadManager.COLUMN_STATUS));
 
            switch(status) {
            case DownloadManager.STATUS_PAUSED:
            case DownloadManager.STATUS_PENDING:
            case DownloadManager.STATUS_RUNNING:
                //Do nothing, still in progress
                break;
            case DownloadManager.STATUS_SUCCESSFUL:
                //Done, display the image
                try {
                    ParcelFileDescriptor file =
                        dm.openDownloadedFile(
                            prefs.getLong(DL_ID, 0) );
                    FileInputStream fis = new ParcelFileDescriptor
                        .AutoCloseInputStream(file);
                    imageView.setImageBitmap(
                        BitmapFactory.decodeStream(fis));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
            case DownloadManager.STATUS_FAILED:
                //Clear the download and try again later
                dm.remove(prefs.getLong(DL_ID, 0));
                prefs.edit().clear().commit();
                break;
            }
        }
    }
 
}

Important  As of this book’s publishing date, there is a bug in the SDK that throws an Exception claiming android.permission.ACCESS_ALL_DOWNLOADS is required to use DownloadManager. This Exception is actually thrown when android.permission.INTERNET is not in your manifest.

This example does all of its useful work in the Activity.onResume() method so the application can determine the status of the download each time the user returns to the activity. Downloads within the manager can be references using a long ID value that is returned when DownloadManager.enqueue() is called. In the example, we persist that value in the application’s preferences in order to monitor and retrieve the downloaded content at any time.

Upon the first launch of the example application, a DownloadManager.Request object is created to represent the content to download. At a minimum, this request needs the Uri of the remote resource. However, there are many useful properties to set on the request as well to control its behavior. Some of the useful properties include the following:

  • Request.setAllowedNetworkTypes(): Set specific network types over which the download may be retrieved.
  • Request.setAllowedOverRoaming(): Set if the download is allowed to occur while the device is on a roaming connection.
  • Request.setDescription(): Set a description to be displayed in the system notification for the download.
  • Request.setTitle(): Set a title to be displayed in the system notification for the download.

Once an ID has been obtained, the application uses that value to check the status of the download. By registering a BroadcastReceiver to listen for the ACTION_DOWNLOAD_COMPLETE broadcast, the application will react to the download finishing by setting the image file on the activity’s ImageView. If the activity is paused while the download completes, upon the next resume the status will be checked and the ImageView content will be set.

It is important to note that ACTION_DOWNLOAD_COMPLETE is a broadcast sent by the DownloadManager for every download it may be managing. Because of this, we still must check that the download ID we are interested in is really ready.

Destinations

In Listing 4-12, we never told the DownloadManager where to place the file. Instead, when we wanted to access the file, we used the DownloadManager.openDownloadedFile() method with the ID value stored in preferences to get a ParcelFileDescriptor, which can be turned into a stream the application can read from. This is a simple and straightforward way to gain access to the downloaded content, but it has some caveats to be aware of.

Without a specific destination, files are downloaded to the shared download cache, where the system retains the right to delete them at any time to reclaim space. Downloading in this fashion is a convenient way to get data quickly, but if your needs for the download are more long-term, a permanent destination should be specified on external storage by using one of the DownloadManager.Request methods:

  • Request.setDestinationInExternalFilesDir(): Set the destination to a hidden directory on external storage.
  • Request.setDestinationInExternalPublicDir(): Set the destination to a public directory on external storage.
  • Request.setDestinationUri(): Set the destination to a file Uri located on external storage.

Note  All destination methods writing to external storage will require your application to declare use of android.permission.WRITE_EXTERNAL_STORAGE in the manifest.

Files without an explicit destination also often get removed when DownloadManager.remove() gets called to clear the entry from the manager list or the user clears the downloads list; files downloaded to external storage will not be removed by the system under these conditions.

4-6. Accessing a REST API

Problem

Your application needs to access a RESTful API over HTTP to interact with the web services of a remote host.

Note  REST stands for Representational State Transfer. It is a common architectural style for web services today. RESTful APIs are typically built using standard HTTP verbs to create requests of the remote resource, and the responses are typically returned in a structured document format, such as XML, JSON, or comma-separated values (CSV).

Solution

There are two recommended ways to use HTTP to send and receive data over a network connection in Android: the first is the Apache HttpClient, and the second is the Java HttpURLConnection. The decision about which to use in your application should be based primarily on what versions of Android you aim to support.

(API Level 3)

If you are targeting earlier Android versions, use the Apache HTTP classes inside an AsyncTask. Android includes the Apache HTTP components library, which provides a robust method of creating connections to remote APIs. The Apache library includes classes to create GET, POST, PUT, and DELETE requests with ease, as well as providing support for Secure Sockets Layer (SSL), cookie storage, authentication, and other HTTP requirements that your specific API may have in its HttpClient.

The other primary advantage of this approach is the level of abstraction provided by the Apache library. Applications require very little code  to do most network operations over HTTP. Much of the lower-level transaction code is hidden away from the developer.

One major disadvantage is that the version of the Apache components bundled with Android does not include MultipartEntity, a class that is necessary to do binary or multipart form data POST transactions. If you need this functionality and want to use HttpClient, you must pull in a newer version of the components library as an external JAR.

(API Level 9)

Use the Java HttpURLConnection class inside an AsyncTask. This class has been part of the Android framework since API Level 1 but has been the recommended method for network I/O only since the release of Android 2.3. The primary reason for this is that there were a few bugs in its implementation prior to that, which made HttpClient a more stable choice. However, moving forward, HttpURLConnection is where the Android team will continue to make performance and stability enhancements, so it is the recommended implementation choice.

The biggest advantage to using HttpURLConnection is performance. The classes are lightweight, and newer versions of Android have response compression and other enhancements built in. Its API is also lower level, so it is more ubiquitous, and implementing any type of HTTP transaction is possible. The drawback to this is that it requires more coding by the developer (but isn’t that why you bought this book?).

How It Works

HttpClient

Let’s look first at using HTTP with the Apache HttpClient. Listing 4-13 is an AsyncTask that can process any HttpUriRequest and return the string response.

Listing 4-13. AsyncTask Processing HttpRequest

public class RestTask extends
        AsyncTask<HttpUriRequest, Void, Object> {
    private static final String TAG = "RestTask";
 
    public interface ResponseCallback {
        public void onRequestSuccess(String response);
        public void onRequestError(Exception error);
    }
 
    private AbstractHttpClient mClient;
 
    private WeakReference<ResponseCallback> mCallback;
    
    public RestTask() {
        this(new DefaultHttpClient());
    }
 
    public RestTask(AbstractHttpClient client) {
        mClient = client;
    }
 
    public void setResponseCallback(ResponseCallback callback) {
        mCallback = new WeakReference<ResponseCallback>(callback);
    }
    
    @Override
    protected Object doInBackground(HttpUriRequest… params) {
        try{
            HttpUriRequest request = params[0];
            HttpResponse serverResponse =
                    mClient.execute(request);
 
            BasicResponseHandler handler =
                    new BasicResponseHandler();
            String response =
                    handler.handleResponse(serverResponse);
            return response;
        } catch (Exception e) {
            Log.w(TAG, e);
            return e;
        }
    }
 
    @Override
    protected void onPostExecute(Object result) {
        if (mCallback != null && mCallback.get() != null) {
            final ResponseCallback callback = mCallback.get();
            if (result instanceof String) {
                callback.onRequestSuccess((String) result);
            } else if (result instanceof Exception) {
                callback.onRequestError((Exception) result);
            } else {
                callback.onRequestError(new IOException(
                        "Unknown Error Contacting Host") );
            }
        }
    }
 
}

The RestTask can be constructed with or without an HttpClient parameter. The reason for allowing this is so multiple requests can use the same client object. This is extremely useful if your API requires cookies to maintain a session or if there is a specific set of required parameters that are easier to set up once (for example, SSL stores). The task takes an HttpUriRequest parameter to process (of which HttpGet, HttpPost, HttpPut, and HttpDelete are all subclasses) and executes it.

A BasicResponseHandler processes the response, which is a convenience class that abstracts our task from needing to check the response for errors. BasicResponseHandler will return the HTTP response as a string if the response code is 1XX or 2XX, but it will throw an HttpResponseException if the response code is 300 or greater.

The final important piece of this class exists in onPostExecute(), after the interaction with the API is complete.  RestTask has an optional callback interface that will be notified when the request is complete (with a string of the response data) or an error has occurred (with the exception that was triggered). This callback is stored in the form of a WeakReference so that we can safely use an activity or other system component as the callback, without worrying about a running task keeping that component from being removed if it gets paused or stopped. Now let’s use this powerful new tool to create some basic API requests.

GET Example

In the following example, we utilize the Google Custom Search REST API. This API takes a few parameters for each request:

  • key: Unique value to identify the application making the request
  • cx: Identifier for the custom search engine you want to access
  • q: String representing the search query you want to execute

Note  Visit https://developers.google.com/custom-search/ to receive more information about this API.

A GET request is the simplest and most common request in many public APIs. Parameters that must be sent with the request are encoded into the URL string itself, so no additional data must be provided. Let’s create a GET request to search for Android (see Listing 4-14).

Listing 4-14. Activity Executing API GET Request

public class SearchActivity extends Activity implements
        ResponseCallback {
 
    private static final String SEARCH_URI =
            "https://www.googleapis.com/customsearch/v1"
            + "?key=%s&cx=%s&q=%s";
    private static final String SEARCH_KEY =
            "AIzaSyBbW-W1SHCK4eW0kK74VGMLJj_b-byNzkI";
    private static final String SEARCH_CX =
            "008212991319514020231:1mkouq8yagw";
    private static final String SEARCH_QUERY = "Android";
    
    private TextView mResult;
    private ProgressDialog mProgress;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ScrollView scrollView = new ScrollView(this);
        mResult = new TextView(this);
        scrollView.addView(mResult, new ViewGroup.LayoutParams(
                LayoutParams.MATCH_PARENT,
                LayoutParams.WRAP_CONTENT) );
        setContentView(scrollView);
        
        try{
            //Simple GET
            String url = String.format(SEARCH_URI, SEARCH_KEY,
                    SEARCH_CX, SEARCH_QUERY);
            HttpGet searchRequest = new HttpGet(url);
 
            RestTask task = new RestTask();
            task.setResponseCallback(this);
            task.execute(searchRequest);
 
            //Display progress to the user
            mProgress = ProgressDialog.show(this, "Searching",
                    "Waiting For Results…", true);
        } catch (Exception e) {
            mResult.setText(e.getMessage());
        }
    }
 
    @Override
    public void onRequestSuccess(String response) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
 
        //Process the response data (here we just display it)
        mResult.setText(response);
    }
 
    @Override
    public void onRequestError(Exception error) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
 
        //Process the response data (here we just display it)
        mResult.setText(error.getMessage());
    }
}

In the example, we create the type of HTTP request that we need with the URL that we want to connect to (in this case, a GET request to googleapis.com). The URL is stored as a constant format string, and the required parameters for the Google API are added at runtime just before the request is created.

A RestTaskis created with the activity set as its callback, and the task is executed. When the task is complete, either onRequestSuccess() or onRequestError() will be called and, in the case of a success, the API response can be unpacked and processed. We will discuss parsing structured XML and JSON responses like this one in Recipes 4-7 and 4-8, so for now the example simply displays the raw response to the user interface.

POST Example

Many times, APIs require that you provide some data as part of the request, perhaps an authentication token or the contents of a search query. The API will require you to send the request over HTTP POST so these values may be encoded into the request body instead of the URL. To demonstrate a working POST, we will be sending a request to httpbin.org, which is a development site designed to read and validate the contents of a request and echo them back (see Listing 4-15).

Listing 4-15. Activity Executing API POST Request

public class SearchActivity extends Activity implements
        ResponseCallback {
 
    private static final String POST_URI =
            "http://httpbin.org/post";
 
    private TextView mResult;
    private ProgressDialog mProgress;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ScrollView scrollView = new ScrollView(this);
        mResult = new TextView(this);
        scrollView.addView(mResult, new ViewGroup.LayoutParams(
                LayoutParams.MATCH_PARENT,
                LayoutParams.WRAP_CONTENT));
        setContentView(scrollView);
        
        try{
            //Simple POST
            HttpPost postRequest =
                    new HttpPost( new URI(POST_URI) );
            List<NameValuePair> parameters =
                    new ArrayList<NameValuePair>();
            parameters.add(new BasicNameValuePair("title",
                    "Android Recipes") );
            parameters.add(new BasicNameValuePair("summary",
                    "Learn Android Quickly") );
            parameters.add(new BasicNameValuePair("author",
                    "Smith"));
            postRequest.setEntity(
                    new UrlEncodedFormEntity(parameters));
            
            RestTask task = new RestTask();
            task.setResponseCallback(this);
            task.execute(postRequest);
 
            //Display progress to the user
            mProgress = ProgressDialog.show(this, "Searching",
                    "Waiting For Results…", true);
        } catch (Exception e) {
            mResult.setText(e.getMessage());
        }
    }
 
    @Override
    public void onRequestSuccess(String response) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
 
        //Process the response data (here we just display it)
        mResult.setText(response);
    }
 
    @Override
    public void onRequestError(Exception error) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
 
        //Process the response data (here we just display it)
        mResult.setText(error.getMessage());
    }
}

Notice in this example that the parameters passed to the API are encoded into an HttpEntity instead of passed directly in the request URL. The request created in this case is an HttpPost instance, which is still a subclass of HttpUriRequest (like HttpGet), so we can use the same RestTask to run the operation. As with the GET example, we will discuss parsing structured XML and JSON responses like this one in Recipes 4-7 and 4-8, so for now the example simply displays the raw response to the user interface.

Reminder  The Apache library bundled with the Android SDK does not include support for multipart HTTP POSTs. However, MultipartEntity, from the publicly available org.apache.http.mime library, is compatible and can be brought in to your project as an external source.

Basic Authorization

Another common requirement for working with an API is some form of authentication. Standards are emerging for REST API authentication such as OAuth 2.0, but a common authentication method is still a basic username and password authorization over HTTP. In Listing 4-16, we modify the RestTask to enable authentication in the HTTP header per request.

Listing 4-16. RestTask with Basic Authorization

public class RestAuthTask extends
        AsyncTask<HttpUriRequest, Void, Object> {
    private static final String TAG = "RestTask";
 
    private static final String AUTH_USER = "[email protected]";
    private static final String AUTH_PASS = "password";
    
    public interface ResponseCallback {
        public void onRequestSuccess(String response);
 
        public void onRequestError(Exception error);
    }
 
    private AbstractHttpClient mClient;
    private WeakReference<ResponseCallback> mCallback;
    
    public RestAuthTask(boolean authenticate) {
        this(new DefaultHttpClient(), authenticate);
 
    }
 
    public RestAuthTask(AbstractHttpClient client,
            boolean authenticate) {
        mClient = client;
        if(authenticate) {
            UsernamePasswordCredentials creds =
                    new UsernamePasswordCredentials(
                            AUTH_USER, AUTH_PASS);
            mClient.getCredentialsProvider()
                    .setCredentials(AuthScope.ANY, creds);
        }
    }
 
    @Override
    protected Object doInBackground(HttpUriRequest… params) {
        try{
            HttpUriRequest request = params[0];
            HttpResponse serverResponse =
                    mClient.execute(request);
 
            BasicResponseHandler handler =
                    new BasicResponseHandler();
            String response =
                    handler.handleResponse(serverResponse);
            return response;
        } catch (Exception e) {
            Log.w(TAG, e);
            return e;
        }
    }
 
    @Override
    protected void onPostExecute(Object result) {
        if (mCallback != null && mCallback.get() != null) {
            final ResponseCallback callback = mCallback.get();
            if (result instanceof String) {
                callback.onRequestSuccess((String) result);
            } else if (result instanceof Exception) {
                callback.onRequestError((Exception) result);
            } else {
                callback.onRequestError(new IOException(
                        "Unknown Error Contacting Host") );
            }
        }
    }
 
}

Basic authentication is added to the HttpClient in the Apache paradigm. Because our example task allows for a specific client object to be passed in for use, which may already have the necessary authentication credentials, we have only modified the case where a default client is created. In this case, a UsernamePasswordCredentials instance is created with the username and password strings, and then set on the client’s CredentialsProvider.

HttpUrlConnection

Now let’s take a look at making HTTP requests with the preferred method for newer applications, HttpUrlConnection. We’ll start off by defining our same RestTask implementation in Listing 4-17, with a helper class in Listing 4-18.

Listing 4-17. RestTask Using HttpUrlConnection

public class RestTask extends AsyncTask<Void, Integer, Object> {
    private static final String TAG = "RestTask";
 
    public interface ResponseCallback {
        public void onRequestSuccess(String response);
 
        public void onRequestError(Exception error);
    }
 
    public interface ProgressCallback {
        public void onProgressUpdate(int progress);
    }
    
    private HttpURLConnection mConnection;
    private String mFormBody;
    private File mUploadFile;
    private String mUploadFileName;
 
    //Activity callbacks. Use WeakReferences to avoid blocking
    // operations causing linked objects to stay in memory
    private WeakReference<ResponseCallback> mResponseCallback;
    private WeakReference<ProgressCallback> mProgressCallback;
 
    public RestTask(HttpURLConnection connection) {
        mConnection = connection;
    }
 
    public void setFormBody(List<NameValuePair> formData) {
        if (formData == null) {
            mFormBody = null;
            return;
        }
        
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < formData.size(); i++) {
            NameValuePair item = formData.get(i);
            sb.append( URLEncoder.encode(item.getName()) );
            sb.append("=");
            sb.append( URLEncoder.encode(item.getValue()) );
            if (i != (formData.size() - 1)) {
                sb.append("&");
            }
        }
 
        mFormBody = sb.toString();
    }
 
    public void setUploadFile(File file, String fileName) {
        mUploadFile = file;
        mUploadFileName = fileName;
    }
 
    public void setResponseCallback(ResponseCallback callback) {
        mResponseCallback =
                new WeakReference<ResponseCallback>(callback);
    }
 
    public void setProgressCallback(ProgressCallback callback) {
        mProgressCallback =
                new WeakReference<ProgressCallback>(callback);
    }
 
    private void writeMultipart(String boundary,
            String charset,
            OutputStream output,
            boolean writeContent) throws IOException {
 
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(
                    new OutputStreamWriter(output,
                            Charset.forName(charset)), 8192);
            // Post Form Data Component
            if (mFormBody != null) {
                writer.write("--" + boundary);
                writer.write(" ");
                writer.write(
                        "Content-Disposition: form-data;"
                        + " name="parameters"");
                writer.write(" ");
                writer.write("Content-Type: text/plain; charset="
                        + charset);
                writer.write(" ");
                writer.write(" ");
                if (writeContent) {
                    writer.write(mFormBody);
                }
                writer.write(" ");
                writer.flush();
            }
 
            // Send binary file.
            writer.write("--" + boundary);
            writer.write(" ");
            writer.write("Content-Disposition: form-data; name=""
                    + mUploadFileName + ""; filename=""
                    + mUploadFile.getName() + """);
            writer.write(" ");
            writer.write("Content-Type: "
                    + URLConnection.guessContentTypeFromName(
                    mUploadFile.getName()));
            writer.write(" ");
            writer.write("Content-Transfer-Encoding: binary");
            writer.write(" ");
            writer.write(" ");
            writer.flush();
            if (writeContent) {
                InputStream input = null;
                try {
                    input = new FileInputStream(mUploadFile);
                    byte[] buffer = new byte[1024];
                    for (int length = 0;
                            (length = input.read(buffer)) > 0;) {
                        output.write(buffer, 0, length);
                    }
                    // Don't close the OutputStream yet
                    output.flush();
                } catch (IOException e) {
                    Log.w(TAG, e);
                } finally {
                    if (input != null) {
                        try {
                            input.close();
                        } catch (IOException e) {
                        }
                    }
                }
            }
            // This CRLF signifies the end of the binary chunk
            writer.write(" ");
            writer.flush();
 
            // End of multipart/form-data.
            writer.write("--" + boundary + "--");
            writer.write(" ");
            writer.flush();
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }
 
    private void writeFormData(String charset,
            OutputStream output) throws IOException {
        try {
            output.write(mFormBody.getBytes(charset));
            output.flush();
        } finally {
            if (output != null) {
                output.close();
            }
        }
    }
 
    @Override
    protected Object doInBackground(Void… params) {
        //Generate random string for boundary
        String boundary =
                Long.toHexString(System.currentTimeMillis());
        String charset = Charset.defaultCharset().displayName();
        
        try {
            // Set up output if applicable
            if (mUploadFile != null) {
                //We must do a multipart request
                mConnection.setRequestProperty("Content-Type",
                        "multipart/form-data; boundary="
                        + boundary);
 
                //Calculate the size of the extra metadata
                ByteArrayOutputStream bos =
                        new ByteArrayOutputStream();
                writeMultipart(boundary, charset, bos, false);
                byte[] extra = bos.toByteArray();
                int contentLength = extra.length;
                //Add the file size to the length
                contentLength += mUploadFile.length();
                //Add the form body, if it exists
                if (mFormBody != null) {
                    contentLength += mFormBody.length();
                }
 
                mConnection
                    .setFixedLengthStreamingMode(contentLength);
            } else if (mFormBody != null) {
                //In this case, it is just form data to post
                mConnection.setRequestProperty("Content-Type",
                    "application/x-www-form-urlencoded; charset="
                    + charset);
                mConnection.setFixedLengthStreamingMode(
                    mFormBody.length() );
            }
            
            //This is the first call on URLConnection that
            // actually does Network IO.  Even openConnection() is
            // still just doing local operations.
            mConnection.connect();
 
            // Do output if applicable (for a POST)
            if (mUploadFile != null) {
                OutputStream out = mConnection.getOutputStream();
                writeMultipart(boundary, charset, out, true);
            } else if (mFormBody != null) {
                OutputStream out = mConnection.getOutputStream();
                writeFormData(charset, out);
            }
 
            // Get response data
            int status = mConnection.getResponseCode();
            if (status >= 300) {
                String message = mConnection.getResponseMessage();
                return new HttpResponseException(status, message);
            }
 
            InputStream in = mConnection.getInputStream();
            String encoding = mConnection.getContentEncoding();
            int contentLength = mConnection.getContentLength();
            if (encoding == null) {
                encoding = "UTF-8";
            }
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(in, encoding));
            char[] buffer = new char[4096];
 
            StringBuilder sb = new StringBuilder();
            int downloadedBytes = 0;
            int len1 = 0;
            while ((len1 = reader.read(buffer)) > 0) {
                downloadedBytes += len1;
                publishProgress(
                        (downloadedBytes * 100) / contentLength);
                sb.append(buffer);
            }
            
            return sb.toString();
        } catch (Exception e) {
            Log.w(TAG, e);
            return e;
        } finally {
            if (mConnection != null) {
                mConnection.disconnect();
            }
        }
    }
 
    @Override
    protected void onProgressUpdate(Integer ... values) {
        // Update progress UI
        if (mProgressCallback != null
                && mProgressCallback.get() != null) {
            mProgressCallback.get().onProgressUpdate(values[0]);
        }
    }
 
    @Override
    protected void onPostExecute(Object result) {
        if (mResponseCallback != null
                && mResponseCallback.get() != null) {
            final ResponseCallback cb = mResponseCallback.get();
            if (result instanceof String) {
                cb.onRequestSuccess((String) result);
            } else if (result instanceof Exception) {
                cb.onRequestError((Exception) result);
            } else {
                cb.onRequestError(new IOException(
                        "Unknown Error Contacting Host"));
            }
        }
    }
}

Listing 4-18. Util Class to Create Requests

public class RestUtil {
    
    public static final RestTask obtainGetTask(String url)
            throws MalformedURLException, IOException {
        HttpURLConnection connection =
                (HttpURLConnection) (new URL(url))
                .openConnection();
 
        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoInput(true);
 
        RestTask task = new RestTask(connection);
        return task;
    }
 
    public static final RestTask obtainFormPostTask(String url,
            List<NameValuePair> formData)
            throws MalformedURLException, IOException {
        HttpURLConnection connection =
                (HttpURLConnection) (new URL(url))
                .openConnection();
 
        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoOutput(true);
 
        RestTask task = new RestTask(connection);
        task.setFormBody(formData);
 
        return task;
    }
 
    public static final RestTask obtainMultipartPostTask(
            String url, List<NameValuePair> formPart,
            File file, String fileName)
            throws MalformedURLException, IOException {
        HttpURLConnection connection =
                (HttpURLConnection) (new URL(url))
                .openConnection();
 
        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoOutput(true);
 
        RestTask task = new RestTask(connection);
        task.setFormBody(formPart);
        task.setUploadFile(file, fileName);
 
        return task;
    }
}

The first thing you probably noticed is that this example requires a lot more code to implement certain requests, due to the low-level nature of the API. We have written a RestTask that is capable of handling GET, simple POST, and multipart POST requests, and we define the parameters of the request dynamically based on the components added to RestTask.

As before, we can attach an optional callback to be notified when the request has completed. However, in addition to that, we have added a progress callback interface that the task will call to update any visible UI of the progress while downloading response content. This is simpler to implement using HttpUrlConnection, rather than HttpClient, because we are interacting directly with the data streams.

In this example, an application would create an instance of RestTask through the RestUtil helper class. This subdivides the setup required on HttpURLConnection, which doesn’t actually do any network I/O from the portions that connect and interact with the host. The helper class creates the connection instance and also sets up any time-out values and the HTTP request method.

Note  By default, any URLConnection will have its request method set to GET. Calling setDoOutput() implicitly sets that method to POST. If you need to set that value to any other HTTP verb, use setRequestMethod().

If there is any body content, in the case of a POST, those values are set directly on our custom task to be written when the task executes.

Once a RestTask is executed, it goes through and determines whether there is any body data attached that it needs to write. If we have attached form data (as name-value pairs) or a file for upload, it takes that as a trigger to construct a POST body and send it. With HttpURLConnection, we are responsible for all aspects of the connection, including telling the server the amount of data that is coming. Therefore, RestTask takes the time to calculate how much data will be posted and calls setFixedLengthStreamingMode() to construct a header field telling the server how large our content is. In the case of a simple form post, this calculation is trivial, and we just pass the length of the body string.

A multipart POST that may include file data is more complex, however. Multipart has lots of extra data in the body to designate the boundaries between each part of the POST, and all those bytes must be accounted for in the length we set. In order to accomplish this, writeMultipart() is constructed in such a way that we can pass a local OutputStream (in this case, a ByteArrayOutputStream) to write all the extra data into it so we can measure it. When the method is called in this way, it skips over the actual content pieces, such as the file and form data, as those can be added in later by calling their respective length() methods, and we don’t want to waste time loading them into memory.

Note  If you do not know how big the content is that you want to POST, HttpURLConnection also supports chunked uploads via setChunkedStreamingMode(). In this case, you need only to pass the size of the data chunks you will be sending.

Once the task has written any POST data to the host, it is time to read the response content. If the initial request was a GET request, the task skips directly to this step because there was no additional data to write. The task first checks the value of the response code to make sure there were no server-side errors, and it then downloads the contents of the response into a StringBuilder. The download reads in chunks of data roughly 4KB at a time, notifying the progress callback handler with a percentage downloaded as a fraction of the total response content length. When all the content is downloaded, the task completes by handing back the resulting response as a string.

GET Example

Let’s take a look at our same Google Custom Search example, but this time let’s use the new and improved RestTask (see Listing 4-19).

Listing 4-19. Activity Executing API GET Request

public class SearchActivity extends Activity implements
        RestTask.ProgressCallback, RestTask.ResponseCallback {
 
    private static final String SEARCH_URI =
            "https://www.googleapis.com/customsearch/v1"
            + "?key=%s&cx=%s&q=%s";
    private static final String SEARCH_KEY =
            "AIzaSyBbW-W1SHCK4eW0kK74VGMLJj_b-byNzkI";
    private static final String SEARCH_CX =
            "008212991319514020231:1mkouq8yagw";
    private static final String SEARCH_QUERY = "Android";
 
    private TextView mResult;
    private ProgressDialog mProgress;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ScrollView scrollView = new ScrollView(this);
        mResult = new TextView(this);
        scrollView.addView(mResult, new ViewGroup.LayoutParams(
                LayoutParams.MATCH_PARENT,
                LayoutParams.WRAP_CONTENT));
        setContentView(scrollView);
        
        //Create the request
        try{
            //Simple GET
            String url = String.format(SEARCH_URI, SEARCH_KEY,
                    SEARCH_CX, SEARCH_QUERY);
            RestTask getTask = RestUtil.obtainGetTask(url);
            getTask.setResponseCallback(this);
            getTask.setProgressCallback(this);
            
            getTask.execute();
            
            //Display progress to the user
            mProgress = ProgressDialog.show(this, "Searching",
                    "Waiting For Results…", true);
        } catch (Exception e) {
            mResult.setText(e.getMessage());
        }
    }
    
    @Override
    public void onProgressUpdate(int progress) {
        if (progress >= 0) {
            if (mProgress != null) {
                mProgress.dismiss();
                mProgress = null;
            }
            //Update user of progress
            mResult.setText( String.format(
                    "Download Progress: %d%%", progress));
        }
    }
 
    @Override
    public void onRequestSuccess(String response) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //Process the response data (here we just display it)
        mResult.setText(response);
    }
 
    @Override
    public void onRequestError(Exception error) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //Process the response data (here we just display it)
        mResult.setText("An Error Occurred: "+error.getMessage());
    }
}

The example is almost identical to our previous iteration. We still construct the URL out of the necessary query parameters and obtain a RestTask instance. We then set this activity as the callback for the request and execute.

You can see, however, that we have added ProgressCallback to the list of interfaces this activity implements so it can be notified of how the download is going. Not all web servers return a valid content length for requests, instead returning –1, which makes progress based on the percentage difficult to do. In those cases, our callback simply leaves the indeterminate progress dialog box visible until the download is complete. However, in cases where valid progress can be determined, the dialog box is dismissed and the percentage of progress is displayed on the screen.

Once the download is complete, the activity receives a callback with the resulting JSON string. We will discuss parsing structured XML and JSON responses like this one in Recipes 4-7 and 4-8, so for now the example simply displays the raw response to the user interface.

POST Example

Listing 4-20 illustrates doing a simple form data POST using the new RestTask. The endpoint will be httpbin.org once again, so the resulting data displayed on the screen will be an echo back to the form parameters we passed in.

Listing 4-20. Activity Executing API POST Request

public class SearchActivity extends Activity implements
        RestTask.ProgressCallback, RestTask.ResponseCallback {
 
    private static final String POST_URI =
            "http://httpbin.org/post";
 
    private TextView mResult;
    private ProgressDialog mProgress;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ScrollView scrollView = new ScrollView(this);
        mResult = new TextView(this);
        scrollView.addView(mResult, new ViewGroup.LayoutParams(
                LayoutParams.MATCH_PARENT,
                LayoutParams.WRAP_CONTENT));
        setContentView(scrollView);
        
        //Create the request
        try{
            //Simple POST
            List<NameValuePair> parameters =
                    new ArrayList<NameValuePair>();
            parameters.add(new BasicNameValuePair("title",
                    "Android Recipes"));
            parameters.add(new BasicNameValuePair("summary",
                    "Learn Android Quickly"));
            parameters.add(new BasicNameValuePair("author",
                    "Smith"));
            RestTask postTask = RestUtil.obtainFormPostTask(
                    POST_URI, parameters);
            postTask.setResponseCallback(this);
            postTask.setProgressCallback(this);
 
            postTask.execute();
            
            //Display progress to the user
            mProgress = ProgressDialog.show(this, "Searching",
                    "Waiting For Results…", true);
        } catch (Exception e) {
            mResult.setText(e.getMessage());
        }
    }
    
    @Override
    public void onProgressUpdate(int progress) {
        if (progress >= 0) {
            if (mProgress != null) {
                mProgress.dismiss();
                mProgress = null;
            }
            //Update user of progress
            mResult.setText( String.format(
                    "Download Progress: %d%%", progress));
        }
    }
 
    @Override
    public void onRequestSuccess(String response) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //Process the response data (here we just display it)
        mResult.setText(response);
    }
 
    @Override
    public void onRequestError(Exception error) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //Process the response data (here we just display it)
        mResult.setText("An Error Occurred: "+error.getMessage());
    }
}

It should be noted here that the progress callbacks are related only to the download of the response, and not the upload of the POST data, though that is certainly possible for the developer to implement.

Upload Example

Listing 4-21 illustrates something we cannot do natively with the Apache components in the Android framework: multipart POST.

Listing 4-21. Activity Executing API Multipart POST Request

public class SearchActivity extends Activity implements
        RestTask.ProgressCallback, RestTask.ResponseCallback {
 
    private static final String POST_URI =
            "http://httpbin.org/post";
 
    private TextView mResult;
    private ProgressDialog mProgress;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ScrollView scrollView = new ScrollView(this);
        mResult = new TextView(this);
        scrollView.addView(mResult, new ViewGroup.LayoutParams(
                LayoutParams.MATCH_PARENT,
                LayoutParams.WRAP_CONTENT));
        setContentView(scrollView);
        
        //Create the request
        try{
            //File POST
            Bitmap image = BitmapFactory.decodeResource(
                    getResources(),
                    R.drawable.ic_launcher);
            File imageFile = new File(
                    getExternalCacheDir(), "myImage.png");
            FileOutputStream out =
                    new FileOutputStream(imageFile);
            image.compress(CompressFormat.PNG, 0, out);
            out.flush();
            out.close();
            List<NameValuePair> fileParameters =
                    new ArrayList<NameValuePair>();
            fileParameters.add(new BasicNameValuePair("title",
                    "Android Recipes"));
            fileParameters.add(new BasicNameValuePair("desc",
                    "Image File Upload"));
            RestTask uploadTask =
                    RestUtil.obtainMultipartPostTask(
                            POST_URI, fileParameters,
                            imageFile, "avatarImage");
            uploadTask.setResponseCallback(this);
            uploadTask.setProgressCallback(this);
            
            uploadTask.execute();
            
            //Display progress to the user
            mProgress = ProgressDialog.show(this, "Searching",
                    "Waiting For Results…", true);
        } catch (Exception e) {
            mResult.setText(e.getMessage());
        }
    }
    
    @Override
    public void onProgressUpdate(int progress) {
        if (progress >= 0) {
            if (mProgress != null) {
                mProgress.dismiss();
                mProgress = null;
            }
            //Update user of progress
            mResult.setText( String.format(
                    "Download Progress: %d%%", progress));
        }
    }
 
    @Override
    public void onRequestSuccess(String response) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //Process the response data (here we just display it)
        mResult.setText(response);
    }
 
    @Override
    public void onRequestError(Exception error) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //Process the response data (here we just display it)
        mResult.setText("An Error Occurred: "+error.getMessage());
    }
}

In this example, we construct a POST request that has two distinct parts: a form data part (made up of name-value pairs) and a file part. For the purposes of the example, we take the application’s icon and quickly write it out to external storage as a PNG file to use for the upload.

In this case, the JSON response from httpbin will echo back both the form data elements as well as a Base64-encoded representation of the PNG image.

Basic Authorization

Adding basic authorization to the new RestTask is fairly straightforward. It can be done in one of two ways: either directly on each request or globally using a class called Authenticator. First let’s take a look at attaching basic authorization to an individual request. Listing 4-22 modifies RestUtil to include methods that attach a username and password in the proper format.

Listing 4-22. RestUtil with Basic Authorization

public class RestUtil {
    
    public static final RestTask obtainGetTask(String url)
            throws MalformedURLException, IOException {
        HttpURLConnection connection =
                (HttpURLConnection) (new URL(url))
                .openConnection();
 
        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoInput(true);
 
        RestTask task = new RestTask(connection);
        return task;
    }
 
    public static final RestTask obtainAuthenticatedGetTask(
            String url, String username, String password)
            throws MalformedURLException, IOException {
        HttpURLConnection connection =
                (HttpURLConnection) (new URL(url))
                .openConnection();
 
        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoInput(true);
        
        attachBasicAuthentication(connection, username, password);
        
        RestTask task = new RestTask(connection);
        return task;
    }
 
    public static final RestTask obtainAuthenticatedFormPostTask(
            String url, List<NameValuePair> formData,
            String username, String password)
            throws MalformedURLException, IOException {
        HttpURLConnection connection =
                (HttpURLConnection) (new URL(url))
                .openConnection();
 
        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoOutput(true);
        
        attachBasicAuthentication(connection, username, password);
 
        RestTask task = new RestTask(connection);
        task.setFormBody(formData);
 
        return task;
    }
 
    private static void attachBasicAuthentication(
            URLConnection connection,
            String username, String password) {
        //Add Basic Authentication Headers
        String userpassword = username + ":" + password;
        String encodedAuthorization = Base64.encodeToString(
                userpassword.getBytes(), Base64.NO_WRAP);
        connection.setRequestProperty("Authorization", "Basic "
                + encodedAuthorization);
    }
 
}

Basic authorization is added to an HTTP request as a header field with the name Authorization and the value of Basic followed by a Base64-encoded string of your username and password. The helper method attachBasicAuthentication() applies this property to the URLConnection before it is given to RestTask. The Base64.NO_WRAP flag is added to ensure that the encoder doesn’t add any extra new lines, which will create an invalid value.

This is a really nice way of applying authentication to requests if not all your requests need to be authenticated in the same way. However, sometimes it’s easier to just set your credentials once and let all your requests use them. This is where Authenticator comes in. Authenticator allows you to globally set the username and password credentials for the requests in your application process. Let’s take a look at Listing 4-23, which shows how this can be done.

Listing 4-23. Activity Using Authenticator

public class AuthActivity extends Activity implements
        ResponseCallback {
 
    private static final String URI =
            "http://httpbin.org/basic-auth/android/recipes";
    private static final String USERNAME = "android";
    private static final String PASSWORD = "recipes";
    
    private TextView mResult;
    private ProgressDialog mProgress;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mResult = new TextView(this);
        setContentView(mResult);
        
        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication
                    getPasswordAuthentication() {
                return new PasswordAuthentication(USERNAME,
                        PASSWORD.toCharArray());
            }
        });
        
        try {
            RestTask task = RestUtil.obtainGetTask(URI);
            task.setResponseCallback(this);
            task.execute();
        } catch (Exception e) {
            mResult.setText(e.getMessage());
        }
    }
    
    @Override
    public void onRequestSuccess(String response) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        mResult.setText(response);
    }
 
    @Override
    public void onRequestError(Exception error) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        mResult.setText(error.getMessage());
    }
 
}

This example connects to httpbin again, this time to an endpoint used to validate credentials. The username and password the host will require are coded into the URL path, and if those credentials are not properly supplied, the response from the host will be UNAUTHORIZED.

With a single call to Authenticator.setDefault(), passing in a new Authenticator instance, all subsequent requests will use the provided credentials for authentication challenges. So we pass the correct username and password to Authenticator by creating a new PasswordAuthentication instance whenever asked, and all URLConnection instances in our process will make use of that. Notice that in this example, our request does not have credentials attached to it, but when the request is made, we will get an authenticated response.

Caching Responses

(API Level 13)

One final platform enhancement you can take advantage of when you use HttpURLConnection is response caching with HttpResponseCache. A great way to speed up the response of your application is to cache responses coming back from the remote host so your application can load frequent requests from the cache rather than hitting the network each time. Installing and removing a cache in your application requires just a few simple lines of code:

//Installing a response cache
try {
    File httpCacheDir = new File(context.getCacheDir(), "http");
    long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
    HttpResponseCache.install(httpCacheDir, httpCacheSize);
catch (IOException e) {
    Log.i(TAG, "HTTP response cache installation failed:" + e);
}
 
//Clearing a response cache
HttpResponseCache cache = HttpResponseCache.getInstalled();
if (cache != null) {
    cache.flush();
}

Note  HttpResponseCache works with only HttpURLConnection variants. It will not work if you are using Apache HttpClient.

4-7. Parsing JSON

Problem

Your application needs to parse responses from an API or other source that is formatted in JavaScript Object Notation (JSON).

Solution

(API Level 1)

Use the org.json parser classes that are baked into Android. The SDK comes with a very efficient set of classes for parsing JSON-formatted strings in the org.json package. Simply create a new JSONObject or JSONArray from the formatted string data and you’ll be armed with a set of accessor methods to get primitive data or nested JSONObjects and JSONArrays from within.

How It Works

This JSON parser is strict by default, meaning that it will halt with an exception when encountering invalid JSON data or an invalid key. Accessor methods that prefix with get will throw a JSONException if the requested value is not found. In some cases this behavior is not ideal, and for that there is a companion set of methods that are prefixed with opt. These methods will return null instead of throwing an exception when a value for the requested key is not found. In addition, many of them have an overloaded version that also takes a fallback parameter to return instead of null.

Let’s look at an example of how to parse a JSON string into useful pieces. Consider the JSON in Listing 4-24.

Listing 4-24. Example JSON

{
    "person": {
        "name": "John",
        "age": 30,
        "children": [
            {
                "name": "Billy"
                "age": 5
            },
            {
                "name": "Sarah"
                "age": 7
            },
            {
                "name": "Tommy"
                "age": 9
            }
        ]
    }
}

This defines a single object with three values: name (string), age (integer), and children. The parameter entitled children is an array of three more objects, each with its own name and age. If we were to use org.json to parse this data and display some elements in TextViews, it would look like the examples in Listings 4-25 and 4-26.

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
        android:id="@+id/line1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/line2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/line3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

Listing 4-26. Sample JSON Parsing Activity

public class MyActivity extends Activity {
    private static final String JSON_STRING =
            "{"person":"
            + "{"name":"John","age":30,"children":["
            + "{"name":"Billy","age":5},"
            + "{"name":"Sarah","age":7},"
            + "{"name":"Tommy","age":9}"
            + "] } }";
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        TextView line1 = (TextView)findViewById(R.id.line1);
        TextView line2 = (TextView)findViewById(R.id.line2);
        TextView line3 = (TextView)findViewById(R.id.line3);
        try {
            JSONObject person = (new JSONObject(JSON_STRING))
                    .getJSONObject("person");
            String name = person.getString("name");
            line1.setText("This person's name is " + name);
            line2.setText(name + " is " + person.getInt("age")
                    + " years old.");
            line3.setText(name + " has "
                    + person.getJSONArray("children").length()
                    + " children.");
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}

For this example, the JSON string has been hard-coded as a constant. When the activity is created, the string is turned into a JSONObject, at which point all its data can be accessed as key-value pairs, just as if it were stored in a map or dictionary. All the business logic is wrapped in a try block because we are using the strict methods for accessing data.

Functions such as JSONObject.getString() and JSONObject.getInt() are used to read primitive data out and place it in the TextView; the getJSONArray() method pulls out the nested children array. JSONArray has the same set of accessor methods as JSONObject to read data, but they take an index into the array as a parameter instead of the name of the key. In addition, a JSONArray can return its length, which we used in the example to display the number of children the person had.

The result of the sample application is shown in Figure 4-3.

9781430263227_Fig04-03.jpg

Figure 4-3. Display of parsed JSON data in the Activity

Debugging Trick

JSON is a very efficient notation; however, it can be difficult for humans to read a raw JSON string, which can make it hard to debug parsing issues. Quite often the JSON you are parsing is coming from a remote source or is not completely familiar to you, and you need to display it for debugging purposes. Both JSONObject and JSONArray have an overloaded toString() method that takes an integer parameter for pretty-printing the data in a returned and indented fashion, making it easier to decipher. Often adding something like myJsonObject.toString(2) to a troublesome section can save you time and a headache.

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

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