com.catchnotes.api.CatchAPI.java Source code

Java tutorial

Introduction

Here is the source code for com.catchnotes.api.CatchAPI.java

Source

//
//  Copyright 2011 Catch.com, Inc.
//  
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//  
//      http://www.apache.org/licenses/LICENSE-2.0
//  
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

package com.catchnotes.api;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParserException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.net.Uri;
import android.os.Build;
import android.text.format.Time;
import android.util.Log;
import android.util.TimeFormatException;

import com.android.http.multipart.FilePart;
import com.android.http.multipart.MultipartEntity;
import com.android.http.multipart.Part;
import com.android.http.multipart.StringPart;

/**
 * Java Android interface to the Catch API.
 */
public class CatchAPI {
    public static final int RESULT_ERROR = 0;
    public static final int RESULT_OK = 1;
    public static final int RESULT_UNAUTHORIZED = 2;
    public static final int RESULT_ERROR_PENDING = 3;
    public static final int RESULT_ERROR_PARSER = 4;
    public static final int RESULT_ERROR_RESPONSE = 5;
    public static final int RESULT_AVAILABLE = 6;
    public static final int RESULT_UNAVAILABLE = 7;
    public static final int RESULT_BAD_REQUEST = 8;
    public static final int RESULT_NOT_FOUND = 9;
    public static final int RESULT_OVER_QUOTA = 10;
    public static final int RESULT_SERVER_ERROR = 11;
    public static final int RESULT_UNSUPPORTED_MEDIA = 12;

    private static final String ENCODING = "UTF-8";
    private String userAgent = "CatchAPI (Android)";
    private String source = "CatchAPI for Android";
    public static final String LOGCAT_NAME = "CatchAPI";
    private static final int BUFFER_SIZE = 8 * 1024;

    private String catchBaseUrl = "https://api.catch.com";
    private static final String API_ENDPOINT_ACCOUNT_INFO = "/v2/user";
    private static final String API_ENDPOINT_NOTES = "/v2/notes";
    private static final String API_ENDPOINT_BULK_NOTES = "/v2/bulk_notes";
    private static final String API_ENDPOINT_COMMENTS = "/v2/comments";
    private static final String API_ENDPOINT_COMMENT = "/v2/comment";
    private static final String API_ENDPOINT_MEDIA = "/v2/media";

    public static final String IMAGE_SMALL = "small";
    public static final String IMAGE_MEDIUM = "medium";
    public static final String IMAGE_LARGE = "large";

    private VersionedCatchHttpClient httpClientAccessToken = null;
    private HttpClient httpClientNoToken = null;

    private Time timestamper;
    private SimpleDateFormat rfc3339;
    private Context mContext;
    public boolean loggingEnabled = false;

    /** 
     * Constructor.
     * 
     * @param appName The name of your application.
     * @param context Android context.
     */
    public CatchAPI(String appName, Context context) {
        source = appName;
        mContext = context;
        String version = "x.xx";

        try {
            PackageInfo info = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
            version = info.versionName;
        } catch (Exception e) {
        }

        StringBuilder locale = new StringBuilder(5);
        String language = Locale.getDefault().getLanguage();

        if (language != null) {
            locale.append(language);
            String country = Locale.getDefault().getCountry();

            if (country != null) {
                locale.append('-');
                locale.append(country);
            }
        }

        userAgent = appName + '/' + version + " (Android; " + Build.VERSION.RELEASE + "; " + Build.BRAND + "; "
                + Build.MODEL + "; " + locale.toString() + ')';

        try {
            if (Integer.parseInt(Build.VERSION.SDK) >= Build.VERSION_CODES.ECLAIR_0_1) {
                // We can use Time & TimeFormatException on Android 2.0.1
                // (API level 6) or greater. It crashes the VM otherwise.
                timestamper = new Time();
            }
        } catch (Exception e) {
        }
    }

    /**
     * Sign in with a username and password.
     * 
     * @param user Username or email address of the account.
     * @param password Account password.
     * @param account An optional CatchAccount object that will be filled in with 
     * account information.
     * @return RESULT_OK on success.
     */
    public int signIn(String user, String password, CatchAccount account) {
        CatchAccount acct = account;
        if (acct == null) {
            acct = new CatchAccount();
        }

        int returnCode = getAccountInfo(user, password, acct);
        if (returnCode == RESULT_OK) {
            setAccessToken(acct.auth_token);
        }

        return returnCode;
    }

    /**
     * Sign out.
     */
    public void close() {
        if (httpClientAccessToken != null) {
            httpClientAccessToken.close();
        }
    }

