Wednesday, April 23, 2014

Android Networking Example and Tutorial (with easy retries)

This post explains how to use the WorxForUs Network framework to have an Android app that works robustly even in areas that have poor network connectivity. 

Background:

I wrote this library because none of the tutorials I found went clearly into the different types of faults that you can experience when developing web apps.  They worked fine to get you started, but what about if you are now on a mobile network and not all of your packets are getting through?  What happens if you socket times out?  What do I do if I need to handle cookies?  This framework is an attempt to address those issues along with adding a simple way to retry communications that did not go through properly.

Update:

A sample project written using Eclipse is available on GitHub.  It shows the most basic usage of the WorxForUs Android network framework.

Features:

  • Automatic cookie handling
  • Baked in network retries (just specify number of retries to allow before failing)
  • Easy to understand handling of the various types of network errors
  • Handles authenticated and unauthenticated HTTP requests

Accessing the Network with Retries (without authentication):

First, download the WorxForUs framework from the github page (or clone here).
Import the project into your Eclipse or Android Studio.

This code all needs to be wrapped in a Thread or AsyncTask, otherwise you can expect to get either an app not responding error or an android.os.NetworkOnMainThreadException, because network tasks are long running and should not be run on the main thread.

Check to see if the network is connected (you don't want to try and download something if the user has activated flight mode).

    //NetResult is an object to store the results
    NetResult netResult = null; 
    String url = "http://www.google.com/";
    //Load the values being posted
    List<NameValuePair> params = new ArrayList<NameValuePair>();    params.add(new BasicNameValuePair("q", "bears"));

    String serverResponse ="";
    //If the network is not currently connected, don't try to talk
    if (NetHandler.isNetworkConnected(con)) {
        netResult = NetHandler.handlePostWithRetry(url, params, NetHandler.NETWORK_DEFAULT_RETRY_ATTEMPTS);

        //get the server response as a string
        serverResponse = Utils.removeUTF8BOM(EntityUtils.toString(net_result.net_response_entity, Utils.CHARSET));
        //Notify the HTTP client that all data was read
        netResult.closeNetResult();
    }
What is this NetResult object ?  This object contains all the information you need to decode the response from the webserver. If netResult.net_response_entity is not null, then that is the response from the server.  Send that value to your handling routing.

    NetResult.net_success - Equals true when the server was successfully contacted (this says nothing about if your request was valid though).
    NetResult.net_error - Contains the error message or exception associated with the connection
    NetResult.net_error_type - Contains the name of the type of error that occurred (ie. HttpResponseException, SocketTimeoutException, SocketException, or IOException)
    NetResult.net_response_entity - This is the actual response from the server.  A common use is to run:
String consume_str = Utils.removeUTF8BOM(EntityUtils.toString(net_result.net_response_entity, Utils.CHARSET)); 
or if you want to capture JSON data
NetResult.handleGenericJsonResponseHelper(net_result, this.getClass().getName()); 
Note: the class name is passed for logging purposes only
Now net_result.object will contain your JSON objects parsed for you

After reading your data from the netResult.net_response_entity, you will need to call netResult.closeNetResult().  This function eventually calls HttpEntity.consumeContent() which releases any resources associated with that object.


Accessing the Network with Authentication:

 To call the network with authentication is simple once you have the authentication helper configured for your particular webserver.  Alternatively, you can also just load the login parameters to most websites by putting the correct variables in POST parameters.





// load authentication data
if (!AuthNetHandler.isAuthenticationSet()) {
    // passing the context here allows the system to update the preferences with a validated usernumber (if found)
    AuthNetHandler.setAuthentication(host, new MyAuthenticationHelper(con));
    NetAuthentication.loadUsernamePassword(username, password);
}
// if network is ready, then continue
// check if network was disabled by the user

if (NetHandler.isNetworkConnected(context)) {
    // if user has credentials - upload then download so data is not lost
    if (NetAuthentication.isReadyForLogin()) {
        netResult = AuthNetHandler.handleAuthPostWithRetry(url, params, num_retries);
        //...handle the netResult response here
        netResult.closeNetResult();
    } else {
        Log.e(this.getClass().getName(), "Did not attempt to login, no authentication info");
    }
}


When you have authenticated requests there are a few extra steps before you call  AuthNetHandler.handleAuthPostWithRetry(...).

First you will need to implement a class from NetAuthenticationHelper.  This will tell the framework how you are going to login, what messages are errors,  and basically how you are validating a login.   It may seem like a lot of extra code, but try to stick with it.  Having all your authentication code in one place can be extremely helpful.

public class MyAuthHelper implements NetAuthenticationHelper {
    @Override
    public String getLoginURL(String host) {
        return "https://myweb/remote_login_address.php";
    }


getLoginURL is simple the location where you expect the app to connect to when sending login parameters.  Just put the URL you need in here.

    @Override
    public void markAsLoginFailure(NetResult result) {

        result.object = new String("Login Error");
    }


or, in my case I use JSON objects

    @Override
    public void markAsLoginFailure(NetResult result) {
        try {
            result.object = new JSONObjectWrapper("Jsonstring");
        } catch (JSONExceptionWrapper e) {
            throw new RuntimeException("Could not parse default login failed JSON string.");
        }
    }

Put whatever your webserver responds with in here on a failed user login.  This section forces a result to be simulated as a login failure in the result.object variable.  Let's say you've identified a failed login, this output gets sent through validateLoginResponse(...) where the failed login will be identified.

    @Override
    public void markAsLoginSuccessFromCache(NetResult result) {

        result.object = new String("Login Successful");
    }


Put whatever your webserver could respond with on a successful login.  This section forces a result to be simulated as a login success in the result.object variable.

    @Override
    public String getLoginErrorMessage() { return "Could not login user"; }


Here put what you would like be passed as the error message to netResult.net_error when a user is not able to be logged in.

    @Override
    public int validateLoginResponse(NetResult netResult) {
        //Returns NetAuthentication.NO_ERRORS, NETWORK_ERROR, or SERVER_ERROR
        //Check the login server result from netResult.object
        //to determine if your login was successful or not
        if (netResult.net_success) {
            String response = netResult.object;
            if (response.contains("not logged in indication")) {
                return NetAuthentication.NOT_LOGGED_IN;
            } else if (response.contains("login error indication"))) {
                return NetAuthentication.LOGIN_FAILURE;
            } else if (response.contains("cant parse data indication"))) {
                return NetAuthentication.SERVER_ERROR;
            }
        } else {
            return NetAuthentication.NETWORK_ERROR;
        }
        return NetAuthentication.NO_ERRORS;    
    }

