RESTful API requests using Java for Android

HTTP request Java Android

In a recent article we showed how HTTP requests are formed in low level. We demonstrated how to create a simple HTTP GET request, a URL encoded POST request, and an upload request (multipart POST HTTP request).

This article has working examples on how to create those HTTP requests programmatically using Java for Android.

The next sections of this article include the code that does the job, and then a couple of examples that demonstrate ways to create different types of HTTP requests.

The code

First, we need to add the Internet permission in the project's AndroidManifest.xml file:

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

Here is the code you can pass on to your activity:

package com.example.lowlevelhttprequest;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import org.apache.http.client.ClientProtocolException;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;


public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private enum HttpRequestVarLayout {NOT_SET, ADDRESS, URL_ENCODED, MULTIPART}

    private class HttpRequestInputFileElement {

        private String variable_name;
        private String local_filename;
        private String request_filename;
        private String mime_type;

    }

    private class HttpRequestInput {

        private String url_str;
        private String http_method;
        private HttpRequestVarLayout var_layout = HttpRequestVarLayout.NOT_SET;
        private HashMap<String, String> vars;
        private ArrayList<HttpRequestInputFileElement> files;

        HttpRequestInput() {
            this.url_str = "";
            this.http_method = "GET";
            this.vars = new HashMap<String, String>();
            this.files = new ArrayList<HttpRequestInputFileElement>();
        }

        HttpRequestInput(String url_str, String http_method) {
            this();
            this.url_str = url_str;
            this.http_method = http_method;
        }

        private void add_var(String key, String value) {
            this.vars.put(key, value);
        }

        private void add_file(String variable_name, String local_filename, String request_filename, String mime_type) {
            HttpRequestInputFileElement file = new HttpRequestInputFileElement();
            file.variable_name = variable_name;
            file.local_filename = local_filename;
            file.request_filename = request_filename;
            file.mime_type = mime_type;
            this.files.add(file);
        }

    }

    private class HttpRequestWorker extends AsyncTask<HttpRequestInput, Void, Object> {

        private Charset utf8 = Charset.forName("UTF-8");

        public String http_attribute_encode(String attribute_name, String input) {
            // result structure follows RFC 5987
            boolean need_utf_encoding = false;
            StringBuilder result = new StringBuilder();
            byte[] input_c = input.getBytes(utf8);
            char c;
            for (int i = 0; i < input_c.length; i++) {
                c = (char) input_c[i];
                if (c == '\\' || c == '/' || c == '\0' || c < ' ' || c > '~') {
                    // ignore and request utf-8 version
                    need_utf_encoding = true;
                }
                else if (c == '"') {
                    result.append("\\\"");
                }
                else {
                    result.append(c);
                }
            }

            if (result.length() == 0) {
                need_utf_encoding = true;
            }

            if (!need_utf_encoding) {
                // return simple version
                return String.format("%s=\"%s\"", attribute_name, result.toString());
            }

            StringBuilder result_utf8 = new StringBuilder();
            for (int i = 0; i < input_c.length; i++) {
                c = (char) input_c[i];
                if (
                    (c >= '0' && c <= '9')
                    || (c >= 'A' && c <= 'Z')
                    || (c >= 'a' && c <= 'z')
                ) {
                    result_utf8.append(c);
                }
                else {
                    result_utf8.append(String.format("%%%02X", input_c[i]));
                }
            }

            // return enhanced version with UTF-8 support
            return String.format("%s=\"%s\"; %s*=utf-8''%s", attribute_name, result.toString(), attribute_name, result_utf8.toString());
        }

        @Override
        protected Object doInBackground(HttpRequestInput... inputs) {
            Object response = null;
            HttpURLConnection connection = null;
            try {
                HttpRequestInput input = inputs[0];

                // decide on the variable layout
                if (input.files.size() > 0) {
                    input.var_layout = HttpRequestVarLayout.MULTIPART;
                }
                if (input.var_layout == HttpRequestVarLayout.NOT_SET) {
                    input.var_layout = input.http_method.equals("GET") || input.http_method.equals("HEAD")
                        ? HttpRequestVarLayout.ADDRESS
                        : HttpRequestVarLayout.URL_ENCODED;
                }


                // prepare request content

                ByteArrayOutputStream content = new ByteArrayOutputStream();
                String boundary = null;

                if (input.var_layout == HttpRequestVarLayout.ADDRESS || input.var_layout == HttpRequestVarLayout.URL_ENCODED) {
                    // variable layout is ADDRESS or URL_ENCODED

                    if (input.vars.size() > 0) {
                        boolean first = true;
                        for (Map.Entry<String, String> element : input.vars.entrySet()) {
                            if (!first) {
                                content.write('&');
                            }
                            first = false;

                            content.write(URLEncoder.encode(element.getKey(), "UTF-8").getBytes(utf8));
                            content.write('=');
                            content.write(URLEncoder.encode(element.getValue(), "UTF-8").getBytes(utf8));
                        }

                        if (input.var_layout == HttpRequestVarLayout.ADDRESS) {
                            input.url_str += "?" + content.toString();
                        }
                    }
                }
                else {
                    // variable layout is MULTIPART

                    Random random = new Random();
                    String str;

                    boundary = "__-----------------------"
                            + String.valueOf(Math.abs(random.nextInt()))
                            + String.valueOf((new Date()).getTime());
                    byte[] boundary_body = boundary.getBytes(utf8);
                    byte[] boundary_delimiter = "--".getBytes(utf8);
                    byte[] new_line = "\r\n".getBytes(utf8);

                    // add variables
                    for (Map.Entry<String, String> element : input.vars.entrySet()) {
                        // add boundary
                        content.write(boundary_delimiter);
                        content.write(boundary_body);
                        content.write(new_line);

                        // add header
                        str = "Content-Disposition: form-data; " + http_attribute_encode("name", element.getKey());
                        content.write(str.getBytes(utf8));
                        content.write(new_line);
                        content.write("Content-Type: text/plain".getBytes(utf8));
                        content.write(new_line);

                        // add header to body splitter
                        content.write(new_line);

                        // add variable content
                        content.write(element.getValue().getBytes(utf8));
                        content.write(new_line);
                    }

                    // add files
                    for (HttpRequestInputFileElement file_info : input.files) {
                        File file = null;
                        if (file_info.local_filename != null && !file_info.local_filename.isEmpty()) {
                            file = new File(file_info.local_filename);
                        }

                        // ensure necessary variables are available
                        if (
                            file_info.variable_name == null || file_info.variable_name.isEmpty()
                            || file == null || !file.exists() || !file.isFile() || !file.canRead()
                        ) {
                            // silent abort for the current file
                            continue;
                        }

                        // ensure filename for the request
                        if (file_info.request_filename == null || file_info.request_filename.isEmpty()) {
                            file_info.request_filename = file.getName();
                            if (file_info.request_filename.isEmpty()) {
                                file_info.request_filename = "file";
                            }
                        }

                        // add boundary
                        content.write(boundary_delimiter);
                        content.write(boundary_body);
                        content.write(new_line);

                        // add header
                        str = String.format("Content-Disposition: form-data; %s; %s",
                            http_attribute_encode("name", file_info.variable_name),
                            http_attribute_encode("filename", file_info.request_filename)
                        );
                        content.write(str.getBytes(utf8));
                        content.write(new_line);

                        if (file_info.mime_type != null && !file_info.mime_type.isEmpty()) {
                            str = "Content-Type: " + file_info.mime_type;
                            content.write(str.getBytes(utf8));
                            content.write(new_line);
                        }

                        str = "Content-Transfer-Encoding: binary";
                        content.write(str.getBytes(utf8));
                        content.write(new_line);

                        // add header to body splitter
                        content.write(new_line);

                        // add file content
                        DataInputStream in = new DataInputStream(new FileInputStream(file));
                        byte[] buffer = new byte[(int) file.length()];
                        in.readFully(buffer);
                        content.write(buffer);
                        content.write(new_line);
                    }

                    // add end of body
                    content.write(boundary_delimiter);
                    content.write(boundary_body);
                    content.write(boundary_delimiter);
                }


                // prepare connection

                URL url = new URL(input.url_str);
                connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod(input.http_method);
                connection.setRequestProperty("User-Agent", "Agent name goes here");

                if (input.var_layout == HttpRequestVarLayout.MULTIPART) {
                    connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
                }

                if (input.var_layout == HttpRequestVarLayout.URL_ENCODED || input.var_layout == HttpRequestVarLayout.MULTIPART) {
                    connection.setDoOutput(true);
                    connection.setFixedLengthStreamingMode(content.size());
                    connection.getOutputStream().write(content.toByteArray());
                }


                // connect, send request, receive response

                connection.connect();
                int status = connection.getResponseCode();
                if (status == 200) {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                    response = new StringBuilder();
                    char[] buffer = new char[4096];
                    int bytes_read;
                    while (-1 != (bytes_read = reader.read(buffer))) {
                        ((StringBuilder) response).append(buffer, 0, bytes_read);
                    }
                }
                else {
                    response = status;
                }
            }
            catch (MalformedURLException e) {
                response = e;
            }
            catch (IOException e) {
                response = e;
            }

            if (connection != null) {
                connection.disconnect();
            }

            return response;
        }

        @Override
        protected void onPostExecute(Object value) {
            // notify main app
            handle_result(value);
        }

    }

    public void on_button_click(View v) {
        // trigger the request
    }

    public void handle_result(Object response) {
        String msg;

        if (response != null && response instanceof StringBuilder) {
            // communication was successful
            msg = "Success - Response: " + (response);
        }
        else if (response == null) {
            // response is null - should never happen
            msg = "Error: Unhandled communication response";
        }
        else if (response instanceof Integer) {
            // HTTP response status is other than 200
            msg = "Response error: " + String.valueOf(response);
        }
        else if (response instanceof ClientProtocolException) {
            // ClientProtocolException exception
            msg = "ClientProtocolException: " + ((ClientProtocolException) response).getMessage();
        }
        else if (response instanceof IOException) {
            // IOException exception
            msg = "IOException: " + ((IOException) response).getMessage();
        }
        else {
            // unknown response - should never happen
            msg = "Error: Unhandled communication response";
        }

        Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
    }
}