    private VersionedCatchHttpClient getHttpClient() {
        if (httpClientAccessToken == null) {
            httpClientAccessToken = VersionedCatchHttpClient.newInstance(userAgent, mContext);
        }

        return httpClientAccessToken;
    }

    private HttpClient getHttpClientNoToken() {
        if (httpClientNoToken == null) {
            HttpParams params = new BasicHttpParams();
            HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
            HttpProtocolParams.setUserAgent(params, userAgent);
            HttpProtocolParams.setContentCharset(params, "UTF-8");
            httpClientNoToken = new DefaultHttpClient(params);
        }

        return httpClientNoToken;
    }

    /**
     * Sign-in using a previously obtained access token.
     * 
     * @param accessToken The access token.
     */
    public void setAccessToken(String accessToken) {
        getHttpClient().setAccessToken(accessToken);
    }

    /**
     * Get information about a user's account (user name, date created, etc).
     * 
     * @param user Username or email address.  Pass null if already signed in.
     * @param password Account password.  Pass null if already signed in.
     * @param account CatchAccount object where information will be returned.
     * @return RESULT_OK on success.
     */
    public int getAccountInfo(String user, String password, CatchAccount account) {
        int returnCode = RESULT_ERROR;

        if (account == null) {
            return returnCode;
        }

        HttpResponse response = null;

        if (user != null && password != null && user.length() > 0 && password.length() > 0) {
            // Use user auth to get user info and obtain auth_token
            response = performPOST(API_ENDPOINT_ACCOUNT_INFO, null, null, false, user + ':' + password);
        } else {
            response = performPOST(API_ENDPOINT_ACCOUNT_INFO, null, null, true, null);
        }

        if (response != null) {
            if (isResponseOK(response)) {
                boolean parseResult = false;

                try {
                    JSONObject json = new JSONObject(istreamToString(response.getEntity().getContent()));

                    // "user" block
                    JSONObject userblock = json.getJSONObject("user");
                    account.id = userblock.getLong("id");
                    account.username = userblock.getString("user_name");
                    account.email = userblock.getString("email");
                    account.accountCreatedOn = parse3339(userblock.getString("created_at"));
                    account.auth_token = userblock.getString("access_token");

                    // "account_limits" block
                    if (userblock.has("account_limits")) {
                        JSONObject limitsblock = userblock.getJSONObject("account_limits");
                        account.periodLimit = limitsblock.getLong("monthly_upload_limit");
                    }

                    // "account_upload_activity_periodic" block
                    if (userblock.has("account_upload_activity_periodic")) {
                        JSONArray activityarray = userblock.getJSONArray("account_upload_activity_periodic");

                        if (activityarray.length() > 0) {
                            JSONObject currentPeriod = activityarray.getJSONObject(0);
                            account.periodStart = parse3339(currentPeriod.getString("period_start"));
                            account.periodUsage = currentPeriod.getLong("period_activity");
                            account.periodEnd = parse3339(currentPeriod.getString("period_end"));
                        }
                    }

                    // account info
                    if (userblock.has("account_level")) {
                        account.accountLevel = userblock.getInt("account_level");
                    }

                    if (userblock.has("account_level_desc")) {
                        account.accountDescription = userblock.getString("account_level_desc");
                    }

                    // "account_subscription_end_advisory"
                    if (userblock.has("account_subscription_end_advisory")) {
                        String subEnd = userblock.getString("account_subscription_end_advisory");

                        if (subEnd != null && !"null".equals(subEnd)) {
                            account.subscriptionEnd = parse3339(subEnd);
                        }
                    }

                    parseResult = true;
                } catch (JSONException e) {
                    log("caught a JSONException processing response from POST " + API_ENDPOINT_ACCOUNT_INFO, e);
                } catch (IOException e) {
                    log("caught an IOException processing response from POST " + API_ENDPOINT_ACCOUNT_INFO, e);
                }

                returnCode = (parseResult == true) ? RESULT_OK : RESULT_ERROR_RESPONSE;
            } else if (isResponseUnauthorized(response)) {
                returnCode = RESULT_UNAUTHORIZED;
            } else if (isResponseServerError(response)) {
                returnCode = RESULT_SERVER_ERROR;
            }

            consumeResponse(response);
        }

        return returnCode;
    }

