Squashed commit of the following:
Merged with batch, change to FORMAT_BATCH
commit 4a6f359119771b7ac785aafcba757d049c82e490
Merge: e41dea4... 10063c1...
Author: fmantek <fmantek@google.com>
Date:   Fri Jul 3 14:58:11 2009 +0200

    Merge branch 'master' of ssh://android-git.corp.google.com:29418/platform/frameworks/base into GDataClient2
    Implemented batch on the android client

commit e41dea4ac27959c8d9f906afbc2e5c4b09e007e0
Author: fmantek <fmantek@google.com>
Date:   Wed Jul 1 15:36:01 2009 +0200

    New GDAtaClientInterface

    commit d70a4b7d088df217360825a79696e8dae669a8b2
    Author: fmantek <fmantek@google.com>
    Date:   Wed Jul 1 15:28:50 2009 +0200

        Removeg 2 unneeded files

    commit f5a25cbd43a14412a4cbd51bbd733ddb7d0e06ba
    Author: fmantek <fmantek@google.com>
    Date:   Wed Jul 1 15:26:07 2009 +0200

        Added gdata2 package

        commit dfd0d875a2eac69511419df375820f0b437ba398
        Author: fmantek <fmantek@google.com>
        Date:   Thu Jun 25 13:00:05 2009 +0200

            New Android Client to use Gdata2 package and support etags