validateLoginResponse(...)performs the bulk of the login checking, it determines if the user wasn't logged in, there was a login error (ie. wrong password), a server error, or network error.  Depending on what you expect from the server, you will send a response of NetAuthentication.NO_ERRORS, NETWORK_ERROR, or SERVER_ERROR.

    @Override
    public int peekForNotLoggedInError(NetResult netResult) {

        //... check the netResult.object for a login failure on a page that wasn't the login page.
    }


Similarly to the previous function, the peekForNotLoggedInError(...) checks for login errors, but on pages that are not the login page.  Consider the example where you have already logged in, but then check a different page to download some other data.  If your user's session is suddenly logged out, you will get an error that could look different than the one you get on the login page.  So that specific logic for unexpected login failure goes in here. 

    @Override
    public NetResult handleUsernameLogin(String host, String username, String password) {
        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("uname_field", "username"));
        params.add(new BasicNameValuePair("pword_field", "password"));
       
        NetResult netResult = NetHandler.handlePostWithRetry(this.getLoginURL(host), params , NetHandler.NETWORK_DEFAULT_RETRY_ATTEMPTS);
        //save the result and close network stream
        consume_str = Utils.removeUTF8BOM(EntityUtils.toString(result.net_response_entity, Utils.CHARSET));
        netResult.object = consume_str;
        netResult.closeNetResult();
        return netResult;
    }


The handleUsernameLogin(...) function provides the actual fields and logic needed to send the request to the webserver.  Simply fill in your specific fields for login.

If you have a different request using a token, the handleTokenLogin(...) function can be used for that purpose.

Wow, if you've made it to the end of this tutorial, you are a real trooper and I salute you!

Notes:

HTTP Params are encoded with UTF-8.  If your webserver expects another character set, you will need to change the handlePost(...) routing in NetHandler to use that encoding scheme.

Permissions: Obviously you will need to have the correct permissions in your app or you will get a permission exception.  These must be in your manifest file:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>