There are a couple of notable things in this code:

  1. The use of internal classes for the communication process.
     
    We separate the communication thread by sub-classing AsyncTask. You can use other ways as well; in any case the way to compile the HTTP requests keep the same format principles.
     
    Android requires that communications work on a different thread than the main one for UI. If you try to ignore the thread separation, Android will throw a android.os.NetworkOnMainThreadException exception and block your communication channel.
     
  2. The significance of specialized classes for the HTTP input.
     
    This code uses the class HttpRequestInput as an encapsulation for input variables of the HTTP request. This was required because AsyncTask takes only one variable as input.
     
  3. The role of http_attribute_encode()
     
    One small detail is the way we must escape attribute values in the HTTP protocol. Being Greek gives us the advantage to have increased appreciation in internationalization/localization issues. When attribute values have non-latin characters things may get out of control. RFC 5987 makes the necessary provisions for not allowing the mess, and this function implements this provision.
     
  4. Trigger code
     
    The code we just showed includes the function on_button_click(). The next sections of this article will differentiate this function to implement different types of HTTP requests. In any case, this function is supposed to be an event handler of a button click from the UI.
     
  5. Result handling
     
    This code uses the handle_result() function to handle the communication result. You may notice that the result can be a number of different classes. For your convenience the one that signals success is on the top. The rest of possible cases handle different types of errors.
     
  6. Little things you need to adjust in your implementation:
     
    This code sets the HTTP header for the name of the HTTP client/agent. You need to change "Agent name goes here" with the name of your client.
     
    You obviously need to set the right code for triggering the code in on_button_click() . Read the next sections for more examples on this function.
     
    Naturally, you should customize handle_result() . Our code is just an example of the different things you can do when the communication is completed.
     