diff --git a/core/java/com/google/android/gdata2/client/AndroidGDataClient.java b/core/java/com/google/android/gdata2/client/AndroidGDataClient.java
new file mode 100644
index 0000000..7ac44c9
--- /dev/null
+++ b/core/java/com/google/android/gdata2/client/AndroidGDataClient.java
@@ -0,0 +1,584 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.android.gdata2.client;
+
+import com.google.android.net.GoogleHttpClient;
+import com.google.wireless.gdata2.client.GDataClient;
+import com.google.wireless.gdata2.client.HttpException;
+import com.google.wireless.gdata2.client.QueryParams;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.serializer.GDataSerializer;
+import com.google.android.gdata2.client.QueryParamsImpl;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.entity.AbstractHttpEntity;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.http.AndroidHttpClient;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.io.BufferedInputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+
+/**
+ * Implementation of a GDataClient using GoogleHttpClient to make HTTP
+ * requests.  Always issues GETs and POSTs, using the X-HTTP-Method-Override
+ * header when a PUT or DELETE is desired, to avoid issues with firewalls, etc.,
+ * that do not allow methods other than GET or POST.
+ */
+public class AndroidGDataClient implements GDataClient {
+
+    private static final String TAG = "GDataClient";
+    private static final boolean DEBUG = false;
+    private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
+
+    private static final String X_HTTP_METHOD_OVERRIDE =
+        "X-HTTP-Method-Override";
+
+    private static final String DEFAULT_USER_AGENT_APP_VERSION = "Android-GData/1.2";
+
+    private static final int MAX_REDIRECTS = 10;
+    private static String DEFAULT_GDATA_VERSION = "2.0";
+
+
+    private String mGDataVersion; 
+    private final GoogleHttpClient mHttpClient;
+    private ContentResolver mResolver;
+
+    /**
+     * Interface for creating HTTP requests.  Used by
+     * {@link AndroidGDataClient#createAndExecuteMethod}, since HttpUriRequest does not allow for
+     * changing the URI after creation, e.g., when you want to follow a redirect.
+     */
+    private interface HttpRequestCreator {
+        HttpUriRequest createRequest(URI uri);
+    }
+
+    private static class GetRequestCreator implements HttpRequestCreator {
+        public GetRequestCreator() {
+        }
+
+        public HttpUriRequest createRequest(URI uri) {
+            HttpGet get = new HttpGet(uri);
+            return get;
+        }
+    }
+
+    private static class PostRequestCreator implements HttpRequestCreator {
+        private final String mMethodOverride;
+        private final HttpEntity mEntity;
+        public PostRequestCreator(String methodOverride, HttpEntity entity) {
+            mMethodOverride = methodOverride;
+            mEntity = entity;
+        }
+
+        public HttpUriRequest createRequest(URI uri) {
+            HttpPost post = new HttpPost(uri);
+            if (mMethodOverride != null) {
+                post.addHeader(X_HTTP_METHOD_OVERRIDE, mMethodOverride);
+            }
+            post.setEntity(mEntity);
+            return post;
+        }
+    }
+
+    // MAJOR TODO: make this work across redirects (if we can reset the InputStream).
+    // OR, read the bits into a local buffer (yuck, the media could be large).
+    private static class MediaPutRequestCreator implements HttpRequestCreator {
+        private final InputStream mMediaInputStream;
+        private final String mContentType;
+        public MediaPutRequestCreator(InputStream mediaInputStream, String contentType) {
+            mMediaInputStream = mediaInputStream;
+            mContentType = contentType;
+        }
+
+        public HttpUriRequest createRequest(URI uri) {
+            HttpPost post = new HttpPost(uri);
+            post.addHeader(X_HTTP_METHOD_OVERRIDE, "PUT");
+            // mMediaInputStream.reset();
+            InputStreamEntity entity = new InputStreamEntity(mMediaInputStream,
+                    -1 /* read until EOF */);
+            entity.setContentType(mContentType);
+            post.setEntity(entity);
+            return post;
+        }
+    }
+
+   
+    /**
+     * Creates a new AndroidGDataClient.
+     * 
+     * @param context The ContentResolver to get URL rewriting rules from
+     * through the Android proxy server, using null to indicate not using proxy.
+     * The context will also be used by GoogleHttpClient for configuration of 
+     * SSL session persistence.
+     */
+    public AndroidGDataClient(Context context) {
+       this(context, DEFAULT_USER_AGENT_APP_VERSION);
+    }
+
+    /**
+     * Creates a new AndroidGDataClient.
+     *
+     * @param context The ContentResolver to get URL rewriting rules from
+     * through the Android proxy server, using null to indicate not using proxy.
+     * The context will also be used by GoogleHttpClient for configuration of
+     * SSL session persistence.
+     * @param appAndVersion The application name and version to be used as the basis of the
+     * User-Agent.  e.g., Android-GData/1.5.0.
+     */
+    public AndroidGDataClient(Context context, String appAndVersion) {
+        this(context, appAndVersion, DEFAULT_GDATA_VERSION);
+    }
+
+    /**
+     * Creates a new AndroidGDataClient.
+     *
+     * @param context The ContentResolver to get URL rewriting rules from
+     * through the Android proxy server, using null to indicate not using proxy.
+     * The context will also be used by GoogleHttpClient for configuration of
+     * SSL session persistence.
+     * @param appAndVersion The application name and version to be used as the basis of the
+     * User-Agent.  e.g., Android-GData/1.5.0. 
+     * @param gdataVersion The gdata service version that should be 
+     * used, e.g. "2.0" 
+     *  
+     */
+    public AndroidGDataClient(Context context, String appAndVersion, String gdataVersion) {
+        mHttpClient = new GoogleHttpClient(context, appAndVersion,
+                true /* gzip capable */);
+        mHttpClient.enableCurlLogging(TAG, Log.VERBOSE);
+        mResolver = context.getContentResolver();
+        mGDataVersion = gdataVersion;
+    }
+
+
+    public void close() {
+        mHttpClient.close();
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see GDataClient#encodeUri(java.lang.String)
+     */
+    public String encodeUri(String uri) {
+        String encodedUri;
+        try {
+            encodedUri = URLEncoder.encode(uri, "UTF-8");
+        } catch (UnsupportedEncodingException uee) {
+            // should not happen.
+            Log.e("JakartaGDataClient",
+                  "UTF-8 not supported -- should not happen.  "
+                  + "Using default encoding.", uee);
+            encodedUri = URLEncoder.encode(uri);
+        }
+        return encodedUri;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.google.wireless.gdata.client.GDataClient#createQueryParams()
+     */
+    public QueryParams createQueryParams() {
+        return new QueryParamsImpl();
+    }
+
+    // follows redirects
+    private InputStream createAndExecuteMethod(HttpRequestCreator creator,
+                                               String uriString,
+                                               String authToken,
+                                               String eTag,
+                                               String protocolVersion)
+        throws HttpException, IOException {
+
+        HttpResponse response = null;
+        int status = 500;
+        int redirectsLeft = MAX_REDIRECTS;
+
+        URI uri;
+        try {
+            uri = new URI(uriString);
+        } catch (URISyntaxException use) {
+            Log.w(TAG, "Unable to parse " + uriString + " as URI.", use);
+            throw new IOException("Unable to parse " + uriString + " as URI: "
+                    + use.getMessage());
+        }
+
+        // we follow redirects ourselves, since we want to follow redirects even on POSTs, which
+        // the HTTP library does not do.  following redirects ourselves also allows us to log
+        // the redirects using our own logging.
+        while (redirectsLeft > 0) {
+
+            HttpUriRequest request = creator.createRequest(uri);
+
+            AndroidHttpClient.modifyRequestToAcceptGzipResponse(request);
+            // only add the auth token if not null (to allow for GData feeds that do not require
+            // authentication.)
+            if (!TextUtils.isEmpty(authToken)) {
+                request.addHeader("Authorization", "GoogleLogin auth=" + authToken);
+            }
+
+            // while by default we have a 2.0 in this variable, it is possible to construct
+            // a client that has an empty version field, to work with 1.0 services. 
+            if (!TextUtils.isEmpty(mGDataVersion)) {
+                request.addHeader("GData-Version", mGDataVersion);
+            }
+
+            // if we have a passed down eTag value, we need to add several headers
+            if (!TextUtils.isEmpty(eTag)) { 
+                String method = request.getMethod();
+                if ("GET".equals(method)) {
+                    // add the none match header, if the resource is not changed
+                    // this request will result in a 304 now.
+                    request.addHeader("If-None-Match", eTag);
+                } else if ("DELETE".equals(method) 
+                           || "PUT".equals(method)) {
+                    // now we send an if-match, but only if the passed in eTag is a strong eTag
+                    // as this only makes sense for a strong eTag
+                     if (eTag.startsWith("W/") == false) {
+                         request.addHeader("If-Match", eTag);
+                     }
+                }
+            }
+
+
+            if (LOCAL_LOGV) {
+                for (Header h : request.getAllHeaders()) {
+                    Log.v(TAG, h.getName() + ": " + h.getValue());
+                }
+            }
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Executing " + request.getRequestLine().toString());
+            }
+
+            response = null;
+
+            try {
+                response = mHttpClient.execute(request);
+            } catch (IOException ioe) {
+                Log.w(TAG, "Unable to execute HTTP request." + ioe);
+                throw ioe;
+            }
+
+            StatusLine statusLine = response.getStatusLine();
+            if (statusLine == null) {
+                Log.w(TAG, "StatusLine is null.");
+                throw new NullPointerException("StatusLine is null -- should not happen.");
+            }
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, response.getStatusLine().toString());
+                for (Header h : response.getAllHeaders()) {
+                    Log.d(TAG, h.getName() + ": " + h.getValue());
+                }
+            }
+            status = statusLine.getStatusCode();
+
+            HttpEntity entity = response.getEntity();
+
+            if ((status >= 200) && (status < 300) && entity != null) {
+                InputStream in = AndroidHttpClient.getUngzippedContent(entity);
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    in = logInputStreamContents(in);
+                }
+                return in;
+            }
+
+            // TODO: handle 301, 307?
+            // TODO: let the http client handle the redirects, if we can be sure we'll never get a
+            // redirect on POST.
+            if (status == 302) {
+                // consume the content, so the connection can be closed.
+                entity.consumeContent();
+                Header location = response.getFirstHeader("Location");
+                if (location == null) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Redirect requested but no Location "
+                                + "specified.");
+                    }
+                    break;
+                }
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Following redirect to " + location.getValue());
+                }
+                try {
+                    uri = new URI(location.getValue());
+                } catch (URISyntaxException use) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Unable to parse " + location.getValue() + " as URI.", use);
+                        throw new IOException("Unable to parse " + location.getValue()
+                                + " as URI.");
+                    }
+                    break;
+                }
+                --redirectsLeft;
+            } else {
+                break;
+            }
+        }
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "Received " + status + " status code.");
+        }
+        String errorMessage = null;
+        HttpEntity entity = response.getEntity();
+        try {
+            if (response != null && entity != null) {
+                InputStream in = AndroidHttpClient.getUngzippedContent(entity);
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                byte[] buf = new byte[8192];
+                int bytesRead = -1;
+                while ((bytesRead = in.read(buf)) != -1) {
+                    baos.write(buf, 0, bytesRead);
+                }
+                // TODO: use appropriate encoding, picked up from Content-Type.
+                errorMessage = new String(baos.toByteArray());
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, errorMessage);
+                }
+            }
+        } finally {
+            if (entity != null) {
+                entity.consumeContent();
+            }
+        }
+        String exceptionMessage = "Received " + status + " status code";
+        if (errorMessage != null) {
+            exceptionMessage += (": " + errorMessage);
+        }
+        throw new HttpException(exceptionMessage, status, null /* InputStream */);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String)
+     */
+    public InputStream getFeedAsStream(String feedUrl,
+                                       String authToken,
+                                       String eTag,
+                                       String protocolVersion)
+        throws HttpException, IOException {
+
+        InputStream in = createAndExecuteMethod(new GetRequestCreator(), feedUrl, authToken, eTag, protocolVersion);
+        if (in != null) {
+            return in;
+        }
+        throw new IOException("Unable to access feed.");
+    }
+
+    /**
+     * Log the contents of the input stream.
+     * The original input stream is consumed, so the caller must use the
+     * BufferedInputStream that is returned.
+     * @param in InputStream
+     * @return replacement input stream for caller to use
+     * @throws IOException
+     */
+    private InputStream logInputStreamContents(InputStream in) throws IOException {
+        if (in == null) {
+            return in;
+        }
+        // bufferSize is the (arbitrary) maximum amount to log.
+        // The original InputStream is wrapped in a
+        // BufferedInputStream with a 16K buffer.  This lets
+        // us read up to 16K, write it to the log, and then
+        // reset the stream so the the original client can
+        // then read the data.  The BufferedInputStream
+        // provides the mark and reset support, even when
+        // the original InputStream does not.
+        final int bufferSize = 16384;
+        BufferedInputStream bin = new BufferedInputStream(in, bufferSize);
+        bin.mark(bufferSize);
+        int wanted = bufferSize;
+        int totalReceived = 0;
+        byte buf[] = new byte[wanted];
+        while (wanted > 0) {
+            int got = bin.read(buf, totalReceived, wanted);
+            if (got <= 0) break; // EOF
+            wanted -= got;
+            totalReceived += got;
+        }
+        Log.d(TAG, new String(buf, 0, totalReceived, "UTF-8"));
+        bin.reset();
+        return bin;
+    }
+
+    public InputStream getMediaEntryAsStream(String mediaEntryUrl, String authToken, String eTag, String protocolVersion)
+            throws HttpException, IOException {
+
+        InputStream in = createAndExecuteMethod(new GetRequestCreator(), mediaEntryUrl, authToken, eTag, protocolVersion);
+
+        if (in != null) {
+            return in;
+        }
+        throw new IOException("Unable to access media entry.");
+    }
+
+    /* (non-Javadoc)
+    * @see GDataClient#createEntry
+    */
+    public InputStream createEntry(String feedUrl,
+                                   String authToken,
+                                   String protocolVersion, 
+                                   GDataSerializer entry)
+        throws HttpException, IOException {
+
+        HttpEntity entity = createEntityForEntry(entry, GDataSerializer.FORMAT_CREATE);
+        InputStream in = createAndExecuteMethod(
+                new PostRequestCreator(null /* override */, entity),
+                feedUrl,
+                authToken,
+                null,
+                protocolVersion);
+        if (in != null) {
+            return in;
+        }
+        throw new IOException("Unable to create entry.");
+    }
+
+    /* (non-Javadoc)
+     * @see GDataClient#updateEntry
+     */
+    public InputStream updateEntry(String editUri,
+                                   String authToken,
+                                   String eTag,
+                                   String protocolVersion, 
+                                   GDataSerializer entry)
+        throws HttpException, IOException {
+        HttpEntity entity = createEntityForEntry(entry, GDataSerializer.FORMAT_UPDATE);
+        InputStream in = createAndExecuteMethod(
+                new PostRequestCreator("PUT", entity),
+                editUri,
+                authToken,
+                eTag,
+                protocolVersion);
+        if (in != null) {
+            return in;
+        }
+        throw new IOException("Unable to update entry.");
+    }
+
+    /* (non-Javadoc)
+     * @see GDataClient#deleteEntry
+     */
+    public void deleteEntry(String editUri, String authToken, String eTag)
+        throws HttpException, IOException {
+        if (StringUtils.isEmpty(editUri)) {
+            throw new IllegalArgumentException(
+                    "you must specify an non-empty edit url");
+        }
+        InputStream in =
+            createAndExecuteMethod(
+                    new PostRequestCreator("DELETE", null /* entity */),
+                    editUri,
+                    authToken,
+                    eTag,
+                    null /* protocolVersion, not required for a delete */);
+        if (in == null) {
+            throw new IOException("Unable to delete entry.");
+        }
+        try {
+            in.close();
+        } catch (IOException ioe) {
+            // ignore
+        }
+    }
+
+    public InputStream updateMediaEntry(String editUri, String authToken, String eTag,
+            String protocolVersion, InputStream mediaEntryInputStream, String contentType)
+        throws HttpException, IOException {
+        InputStream in = createAndExecuteMethod(
+                new MediaPutRequestCreator(mediaEntryInputStream, contentType),
+                editUri,
+                authToken,
+                eTag,
+                protocolVersion);
+        if (in != null) {
+            return in;
+        }
+        throw new IOException("Unable to write media entry.");
+    }
+
+    private HttpEntity createEntityForEntry(GDataSerializer entry, int format) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            entry.serialize(baos, format);
+        } catch (IOException ioe) {
+            Log.e(TAG, "Unable to serialize entry.", ioe);
+            throw ioe;
+        } catch (ParseException pe) {
+            Log.e(TAG, "Unable to serialize entry.", pe);
+            throw new IOException("Unable to serialize entry: " + pe.getMessage());
+        }
+
+        byte[] entryBytes = baos.toByteArray();
+
+        if (entryBytes != null && Log.isLoggable(TAG, Log.DEBUG)) {
+            try {
+                Log.d(TAG, "Serialized entry: " + new String(entryBytes, "UTF-8"));
+            } catch (UnsupportedEncodingException uee) {
+                // should not happen
+                throw new IllegalStateException("UTF-8 should be supported!",
+                        uee);
+            }
+        }
+
+        AbstractHttpEntity entity = AndroidHttpClient.getCompressedEntity(entryBytes, mResolver);
+        entity.setContentType(entry.getContentType());
+        return entity;
+    }
+
+    /**
+     * Connects to a GData server (specified by the batchUrl) and submits a
+     * batch for processing.  The response from the server is returned as an
+     * {@link InputStream}.  The caller is responsible for calling
+     * {@link InputStream#close()} on the returned {@link InputStream}.
+     *
+     * @param batchUrl The batch url to which the batch is submitted.
+     * @param authToken the authentication token that should be used when
+     * submitting the batch.
+     * @param protocolVersion The version of the protocol that 
+     *                 should be used for this request.
+     * @param batch The batch of entries to submit.
+     * @throws IOException Thrown if an io error occurs while communicating with
+     * the service.
+     * @throws HttpException if the service returns an error response.
+     */
+    public InputStream submitBatch(String batchUrl,
+       String authToken,
+       String protocolVersion,
+       GDataSerializer batch)
+       throws HttpException, IOException
+    {
+        HttpEntity entity = createEntityForEntry(batch, GDataSerializer.FORMAT_BATCH);
+        InputStream in = createAndExecuteMethod(
+                new PostRequestCreator("POST", entity),
+                batchUrl,
+                authToken,
+                null,
+                protocolVersion);
+        if (in != null) {
+            return in;
+        }
+        throw new IOException("Unable to process batch request.");
+    }
+}   
+
diff --git a/core/java/com/google/android/gdata2/client/AndroidXmlParserFactory.java b/core/java/com/google/android/gdata2/client/AndroidXmlParserFactory.java
new file mode 100644
index 0000000..f097706
--- /dev/null
+++ b/core/java/com/google/android/gdata2/client/AndroidXmlParserFactory.java
@@ -0,0 +1,31 @@
+package com.google.android.gdata2.client;
+
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import android.util.Xml;
+
+/**
+ * XmlParserFactory for the Android platform.
+ */
+public class AndroidXmlParserFactory implements XmlParserFactory {
+
+    /*
+     * (non-javadoc)
+     * @see XmlParserFactory#createParser
+     */
+    public XmlPullParser createParser() throws XmlPullParserException {
+        return Xml.newPullParser();
+    }
+
+    /*
+     * (non-javadoc)
+     * @see XmlParserFactory#createSerializer
+     */
+    public XmlSerializer createSerializer() throws XmlPullParserException {
+        return Xml.newSerializer();
+    }
+}
diff --git a/core/java/com/google/android/gdata2/client/QueryParamsImpl.java b/core/java/com/google/android/gdata2/client/QueryParamsImpl.java
new file mode 100644
index 0000000..a26f4ce
--- /dev/null
+++ b/core/java/com/google/android/gdata2/client/QueryParamsImpl.java
@@ -0,0 +1,99 @@
+package com.google.android.gdata2.client;
+import com.google.wireless.gdata2.client.QueryParams;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Simple implementation of the QueryParams interface.
+ */
+// TODO: deal with categories
+public class QueryParamsImpl extends QueryParams {
+
+    private final Map<String,String> mParams = new HashMap<String,String>();
+
+    /**
+     * Creates a new empty QueryParamsImpl.
+     */
+    public QueryParamsImpl() {
+    }
+
+    @Override
+    public void clear() {
+        setEntryId(null);
+        mParams.clear();
+    }
+
+    @Override
+    public String generateQueryUrl(String feedUrl) {
+
+        if (TextUtils.isEmpty(getEntryId()) &&
+            mParams.isEmpty()) {
+            // nothing to do
+            return feedUrl;
+        }
+
+        // handle entry IDs
+        if (!TextUtils.isEmpty(getEntryId())) {
+            if (!mParams.isEmpty()) {
+                throw new IllegalStateException("Cannot set both an entry ID "
+                        + "and other query paramters.");
+            }
+            return feedUrl + '/' + getEntryId();
+        }
+
+        // otherwise, append the querystring params.
+        StringBuilder sb = new StringBuilder();
+        sb.append(feedUrl);
+        Set<String> params = mParams.keySet();
+        boolean first = true;
+        if (feedUrl.contains("?")) {
+            first = false;
+        } else {
+            sb.append('?');
+        }
+        for (String param : params) {
+            if (first) {
+                first = false;
+            } else {
+                sb.append('&');
+            }
+            sb.append(param);
+            sb.append('=');
+            String value = mParams.get(param);
+            String encodedValue = null;
+
+            try {
+                encodedValue = URLEncoder.encode(value, "UTF-8");
+            } catch (UnsupportedEncodingException uee) {
+                // should not happen.
+                Log.w("QueryParamsImpl",
+                      "UTF-8 not supported -- should not happen.  "
+                      + "Using default encoding.", uee);
+                encodedValue = URLEncoder.encode(value);
+            }
+            sb.append(encodedValue);
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public String getParamValue(String param) {
+        if (!(mParams.containsKey(param))) {
+            return null;
+        }
+        return mParams.get(param);
+    }
+
+    @Override
+    public void setParamValue(String param, String value) {
+        mParams.put(param, value);
+    }
+
+}