Java tutorial
/** * This file is part of PackRat, an Android app for managing media collections. * Copyright (C) 2009-2012 Jens Finkhaeuser <jens@finkhaeuser.de> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.unwesen.packrat.api; import android.net.Uri; import android.content.Context; import android.content.Intent; import java.io.InputStream; import java.io.ByteArrayOutputStream; import java.net.URI; import java.io.IOException; import android.os.Handler; import android.os.Message; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.params.HttpParams; import org.apache.http.params.BasicHttpParams; import org.apache.http.client.methods.HttpGet; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpProtocolParams; import org.apache.http.HttpVersion; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.auth.AuthScope; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.methods.HttpGet; import org.apache.http.HttpResponse; import org.apache.http.HttpEntity; import android.database.Cursor; import java.util.StringTokenizer; import java.text.SimpleDateFormat; import android.util.Log; /** * Shared functionality for all backend API classes, i.e. BackendAPI and * Tracker. * - Sets up HTTP client * - Includes helper functions for IO and parsing. * - etc. * * Most data members are static as there is technically no need for more than * one APIBase instance per process. On the other hand, we do need a Context, * so making the APIBase a singleton is impractical. **/ public class APIBase { /*************************************************************************** * Public constants **/ // Log ID public static final String LTAG = "APIBase"; // Error message IDs. public static final int ERR_SUCCESS = 0; public static final int ERR_EMPTY_RESPONSE = 1; public static final int ERR_TRANSMISSION = 2; public static final int ERR_SERIALIZATION = 3; public static final int ERR_SERVER = 4; public static final int ERR_UNKNOWN = 5; // Referer URL, required for some APIs. public static final String REFERER_URL = "http://packrat-app.unwesen.de/"; /*************************************************************************** * Private constants **/ // HTTP Client parameters // XXX These values are pretty much copied from example code and likely need // to be tweaked. private static final int HTTP_TIMEOUT = 60 * 1000; private static final HttpVersion HTTP_VERSION = HttpVersion.HTTP_1_1; // Requester startup delay. Avoids high load at startup that could impact UX. private static final int REQUESTER_STARTUP_DELAY = 1000; // Response processing private static final int READ_BUFFER_SIZE = 8192; /*************************************************************************** * Protected static data **/ protected static DefaultHttpClient sClient; protected static ThreadSafeClientConnManager sConnectionManager; /*************************************************************************** * Helper class for fetching API responses in the background. **/ protected class Requester extends Thread { public boolean keepRunning = true; protected Uri mUri; protected Handler mHandler; public Requester(Uri uri, Handler handler) { super(); mUri = uri; mHandler = handler; } @Override public void run() { // Delay before starting to fetch stuff. if (mDelayStartup) { try { sleep(REQUESTER_STARTUP_DELAY); mDelayStartup = false; } catch (java.lang.InterruptedException ex) { return; } } // The loop ensures that external interrrupts don't mean the request is // never executed.. while (keepRunning) { fetch(); keepRunning = false; } } /** * Fetch raw bytes by default. **/ public void fetch() { // Construct request. // TODO distinguish between creating get/post requests; probably // requires the Requester interface to not accept Uris but rather // API-strings, and params, etc. HttpGet request = new HttpGet(mUri.toString()); request.addHeader("Referer", REFERER_URL); // Perform request. byte[] data = fetchRawSynchronous(request, mHandler); if (null != data) { mHandler.obtainMessage(ERR_SUCCESS, new String(data)).sendToTarget(); } } } /*************************************************************************** * Custom Requester for fetching XML documents. Instead of passing raw * string data, it passes a HttpEntity. **/ protected class XMLRequester extends Requester { public XMLRequester(Uri uri, Handler handler) { super(uri, handler); } public void fetch() { // Construct request HttpGet request = new HttpGet(mUri.toString()); request.addHeader("Referer", REFERER_URL); HttpResponse response; try { response = sClient.execute(request); // Read response HttpEntity entity = response.getEntity(); if (null == entity) { mHandler.obtainMessage(ERR_EMPTY_RESPONSE).sendToTarget(); return; } mHandler.obtainMessage(ERR_SUCCESS, entity.getContent()).sendToTarget(); } catch (IOException ex) { Log.w(LTAG, "Error fetching XML document: " + ex); mHandler.obtainMessage(ERR_TRANSMISSION).sendToTarget(); } catch (Exception ex) { Log.e(LTAG, "Unknown exception: " + ex); mHandler.obtainMessage(ERR_UNKNOWN).sendToTarget(); } } } /*************************************************************************** } /*************************************************************************** * Protected data members **/ protected Context mContext; // Requester. There's only one instance, so only one API call can be scheduled // at a time. private Requester mRequester; private boolean mDelayStartup = true; /*************************************************************************** * Implementation **/ public APIBase(Context context) { mContext = context; if (null == sConnectionManager || null == sClient) { // Set up an HttpClient instance that can be used by multiple threads HttpParams params = defaultHttpParams(); SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443)); sConnectionManager = new ThreadSafeClientConnManager(params, registry); sClient = new DefaultHttpClient(sConnectionManager, params); } } /** * Request the given Uri. Send ERR_SUCCESS and a response string on success, * else other message IDs and null for the object. * Interrupts/cancels any requests currently in flight. **/ public void requestUri(Uri uri, Handler result_handler) { startRequester(new Requester(uri, result_handler)); } /** * Start the given Requester. **/ protected void startRequester(Requester requester) { if (null != mRequester) { mRequester.keepRunning = false; mRequester.interrupt(); } mRequester = requester; mRequester.start(); } /** * Converts an InputStream to a String containing the InputStream's content. **/ public static String readStream(InputStream is) { try { return new String(readStreamRaw(is)); } catch (IOException ex) { Log.e(LTAG, "Error: " + ex.getMessage()); return null; } } /** * Converts an InputStream to a byte array containing the InputStream's content. **/ public static byte[] readStreamRaw(InputStream is) throws IOException { ByteArrayOutputStream os = new ByteArrayOutputStream(READ_BUFFER_SIZE); byte[] bytes = new byte[READ_BUFFER_SIZE]; try { // Read bytes from the input stream in chunks and write // them into the output stream int bytes_read = 0; while (-1 != (bytes_read = is.read(bytes))) { os.write(bytes, 0, bytes_read); } byte[] retval = os.toByteArray(); is.close(); os.close(); return retval; } catch (java.io.IOException ex) { Log.e(LTAG, "Could not read input stream: " + ex.getMessage()); } return null; } public byte[] fetchRawSynchronous(HttpRequestBase request, Handler handler) { HttpResponse response; try { response = sClient.execute(request); // Read response HttpEntity entity = response.getEntity(); if (null == entity) { Log.e(LTAG, "Response is empty: " + request.getURI().toString()); if (null != handler) { handler.obtainMessage(ERR_EMPTY_RESPONSE).sendToTarget(); } return null; } return readStreamRaw(entity.getContent()); } catch (IOException ex) { Log.w(LTAG, "An exception occurred when reading the API response: " + ex.getMessage()); if (null != handler) { handler.obtainMessage(ERR_TRANSMISSION).sendToTarget(); } return null; } } /** * Returns a SimpleDateFormat instance initialized to parse/format UTC * timestamps according to ISO 8601. **/ public static SimpleDateFormat ISO8601Format() { return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); } /** * Returns a set of default HTTP parameters. **/ protected static HttpParams defaultHttpParams() { HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, HTTP_TIMEOUT); HttpProtocolParams.setVersion(params, HTTP_VERSION); return params; } }