Simple HTTP GET requests

Lets see how to run a simple GET request.

public void on_button_click(View v) {
    String url_str = "http://www.example.com/path/to/page.php";

    HttpRequestInput input = new HttpRequestInput(url_str, "GET");

    new HttpRequestWorker().execute(input);
}

This will produce the following HTTP request:

GET /path/to/page.php HTTP/1.1
User-Agent: Agent name goes here
Host: www.example.com
Connection: Keep-Alive
Accept-Encoding: gzip

The previous example calls a plain URL. Lets add a few variables.

public void on_button_click(View v) {
    String url_str = "http://www.example.com/path/to/page.php";

    HttpRequestInput input = new HttpRequestInput(url_str, "GET");

    input.add_var("key1", "value1");
    input.add_var("key2", "value2");

    new HttpRequestWorker().execute(input);
}

This will produce the following HTTP request:

GET /path/to/page.php?key2=value2&key1=value1 HTTP/1.1
User-Agent: Agent name goes here
Host: www.example.com
Connection: Keep-Alive
Accept-Encoding: gzip

URL encoded HTTP POST requests

We can make a slight adjustment and turn the GET request to a URL encoded POST request.

public void on_button_click(View v) {
    String url_str = "http://www.example.com/path/to/page.php";

    HttpRequestInput input = new HttpRequestInput(url_str, "POST");

    input.add_var("key1", "value1");
    input.add_var("key2", "value2");

    new HttpRequestWorker().execute(input);
}

This will produce the following HTTP request:

POST /path/to/page.php HTTP/1.1
User-Agent: Agent name goes here
Host: www.example.com
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Type: application/x-www-form-urlencoded
Content-Length: 23

key2=value2&key1=value1

Multipart HTTP POST requests

Finally, lets push it to the limits. Lets upload some files using a multipart POST request.

public void on_button_click(View v) {
    String url_str = "http://www.example.com/path/to/page.php";

    HttpRequestInput input = new HttpRequestInput(url_str, "POST");

    input.add_var("key1", "value1");
    input.add_var("key2", "value2");

    input.add_file("file1", "/path/to/file1.png", null, "image/png");
    input.add_file("file2", "/path/to/file2.png", null, "image/png");

    new HttpRequestWorker().execute(input);
}

This will produce the following HTTP request:

POST /path/to/page.php HTTP/1.1
User-Agent: Agent name goes here
Content-Type: multipart/form-data; boundary=__-----------------------9446182961397930864818
Host: www.example.com
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length: 686

--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="key2"
Content-Type: text/plain

value2
--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="key1"
Content-Type: text/plain

value1
--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="file1"; filename="file1.png"
Content-Type: image/png
Content-Transfer-Encoding: binary

[... contents of /path/to/file1.png ...]
--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="file2"; filename="file2.png"
Content-Type: image/png
Content-Transfer-Encoding: binary

[... contents of /path/to/file2.png ...]
--__-----------------------9446182961397930864818--