    /**
     * Fetches a list of note references representing all of the user's notes.
     * 
     * @param noteRefs List where fetched note refs will be returned.
     * @return RESULT_OK on success.
     */
    public int getSyncV2(List<CatchNoteRef> noteRefs) {
        int returnCode = RESULT_ERROR;

        if (noteRefs == null) {
            return returnCode;
        }

        String endpoint = API_ENDPOINT_NOTES + ".xml";
        HttpResponse response = performGET(endpoint, null, true);

        if (response != null) {
            if (isResponseOK(response)) {
                boolean parseResult = false;

                try {
                    CatchNotesXmlParser xmlParser = new CatchNotesXmlParser();
                    xmlParser.parseSyncV2XML(response, noteRefs);
                    parseResult = true;
                } catch (IllegalArgumentException e) {
                    log("caught an IllegalArgumentException processing response from GET " + endpoint, e);
                } catch (XmlPullParserException e) {
                    log("caught an XmlPullParserException processing response from GET " + endpoint, e);
                } catch (IOException e) {
                    log("caught an IOException processing response from GET " + endpoint, e);
                }

                returnCode = (parseResult == true) ? RESULT_OK : RESULT_ERROR_RESPONSE;
            } else if (isResponseUnauthorized(response)) {
                returnCode = RESULT_UNAUTHORIZED;
            } else if (isResponseServerError(response)) {
                returnCode = RESULT_SERVER_ERROR;
            }

            consumeResponse(response);
        }

        return returnCode;
    }

    /**
     * Fetches a list of specific requested notes.
     * 
     * @param noteRefs List of note refs representing the notes you want to fetch.
     * @param notes List where fetched notes will be returned.
     * @return RESULT_OK on success.
     */
    public int getNotesBulk(List<CatchNoteRef> noteRefs, ArrayList<CatchNote> notes) {
        int returnCode = RESULT_ERROR;

        if (notes == null) {
            return returnCode;
        }

        String endpoint;
        endpoint = API_ENDPOINT_BULK_NOTES + ".xml";
        StringBuilder bulkNotes = new StringBuilder("{\"notes\": [\"");
        Iterator<CatchNoteRef> syncIterator = noteRefs.iterator();

        while (syncIterator.hasNext()) {
            bulkNotes.append(syncIterator.next().nodeId);

            if (syncIterator.hasNext()) {
                bulkNotes.append("\", \"");
            }
        }

        bulkNotes.append("\"]}");
        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("bulk", bulkNotes.toString()));
        HttpResponse response = performPOST(endpoint, params, null, true, null);

        if (response != null) {
            if (isResponseOK(response)) {
                boolean parseResult = false;

                try {
                    CatchNotesXmlParser xmlParser = new CatchNotesXmlParser();
                    xmlParser.parseNotesXml(response, notes, -1);
                    parseResult = true;
                } catch (IllegalArgumentException e) {
                    log("caught an IllegalArgumentException processing response from POST " + endpoint, e);
                } catch (XmlPullParserException e) {
                    log("caught an XmlPullParserException processing response from POST " + endpoint, e);
                } catch (IOException e) {
                    log("caught an IOException processing response from POST " + endpoint, e);
                }

                returnCode = (parseResult == true) ? RESULT_OK : RESULT_ERROR_RESPONSE;
            } else if (isResponseUnauthorized(response)) {
                returnCode = RESULT_UNAUTHORIZED;
            } else if (isResponseServerError(response)) {
                returnCode = RESULT_SERVER_ERROR;
            }

            consumeResponse(response);
        }

        return returnCode;
    }

    /**
     * Fetches a list of all of the user's notes.
     * 
     * @param notes List where fetched notes will be returned.
     * @return RESULT_OK on success.
     */
    public int getNotes(ArrayList<CatchNote> notes) {
        return getComments(-1, notes);
    }

    /**
     * Fetches a list of comments belonging to a certain note.
     * 
     * @param parentId The parent note of the requested comments.
     * @param notes List where fetched comments will be returned.
     * @return RESULT_OK on success.
     */
    public int getComments(long parentId, ArrayList<CatchNote> notes) {
        int returnCode = RESULT_ERROR;

        if (notes == null) {
            return returnCode;
        }

        String endpoint;
        List<NameValuePair> params = null;

        if (parentId != -1) {
            endpoint = API_ENDPOINT_COMMENTS + '/' + parentId + ".xml";
        } else {
            endpoint = API_ENDPOINT_NOTES + ".xml";
            params = new ArrayList<NameValuePair>();
            params.add(new BasicNameValuePair("full", "1"));
        }

        HttpResponse response = performGET(endpoint, params, true);

        if (response != null) {
            if (isResponseOK(response)) {
                boolean parseResult = false;

                try {
                    CatchNotesXmlParser xmlParser = new CatchNotesXmlParser();
                    xmlParser.parseNotesXml(response, notes, parentId);
                    parseResult = true;
                } catch (IllegalArgumentException e) {
                    log("caught an IllegalArgumentException processing response from GET " + endpoint, e);
                } catch (XmlPullParserException e) {
                    log("caught an XmlPullParserException processing response from GET " + endpoint, e);
                } catch (IOException e) {
                    log("caught an IOException processing response from GET " + endpoint, e);
                }

                returnCode = (parseResult == true) ? RESULT_OK : RESULT_ERROR_RESPONSE;
            } else if (isResponseUnauthorized(response)) {
                returnCode = RESULT_UNAUTHORIZED;
            } else if (isResponseServerError(response)) {
                returnCode = RESULT_SERVER_ERROR;
            }

            consumeResponse(response);
        }

        return returnCode;
    }

    /**
     * Gets a data stream for a media item.
     * 
     * @param uri The URI of the media to fetch.  See {@link CatchMedia#src}.
     * @param size If the media is an image, then you may pass IMAGE_SMALL, 
     * IMAGE_MEDIUM, or IMAGE_LARGE.  Otherwise pass null.
     * @param outputStream Data is returned here.
     * @return RESULT_OK on success.
     */
    public int getMedia(String uri, String size, OutputStream outputStream) {
        int returnCode = RESULT_ERROR;

        if (uri == null || outputStream == null) {
            return returnCode;
        }

        try {
            List<NameValuePair> httpParams = new ArrayList<NameValuePair>();

            if (size != null) {
                httpParams.add(new BasicNameValuePair("size", size));
            }

            if (uri.contains("viewImage.action?")) {
                // Legacy viewImage.action image source URLs need to be pulled
                // apart a bit so as to fit into the new logic
                httpParams.add(new BasicNameValuePair("imageId", Uri.parse(uri).getQueryParameter("imageId")));
                uri = uri.split("\\?")[0];
            }

            HttpResponse response = performGET(uri, (httpParams.size() > 0) ? httpParams : null, true);

            if (response != null) {
                if (isResponseOK(response)) {
                    BufferedInputStream inputStream = new BufferedInputStream(response.getEntity().getContent(),
                            BUFFER_SIZE);

                    byte[] b = new byte[BUFFER_SIZE];
                    int read;

                    while ((read = inputStream.read(b)) != -1) {
                        outputStream.write(b, 0, read);
                    }

                    outputStream.flush();
                    outputStream.close();
                    inputStream.close();
                    returnCode = RESULT_OK;
                } else if (isResponseUnauthorized(response)) {
                    returnCode = RESULT_UNAUTHORIZED;
                } else if (isResponseNotFound(response)) {
                    returnCode = RESULT_NOT_FOUND;
                } else if (isResponseServerError(response)) {
                    returnCode = RESULT_SERVER_ERROR;
                }

                consumeResponse(response);
            }
        } catch (IOException e) {
            log("caught a IOException in getImage() for " + uri, e);
        }

        return returnCode;
    }

    /**
     * Adds a new note to the account.
     * 
     * @param note Note to be added.
     * @return RESULT_OK on success.
     */
    public int addNote(CatchNote note) {
        int returnCode = RESULT_ERROR;

        if (note == null) {
            return returnCode;
        }

        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("text", (note.text == null) ? "" : note.text.toString()));
        params.add(new BasicNameValuePair("created_at", Long.toString(note.creationTime)));
        params.add(new BasicNameValuePair("modified_at", Long.toString(note.modificationTime)));

        if (note.source == null || note.source.length() == 0 || note.source.equals(CatchNote.NOT_SET)) {
            params.add(new BasicNameValuePair("source", source));
        } else {
            params.add(new BasicNameValuePair("source", note.source));

            if (note.sourceUrl != null && note.source.length() > 0 && note.sourceUrl.equals(CatchNote.NOT_SET)) {
                params.add(new BasicNameValuePair("source_url", note.sourceUrl));
            }
        }

        //XXXaes
        if (note.annotations != null) {
            for (String key : note.annotations.keySet()) {
                params.add(new BasicNameValuePair(CatchNotesXmlParser.CATCH_NAMESPACE_PREFIX + ":" + key,
                        note.annotations.get(key)));
            }
        }

        String endpoint;

        if (note.parentId != -1) {
            // adding a comment
            endpoint = API_ENDPOINT_COMMENTS + '/' + note.parentNodeId + ".xml";
        } else {
            // adding a note
            endpoint = API_ENDPOINT_NOTES + ".xml";
            params.add(new BasicNameValuePair("mode",
                    (note.mode == null) ? CatchNote.MODE_PRIVATE : note.mode.toString()));
            params.add(new BasicNameValuePair("reminder_at", Long.toString(note.reminderTime)));
            params.add(new BasicNameValuePair("latitude", Double.toString(note.latitude)));
            params.add(new BasicNameValuePair("longitude", Double.toString(note.longitude)));
            params.add(new BasicNameValuePair("altitude", Double.toString(note.altitude)));
            params.add(new BasicNameValuePair("speed", Double.toString(note.speed)));
            params.add(new BasicNameValuePair("bearing", Double.toString(note.bearing)));
            params.add(new BasicNameValuePair("accuracy_position", Double.toString(note.accuracyPosition)));
            params.add(new BasicNameValuePair("accuracy_altitude", Double.toString(note.accuracyAltitude)));
        }

        HttpResponse response = performPOST(endpoint, params, null, true, null);

        if (response != null) {
            if (isResponseOK(response)) {
                boolean parseResult = false;

                try {
                    CatchNotesXmlParser xmlParser = new CatchNotesXmlParser();
                    ArrayList<CatchNote> notes = new ArrayList<CatchNote>(1);
                    xmlParser.parseNotesXml(response, notes, note.parentId);

                    if (notes.size() > 0 && notes.get(0) != null) {
                        // Copy the returned note values into the original note object
                        note.copy(notes.get(0));
                        parseResult = true;
                    }
                } catch (IllegalArgumentException e) {
                    log("caught an IllegalArgumentException processing response from POST " + endpoint, e);
                } catch (XmlPullParserException e) {
                    log("caught an XmlPullParserException processing response from POST " + endpoint, e);
                } catch (IOException e) {
                    log("caught an IOException processing response from POST " + endpoint, e);
                }

                returnCode = (parseResult == true) ? RESULT_OK : RESULT_ERROR_RESPONSE;
            } else if (isResponseUnauthorized(response)) {
                returnCode = RESULT_UNAUTHORIZED;
            } else if (isResponseNotFound(response)) {
                returnCode = RESULT_NOT_FOUND;
            } else if (isResponsePaymentRequired(response)) {
                returnCode = RESULT_OVER_QUOTA;
            } else if (isResponseServerError(response)) {
                returnCode = RESULT_SERVER_ERROR;
            } else {
                returnCode = RESULT_ERROR_PENDING;
            }

            consumeResponse(response);
        }

        return returnCode;
    }

    /**
     * Adds a media item to a note.
     * 
     * @param media A CatchMedia object that specifies the media data and note ID.
     * @return RESULT_OK on success.
     */
    public int addMedia(CatchMedia media) {
        int returnCode = RESULT_ERROR;

        if (media == null || media.data == null || media.content_type == null || media.note_id == null) {
            return returnCode;
        }

        try {
            String endpoint = API_ENDPOINT_MEDIA + '/' + media.note_id;

            StringPart contentType = new StringPart("content_type", media.content_type, HTTP.UTF_8);
            StringPart createdAt = new StringPart("created_at", Long.toString(media.created_at), HTTP.UTF_8);
            StringPart voiceHint = new StringPart("voicenote_hint", "true", HTTP.UTF_8);
            FilePart data = new FilePart("data", media.data, media.content_type, null);

            HttpResponse response;

            if (media.voice_hint) {
                response = performPOST(endpoint, null, new Part[] { contentType, createdAt, voiceHint, data }, true,
                        null);
            } else {
                response = performPOST(endpoint, null, new Part[] { contentType, createdAt, data }, true, null);
            }

            if (response != null) {
                if (isResponseOK(response)) {
                    boolean parseResult = false;

                    try {
                        JSONObject json = new JSONObject(istreamToString(response.getEntity().getContent()));
                        media.id = json.getString("id");
                        media.src = json.getString("src");
                        media.size = json.getLong("size");
                        media.content_type = json.getString("type");
                        media.created_at = parse3339(json.getString("created_at"));
                        media.voice_hint = json.getBoolean("voicenote_hint");
                        parseResult = true;
                    } catch (JSONException e) {
                        log("caught a JSONException processing response from POST " + endpoint, e);
                    } catch (IOException e) {
                        log("caught an IOException processing response from POST " + endpoint, e);
                    }

                    returnCode = (parseResult == true) ? RESULT_OK : RESULT_ERROR_RESPONSE;
                } else if (isResponsePaymentRequired(response)) {
                    returnCode = RESULT_OVER_QUOTA;
                } else if (isResponseUnsupportedMedia(response)) {
                    returnCode = RESULT_UNSUPPORTED_MEDIA;
                } else if (isResponseUnauthorized(response)) {
                    returnCode = RESULT_UNAUTHORIZED;
                } else if (isResponseBadRequest(response)) {
                    returnCode = RESULT_BAD_REQUEST;
                } else if (isResponseServerError(response)) {
                    returnCode = RESULT_SERVER_ERROR;
                }

                consumeResponse(response);
            }
        } catch (FileNotFoundException e) {
            log("caught a FileNotFoundException in addMedia() for file: " + media.data.getAbsolutePath(), e);
        }

        return returnCode;
    }

    /**
     * Edits an existing note.
     * 
     * @param note Note to be updated. note.id must be a valid existing note id.
     * @return RESULT_OK on success.
     */
    public int editNote(CatchNote note) {
        int returnCode = RESULT_ERROR;

        if (note == null) {
            return returnCode;
        }

        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("text", (note.text == null) ? "" : note.text.toString()));
        params.add(new BasicNameValuePair("source", source));

        // TODO: if we supply server_modified_at, the backend will perform conflict detection
        //params.add(new BasicNameValuePair("server_modified_at", Long.toString(note.serverModifiedAt)));

        params.add(new BasicNameValuePair("modified_at", Long.toString(note.modificationTime)));

        String endpoint;

        if (note.parentId != -1) {
            // editing a comment
            params.add(new BasicNameValuePair("comment", note.id));
            endpoint = API_ENDPOINT_COMMENT + '/' + note.parentNodeId + ".xml";
        } else {
            // editing a note
            endpoint = API_ENDPOINT_NOTES + '/' + note.id + ".xml";
            params.add(new BasicNameValuePair("mode",
                    (note.mode == null) ? CatchNote.MODE_PRIVATE : note.mode.toString()));
            params.add(new BasicNameValuePair("reminder_at", Long.toString(note.reminderTime)));
        }

        //XXXaes
        if (note.annotations != null) {
            for (String key : note.annotations.keySet()) {
                params.add(new BasicNameValuePair(CatchNotesXmlParser.CATCH_NAMESPACE_PREFIX + ":" + key,
                        note.annotations.get(key)));
            }
        }

        HttpResponse response = performPOST(endpoint, params, null, true, null);

        if (response != null) {
            if (isResponseOK(response)) {
                boolean parseResult = false;

                try {
                    CatchNotesXmlParser xmlParser = new CatchNotesXmlParser();
                    ArrayList<CatchNote> notes = new ArrayList<CatchNote>(1);
                    xmlParser.parseNotesXml(response, notes, note.parentId);

                    if (notes.size() > 0 && notes.get(0) != null) {
                        // Copy the returned note values into the original note object
                        note.copy(notes.get(0));
                        parseResult = true;
                    }
                } catch (IllegalArgumentException e) {
                    log("caught an IllegalArgumentException processing response from POST " + endpoint, e);
                } catch (XmlPullParserException e) {
                    log("caught an XmlPullParserException processing response from POST " + endpoint, e);
                } catch (IOException e) {
                    log("caught an IOException processing response from POST " + endpoint, e);
                }

                returnCode = (parseResult == true) ? RESULT_OK : RESULT_ERROR_RESPONSE;
            } else if (isResponseUnauthorized(response)) {
                returnCode = RESULT_UNAUTHORIZED;
            } else if (isResponseNotFound(response)) {
                returnCode = RESULT_NOT_FOUND;
            } else if (isResponsePaymentRequired(response)) {
                returnCode = RESULT_OVER_QUOTA;
            } else if (isResponseServerError(response)) {
                returnCode = RESULT_SERVER_ERROR;
            } else {
                returnCode = RESULT_ERROR_PENDING;
            }

            consumeResponse(response);
        }

        return returnCode;
    }

    /**
     * Deletes a note.
     * 
     * @param id ID of the note to be deleted
     * @param parentNodeId If you are deleting a comment, this is the parent note's ID.
     * @return RESULT_OK on success
     */
    public int deleteNote(String id, String parentNodeId) {
        int returnCode = RESULT_ERROR;
        HttpResponse response;

        if (parentNodeId == null || CatchNote.NODE_ID_NEVER_SYNCED.equals(parentNodeId)) {
            response = performDELETE(API_ENDPOINT_NOTES + '/' + id, null);
        } else {
            List<NameValuePair> params = new ArrayList<NameValuePair>();
            params.add(new BasicNameValuePair("comment", id));
            response = performDELETE(API_ENDPOINT_COMMENT + '/' + parentNodeId, params);
        }

        if (response != null) {
            if (isResponseOK(response)) {
                returnCode = RESULT_OK;
            } else if (isResponseUnauthorized(response)) {
                returnCode = RESULT_UNAUTHORIZED;
            } else if (isResponseNotFound(response)) {
                returnCode = RESULT_NOT_FOUND;
            } else if (isResponseServerError(response)) {
                returnCode = RESULT_SERVER_ERROR;
            } else {
                returnCode = RESULT_ERROR_PENDING;
            }

            consumeResponse(response);
        }

        return returnCode;
    }

    /**
     * Deletes a media item.
     * 
     * @param noteId The note containing the media you want to remove.
     * @param mediaId ID of the media.
     * @return RESULT_OK on success.
     */
    public int deleteMedia(String noteId, String mediaId) {
        int returnCode = RESULT_ERROR;
        HttpResponse response = performDELETE(API_ENDPOINT_MEDIA + '/' + noteId + '/' + mediaId, null);

        if (response != null) {
            if (isResponseOK(response)) {
                returnCode = RESULT_OK;
            } else if (isResponseUnauthorized(response)) {
                returnCode = RESULT_UNAUTHORIZED;
            }

            consumeResponse(response);
        }

        return returnCode;
    }

    private HttpResponse performGET(String method, List<NameValuePair> httpParams, boolean useToken) {
        HttpGet httpget;
        String uri = method;

        if (!uri.startsWith("http")) {
            // method isn't a fully-qualified URI
            uri = catchBaseUrl + uri;
        }

        if (httpParams == null || httpParams.isEmpty()) {
            httpget = new HttpGet(uri);
        } else {
            httpget = new HttpGet(uri + '?' + URLEncodedUtils.format(httpParams, "UTF-8"));
        }

        HttpResponse response = null;

        try {
            response = useToken ? getHttpClient().execute(httpget) : getHttpClientNoToken().execute(httpget);
        } catch (ClientProtocolException e) {
            log("caught ClientProtocolException performing GET " + httpget.getURI(), e);
            return null;
        } catch (UnknownHostException e) {
            log("caught UnknownHostException performing GET " + httpget.getURI(), null);
            return null;
        } catch (IOException e) {
            log("caught IOException performing GET " + httpget.getURI(), e);
            return null;
        }

        sync_trace("GET " + httpget.getURI() + " returned " + response.getStatusLine().getStatusCode() + ' '
                + response.getStatusLine().getReasonPhrase());
        return response;
    }

    private HttpResponse performPOST(String method, List<NameValuePair> parameters, Part[] parts, boolean useToken,
            String auth) {
        HttpPost httppost = new HttpPost(catchBaseUrl + method);

        if (parameters != null && !parameters.isEmpty()) {
            try {
                httppost.setEntity(new UrlEncodedFormEntity(parameters, HTTP.UTF_8));
            } catch (UnsupportedEncodingException e) {
                log("caught UnsupportedEncodingException performing POST " + httppost.getURI(), e);
                return null;
            }
        } else if (parts != null) {
            HttpProtocolParams.setUseExpectContinue(httppost.getParams(), false);
            httppost.setEntity(new MultipartEntity(parts, httppost.getParams()));
        }

        if (auth != null) {
            if (useToken) {
                return null;
            }

            try {
                httppost.addHeader("Authorization",
                        "Basic " + new String(Base64.encodeBase64(auth.getBytes("UTF-8"))));
            } catch (UnsupportedEncodingException e) {
                Log.e(LOGCAT_NAME, "unable to perform basic auth, UTF-8 encoding not supported!");
                return null;
            }
        }

        HttpResponse response = null;

        try {
            response = useToken ? getHttpClient().execute(httppost) : getHttpClientNoToken().execute(httppost);
        } catch (ClientProtocolException e) {
            log("caught ClientProtocolException performing POST " + httppost.getURI(), e);
            return null;
        } catch (IOException e) {
            log("caught IOException performing POST " + httppost.getURI(), e);
            return null;
        }

        sync_trace("POST " + httppost.getURI() + " returned " + response.getStatusLine().getStatusCode() + ' '
                + response.getStatusLine().getReasonPhrase());
        return response;
    }

    private HttpResponse performDELETE(String method, List<NameValuePair> httpParams) {
        HttpDelete httpdelete;

        if (httpParams == null || httpParams.isEmpty()) {
            httpdelete = new HttpDelete(catchBaseUrl + method);
        } else {
            httpdelete = new HttpDelete(catchBaseUrl + method + '?' + URLEncodedUtils.format(httpParams, "UTF-8"));
        }

        HttpResponse response = null;

        try {
            response = getHttpClient().execute(httpdelete);
        } catch (ClientProtocolException e) {
            log("caught ClientProtocolException performing DELETE " + httpdelete.getURI(), e);
            return null;
        } catch (IOException e) {
            log("caught IOException performing DELETE " + httpdelete.getURI(), e);
            return null;
        }

        sync_trace("DELETE " + httpdelete.getURI() + " returned " + response.getStatusLine().getStatusCode() + ' '
                + response.getStatusLine().getReasonPhrase());
        return response;
    }

    private boolean isResponseOK(HttpResponse response) {
        return (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK);
    }

    private boolean isResponseUnauthorized(HttpResponse response) {
        return (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED);
    }

    private boolean isResponseNotFound(HttpResponse response) {
        return (response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND);
    }

    private boolean isResponseBadRequest(HttpResponse response) {
        return (response.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST);
    }

    private boolean isResponsePaymentRequired(HttpResponse response) {
        return (response.getStatusLine().getStatusCode() == HttpStatus.SC_PAYMENT_REQUIRED);
    }

    private boolean isResponseUnsupportedMedia(HttpResponse response) {
        return (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE);
    }

    private boolean isResponseServerError(HttpResponse response) {
        return (response.getStatusLine().getStatusCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR);
    }

    private void consumeResponse(HttpResponse response) {
        if (response != null && response.getEntity() != null) {
            try {
                response.getEntity().consumeContent();
            } catch (IOException e) {
                log("caught an IOException attempting to consume the content of an HttpResponse", e);
            }
        }
    }

    private long parse3339(String time) {
        if (time == null || time.length() == 0) {
            return 0;
        }

        if (timestamper != null) {
            try {
                timestamper.parse3339(time);
            } catch (TimeFormatException e) {
                log("caught a TimeFormatException parsing timestamp: \"" + time + '"', e);
                return 0;
            }

            return timestamper.normalize(false);
        } else {
            Date timestamp = new Date();

            if (rfc3339 == null) {
                rfc3339 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
                rfc3339.setTimeZone(TimeZone.getTimeZone("GMT+0"));
                rfc3339.setLenient(true);
            }

            try {
                timestamp = rfc3339.parse(time);
            } catch (ParseException e) {
                log("caught a ParseException parsing timestamp: \"" + time + '"', e);
                return 0;
            }

            return timestamp.getTime();
        }
    }

    // from Translate.java
    /*
     * Copyright (C) 2008 Google Inc.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    private static String istreamToString(InputStream inputStream) throws IOException {
        StringBuilder outputBuilder = new StringBuilder();
        String string;

        if (inputStream != null) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, ENCODING));

            while (null != (string = reader.readLine())) {
                outputBuilder.append(string).append('\n');
            }

            reader.close();
        }

        return outputBuilder.toString();
    }

    // Helper function to log error messages & exceptions.
    private void log(String msg, Exception e) {
        Log.e(LOGCAT_NAME, msg, e);
    }

    // Helper function to log tracing messages.
    private void sync_trace(String msg) {
        if (loggingEnabled) {
            Log.d(LOGCAT_NAME, msg);
        }
    }

    /**
     * Helper function to turn return codes into readable text.
     * 
     * @param code The code to convert.
     * @return The String representation of the code.
     */
    public static String resultToString(int code) {
        switch (code) {
        case RESULT_ERROR:
            return "RESULT_ERROR";
        case RESULT_OK:
            return "RESULT_OK";
        case RESULT_UNAUTHORIZED:
            return "RESULT_UNAUTHORIZED";
        case RESULT_ERROR_PENDING:
            return "RESULT_ERROR_PENDING";
        case RESULT_ERROR_PARSER:
            return "RESULT_ERROR_PARSER";
        case RESULT_ERROR_RESPONSE:
            return "RESULT_ERROR_RESPONSE";
        case RESULT_AVAILABLE:
            return "RESULT_AVAILABLE";
        case RESULT_UNAVAILABLE:
            return "RESULT_UNAVAILABLE";
        case RESULT_BAD_REQUEST:
            return "RESULT_BAD_REQUEST";
        case RESULT_NOT_FOUND:
            return "RESULT_NOT_FOUND";
        case RESULT_OVER_QUOTA:
            return "RESULT_OVER_QUOTA";
        case RESULT_SERVER_ERROR:
            return "RESULT_SERVER_ERROR";
        case RESULT_UNSUPPORTED_MEDIA:
            return "RESULT_UNSUPPORTED_MEDIA";
        default:
            return "UNKNOWN (" + code + ")";
        }
    }
}