com.trk.aboutme.facebook.Request.java Source code

Java tutorial

Introduction

Here is the source code for com.trk.aboutme.facebook.Request.java

Source

/**
 * Copyright 2010-present Facebook.
 *
 * 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.trk.aboutme.facebook;

import android.graphics.Bitmap;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Pair;
import com.trk.aboutme.facebook.internal.ServerProtocol;
import com.trk.aboutme.facebook.model.*;
import com.trk.aboutme.facebook.internal.Logger;
import com.trk.aboutme.facebook.internal.Utility;
import com.trk.aboutme.facebook.internal.Validate;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;

/**
 * A single request to be sent to the Facebook Platform through either the <a
 * href="https://developers.facebook.com/docs/reference/api/">Graph API</a> or <a
 * href="https://developers.facebook.com/docs/reference/rest/">REST API</a>. The Request class provides functionality
 * relating to serializing and deserializing requests and responses, making calls in batches (with a single round-trip
 * to the service) and making calls asynchronously.
 *
 * The particular service endpoint that a request targets is determined by either a graph path (see the
 * {@link #setGraphPath(String) setGraphPath} method) or a REST method name (see the {@link #setRestMethod(String)
 * setRestMethod} method); a single request may not target both.
 *
 * A Request can be executed either anonymously or representing an authenticated user. In the former case, no Session
 * needs to be specified, while in the latter, a Session that is in an opened state must be provided. If requests are
 * executed in a batch, a Facebook application ID must be associated with the batch, either by supplying a Session for
 * at least one of the requests in the batch (the first one found in the batch will be used) or by calling the
 * {@link #setDefaultBatchApplicationId(String) setDefaultBatchApplicationId} method.
 *
 * After completion of a request, its Session, if any, will be checked to determine if its Facebook access token needs
 * to be extended; if so, a request to extend it will be issued in the background.
 */
public class Request {
    /**
     * The maximum number of requests that can be submitted in a single batch. This limit is enforced on the service
     * side by the Facebook platform, not by the Request class.
     */
    public static final int MAXIMUM_BATCH_SIZE = 50;

    private static final String ME = "me";
    private static final String MY_FRIENDS = "me/friends";
    private static final String MY_PHOTOS = "me/photos";
    private static final String MY_VIDEOS = "me/videos";
    private static final String SEARCH = "search";
    private static final String MY_FEED = "me/feed";

    private static final String USER_AGENT_BASE = "FBAndroidSDK";
    private static final String USER_AGENT_HEADER = "User-Agent";
    private static final String CONTENT_TYPE_HEADER = "Content-Type";

    // Parameter names/values
    private static final String PICTURE_PARAM = "picture";
    private static final String FORMAT_PARAM = "format";
    private static final String FORMAT_JSON = "json";
    private static final String SDK_PARAM = "sdk";
    private static final String SDK_ANDROID = "android";
    private static final String ACCESS_TOKEN_PARAM = "access_token";
    private static final String BATCH_ENTRY_NAME_PARAM = "name";
    private static final String BATCH_ENTRY_OMIT_RESPONSE_ON_SUCCESS_PARAM = "omit_response_on_success";
    private static final String BATCH_ENTRY_DEPENDS_ON_PARAM = "depends_on";
    private static final String BATCH_APP_ID_PARAM = "batch_app_id";
    private static final String BATCH_RELATIVE_URL_PARAM = "relative_url";
    private static final String BATCH_BODY_PARAM = "body";
    private static final String BATCH_METHOD_PARAM = "method";
    private static final String BATCH_PARAM = "batch";
    private static final String ATTACHMENT_FILENAME_PREFIX = "file";
    private static final String ATTACHED_FILES_PARAM = "attached_files";
    private static final String MIGRATION_BUNDLE_PARAM = "migration_bundle";
    private static final String ISO_8601_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ssZ";

    private static final String MIME_BOUNDARY = "3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f";

    private static String defaultBatchApplicationId;

    private Session session;
    private HttpMethod httpMethod;
    private String graphPath;
    private GraphObject graphObject;
    private String restMethod;
    private String batchEntryName;
    private String batchEntryDependsOn;
    private boolean batchEntryOmitResultOnSuccess = true;
    private Bundle parameters;
    private Callback callback;
    private String overriddenURL;

    /**
     * Constructs a request without a session, graph path, or any other parameters.
     */
    public Request() {
        this(null, null, null, null, null);
    }

    /**
     * Constructs a request with a Session to retrieve a particular graph path. A Session need not be provided, in which
     * case the request is sent without an access token and thus is not executed in the context of any particular user.
     * Only certain graph requests can be expected to succeed in this case. If a Session is provided, it must be in an
     * opened state or the request will fail.
     *
     * @param session
     *            the Session to use, or null
     * @param graphPath
     *            the graph path to retrieve
     */
    public Request(Session session, String graphPath) {
        this(session, graphPath, null, null, null);
    }

    /**
     * Constructs a request with a specific Session, graph path, parameters, and HTTP method. A Session need not be
     * provided, in which case the request is sent without an access token and thus is not executed in the context of
     * any particular user. Only certain graph requests can be expected to succeed in this case. If a Session is
     * provided, it must be in an opened state or the request will fail.
     *
     * Depending on the httpMethod parameter, the object at the graph path may be retrieved, created, or deleted.
     *
     * @param session
     *            the Session to use, or null
     * @param graphPath
     *            the graph path to retrieve, create, or delete
     * @param parameters
     *            additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers,
     *            Bitmaps, Dates, or Byte arrays.
     * @param httpMethod
     *            the {@link HttpMethod} to use for the request, or null for default (HttpMethod.GET)
     */
    public Request(Session session, String graphPath, Bundle parameters, HttpMethod httpMethod) {
        this(session, graphPath, parameters, httpMethod, null);
    }

    /**
     * Constructs a request with a specific Session, graph path, parameters, and HTTP method. A Session need not be
     * provided, in which case the request is sent without an access token and thus is not executed in the context of
     * any particular user. Only certain graph requests can be expected to succeed in this case. If a Session is
     * provided, it must be in an opened state or the request will fail.
     *
     * Depending on the httpMethod parameter, the object at the graph path may be retrieved, created, or deleted.
     *
     * @param session
     *            the Session to use, or null
     * @param graphPath
     *            the graph path to retrieve, create, or delete
     * @param parameters
     *            additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers,
     *            Bitmaps, Dates, or Byte arrays.
     * @param httpMethod
     *            the {@link HttpMethod} to use for the request, or null for default (HttpMethod.GET)
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     */
    public Request(Session session, String graphPath, Bundle parameters, HttpMethod httpMethod, Callback callback) {
        this.session = session;
        this.graphPath = graphPath;
        this.callback = callback;

        setHttpMethod(httpMethod);

        if (parameters != null) {
            this.parameters = new Bundle(parameters);
        } else {
            this.parameters = new Bundle();
        }

        if (!this.parameters.containsKey(MIGRATION_BUNDLE_PARAM)) {
            this.parameters.putString(MIGRATION_BUNDLE_PARAM, FacebookSdkVersion.MIGRATION_BUNDLE);
        }
    }

    Request(Session session, URL overriddenURL) {
        this.session = session;
        this.overriddenURL = overriddenURL.toString();

        setHttpMethod(HttpMethod.GET);

        this.parameters = new Bundle();
    }

    /**
     * Creates a new Request configured to post a GraphObject to a particular graph path, to either create or update the
     * object at that path.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param graphPath
     *            the graph path to retrieve, create, or delete
     * @param graphObject
     *            the GraphObject to create or update
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a Request that is ready to execute
     */
    public static Request newPostRequest(Session session, String graphPath, GraphObject graphObject,
            Callback callback) {
        Request request = new Request(session, graphPath, null, HttpMethod.POST, callback);
        request.setGraphObject(graphObject);
        return request;
    }

    /**
     * Creates a new Request configured to make a call to the Facebook REST API.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param restMethod
     *            the method in the Facebook REST API to execute
     * @param parameters
     *            additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers,
     *            Bitmaps, Dates, or Byte arrays.
     * @param httpMethod
     *            the HTTP method to use for the request; must be one of GET, POST, or DELETE
     * @return a Request that is ready to execute
     */
    public static Request newRestRequest(Session session, String restMethod, Bundle parameters,
            HttpMethod httpMethod) {
        Request request = new Request(session, null, parameters, httpMethod);
        request.setRestMethod(restMethod);
        return request;
    }

    /**
     * Creates a new Request configured to retrieve a user's own profile.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a Request that is ready to execute
     */
    public static Request newMeRequest(Session session, final GraphUserCallback callback) {
        Callback wrapper = new Callback() {
            @Override
            public void onCompleted(Response response) {
                if (callback != null) {
                    callback.onCompleted(response.getGraphObjectAs(GraphUser.class), response);
                }
            }
        };
        return new Request(session, ME, null, null, wrapper);
    }

    /**
     * Creates a new Request configured to retrieve a user's friend list.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a Request that is ready to execute
     */
    public static Request newMyFriendsRequest(Session session, final GraphUserListCallback callback) {
        Callback wrapper = new Callback() {
            @Override
            public void onCompleted(Response response) {
                if (callback != null) {
                    callback.onCompleted(typedListFromResponse(response, GraphUser.class), response);
                }
            }
        };
        return new Request(session, MY_FRIENDS, null, null, wrapper);
    }

    /**
     * Creates a new Request configured to upload a photo to the user's default photo album.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param image
     *            the image to upload
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a Request that is ready to execute
     */
    public static Request newUploadPhotoRequest(Session session, Bitmap image, Callback callback) {
        Bundle parameters = new Bundle(1);
        parameters.putParcelable(PICTURE_PARAM, image);

        return new Request(session, MY_PHOTOS, parameters, HttpMethod.POST, callback);
    }

    /**
     * Creates a new Request configured to upload a photo to the user's default photo album. The photo
     * will be read from the specified stream.
     *
     * @param session  the Session to use, or null; if non-null, the session must be in an opened state
     * @param file     the file containing the photo to upload
     * @param callback a callback that will be called when the request is completed to handle success or error conditions
     * @return a Request that is ready to execute
     */
    public static Request newUploadPhotoRequest(Session session, File file, Callback callback)
            throws FileNotFoundException {
        ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
        Bundle parameters = new Bundle(1);
        parameters.putParcelable(PICTURE_PARAM, descriptor);

        return new Request(session, MY_PHOTOS, parameters, HttpMethod.POST, callback);
    }

    /**
     * Creates a new Request configured to upload a photo to the user's default photo album. The photo
     * will be read from the specified file descriptor.
     *
     * @param session  the Session to use, or null; if non-null, the session must be in an opened state
     * @param file     the file to upload
     * @param callback a callback that will be called when the request is completed to handle success or error conditions
     * @return a Request that is ready to execute
     */
    public static Request newUploadVideoRequest(Session session, File file, Callback callback)
            throws FileNotFoundException {
        ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
        Bundle parameters = new Bundle(1);
        parameters.putParcelable(file.getName(), descriptor);

        return new Request(session, MY_VIDEOS, parameters, HttpMethod.POST, callback);
    }

    /**
     * Creates a new Request configured to retrieve a particular graph path.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param graphPath
     *            the graph path to retrieve
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a Request that is ready to execute
     */
    public static Request newGraphPathRequest(Session session, String graphPath, Callback callback) {
        return new Request(session, graphPath, null, null, callback);
    }

    /**
     * Creates a new Request that is configured to perform a search for places near a specified location via the Graph
     * API. At least one of location or searchText must be specified.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param location
     *            the location around which to search; only the latitude and longitude components of the location are
     *            meaningful
     * @param radiusInMeters
     *            the radius around the location to search, specified in meters; this is ignored if
     *            no location is specified
     * @param resultsLimit
     *            the maximum number of results to return
     * @param searchText
     *            optional text to search for as part of the name or type of an object
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a Request that is ready to execute
     *
     * @throws FacebookException If neither location nor searchText is specified
     */
    public static Request newPlacesSearchRequest(Session session, Location location, int radiusInMeters,
            int resultsLimit, String searchText, final GraphPlaceListCallback callback) {
        if (location == null && Utility.isNullOrEmpty(searchText)) {
            throw new FacebookException("Either location or searchText must be specified.");
        }

        Bundle parameters = new Bundle(5);
        parameters.putString("type", "place");
        parameters.putInt("limit", resultsLimit);
        if (location != null) {
            parameters.putString("center",
                    String.format(Locale.US, "%f,%f", location.getLatitude(), location.getLongitude()));
            parameters.putInt("distance", radiusInMeters);
        }
        if (!Utility.isNullOrEmpty(searchText)) {
            parameters.putString("q", searchText);
        }

        Callback wrapper = new Callback() {
            @Override
            public void onCompleted(Response response) {
                if (callback != null) {
                    callback.onCompleted(typedListFromResponse(response, GraphPlace.class), response);
                }
            }
        };

        return new Request(session, SEARCH, parameters, HttpMethod.GET, wrapper);
    }

    /**
     * Creates a new Request configured to post a status update to a user's feed.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param message
     *            the text of the status update
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a Request that is ready to execute
     */
    public static Request newStatusUpdateRequest(Session session, String message, Callback callback) {
        Bundle parameters = new Bundle();
        parameters.putString("message", message);

        return new Request(session, MY_FEED, parameters, HttpMethod.POST, callback);
    }

    /**
     * Returns the GraphObject, if any, associated with this request.
     *
     * @return the GraphObject associated with this requeset, or null if there is none
     */
    public final GraphObject getGraphObject() {
        return this.graphObject;
    }

    /**
     * Sets the GraphObject associated with this request. This is meaningful only for POST requests.
     *
     * @param graphObject
     *            the GraphObject to upload along with this request
     */
    public final void setGraphObject(GraphObject graphObject) {
        this.graphObject = graphObject;
    }

    /**
     * Returns the graph path of this request, if any.
     *
     * @return the graph path of this request, or null if there is none
     */
    public final String getGraphPath() {
        return this.graphPath;
    }

    /**
     * Sets the graph path of this request. A graph path may not be set if a REST method has been specified.
     *
     * @param graphPath
     *            the graph path for this request
     */
    public final void setGraphPath(String graphPath) {
        this.graphPath = graphPath;
    }

    /**
     * Returns the {@link HttpMethod} to use for this request.
     *
     * @return the HttpMethod
     */
    public final HttpMethod getHttpMethod() {
        return this.httpMethod;
    }

    /**
     * Sets the {@link HttpMethod} to use for this request.
     *
     * @param httpMethod
     *            the HttpMethod, or null for the default (HttpMethod.GET).
     */
    public final void setHttpMethod(HttpMethod httpMethod) {
        if (overriddenURL != null && httpMethod != HttpMethod.GET) {
            throw new FacebookException("Can't change HTTP method on request with overridden URL.");
        }
        this.httpMethod = (httpMethod != null) ? httpMethod : HttpMethod.GET;
    }

    /**
     * Returns the parameters for this request.
     *
     * @return the parameters
     */
    public final Bundle getParameters() {
        return this.parameters;
    }

    /**
     * Sets the parameters for this request.
     *
     * @param parameters
     *            the parameters
     */
    public final void setParameters(Bundle parameters) {
        this.parameters = parameters;
    }

    /**
     * Returns the REST method to call for this request.
     *
     * @return the REST method
     */
    public final String getRestMethod() {
        return this.restMethod;
    }

    /**
     * Sets the REST method to call for this request. A REST method may not be set if a graph path has been specified.
     *
     * @param restMethod
     *            the REST method to call
     */
    public final void setRestMethod(String restMethod) {
        this.restMethod = restMethod;
    }

    /**
     * Returns the Session associated with this request.
     *
     * @return the Session associated with this request, or null if none has been specified
     */
    public final Session getSession() {
        return this.session;
    }

    /**
     * Sets the Session to use for this request. The Session does not need to be opened at the time it is specified, but
     * it must be opened by the time the request is executed.
     *
     * @param session
     *            the Session to use for this request
     */
    public final void setSession(Session session) {
        this.session = session;
    }

    /**
     * Returns the name of this request's entry in a batched request.
     *
     * @return the name of this request's batch entry, or null if none has been specified
     */
    public final String getBatchEntryName() {
        return this.batchEntryName;
    }

    /**
     * Sets the name of this request's entry in a batched request. This value is only used if this request is submitted
     * as part of a batched request. It can be used to specified dependencies between requests. See <a
     * href="https://developers.facebook.com/docs/reference/api/batch/">Batch Requests</a> in the Graph API
     * documentation for more details.
     *
     * @param batchEntryName
     *            the name of this request's entry in a batched request, which must be unique within a particular batch
     *            of requests
     */
    public final void setBatchEntryName(String batchEntryName) {
        this.batchEntryName = batchEntryName;
    }

    /**
     * Returns the name of the request that this request entry explicitly depends on in a batched request.
     *
     * @return the name of this request's dependency, or null if none has been specified
     */
    public final String getBatchEntryDependsOn() {
        return this.batchEntryDependsOn;
    }

    /**
     * Sets the name of the request entry that this request explicitly depends on in a batched request. This value is
     * only used if this request is submitted as part of a batched request. It can be used to specified dependencies
     * between requests. See <a href="https://developers.facebook.com/docs/reference/api/batch/">Batch Requests</a> in
     * the Graph API documentation for more details.
     *
     * @param batchEntryDependsOn
     *            the name of the request entry that this entry depends on in a batched request
     */
    public final void setBatchEntryDependsOn(String batchEntryDependsOn) {
        this.batchEntryDependsOn = batchEntryDependsOn;
    }

    /**
     * Returns whether or not this batch entry will return a response if it is successful. Only applies if another
     * request entry in the batch specifies this entry as a dependency.
     *
     * @return the name of this request's dependency, or null if none has been specified
     */
    public final boolean getBatchEntryOmitResultOnSuccess() {
        return this.batchEntryOmitResultOnSuccess;
    }

    /**
     * Sets whether or not this batch entry will return a response if it is successful. Only applies if another
     * request entry in the batch specifies this entry as a dependency. See
     * <a href="https://developers.facebook.com/docs/reference/api/batch/">Batch Requests</a> in the Graph API
     * documentation for more details.
     *
     * @param batchEntryOmitResultOnSuccess
     *            the name of the request entry that this entry depends on in a batched request
     */
    public final void setBatchEntryOmitResultOnSuccess(boolean batchEntryOmitResultOnSuccess) {
        this.batchEntryOmitResultOnSuccess = batchEntryOmitResultOnSuccess;
    }

    /**
     * Gets the default Facebook application ID that will be used to submit batched requests if none of those requests
     * specifies a Session. Batched requests require an application ID, so either at least one request in a batch must
     * specify a Session or the application ID must be specified explicitly.
     *
     * @return the Facebook application ID to use for batched requests if none can be determined
     */
    public static final String getDefaultBatchApplicationId() {
        return Request.defaultBatchApplicationId;
    }

    /**
     * Sets the default application ID that will be used to submit batched requests if none of those requests specifies
     * a Session. Batched requests require an application ID, so either at least one request in a batch must specify a
     * Session or the application ID must be specified explicitly.
     *
     * @param applicationId
     *            the Facebook application ID to use for batched requests if none can be determined
     */
    public static final void setDefaultBatchApplicationId(String applicationId) {
        Request.defaultBatchApplicationId = applicationId;
    }

    /**
     * Returns the callback which will be called when the request finishes.
     *
     * @return the callback
     */
    public final Callback getCallback() {
        return callback;
    }

    /**
     * Sets the callback which will be called when the request finishes.
     *
     * @param callback
     *            the callback
     */
    public final void setCallback(Callback callback) {
        this.callback = callback;
    }

    /**
     * Starts a new Request configured to post a GraphObject to a particular graph path, to either create or update the
     * object at that path.
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param graphPath
     *            the graph path to retrieve, create, or delete
     * @param graphObject
     *            the GraphObject to create or update
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a RequestAsyncTask that is executing the request
     */
    public static RequestAsyncTask executePostRequestAsync(Session session, String graphPath,
            GraphObject graphObject, Callback callback) {
        return newPostRequest(session, graphPath, graphObject, callback).executeAsync();
    }

    /**
     * Creates a new Request configured to make a call to the Facebook REST API.
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param restMethod
     *            the method in the Facebook REST API to execute
     * @param parameters
     *            additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers,
     *            Bitmaps, Dates, or Byte arrays.
     * @param httpMethod
     *            the HTTP method to use for the request; must be one of GET, POST, or DELETE
     * @return a RequestAsyncTask that is executing the request
     */
    public static RequestAsyncTask executeRestRequestAsync(Session session, String restMethod, Bundle parameters,
            HttpMethod httpMethod) {
        return newRestRequest(session, restMethod, parameters, httpMethod).executeAsync();
    }

    /**
     * Creates a new Request configured to retrieve a user's own profile.
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a RequestAsyncTask that is executing the request
     */
    public static RequestAsyncTask executeMeRequestAsync(Session session, GraphUserCallback callback) {
        return newMeRequest(session, callback).executeAsync();
    }

    /**
     * Creates a new Request configured to retrieve a user's friend list.
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a RequestAsyncTask that is executing the request
     */
    public static RequestAsyncTask executeMyFriendsRequestAsync(Session session, GraphUserListCallback callback) {
        return newMyFriendsRequest(session, callback).executeAsync();
    }

    /**
     * Creates a new Request configured to upload a photo to the user's default photo album.
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param image
     *            the image to upload
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a RequestAsyncTask that is executing the request
     */
    public static RequestAsyncTask executeUploadPhotoRequestAsync(Session session, Bitmap image,
            Callback callback) {
        return newUploadPhotoRequest(session, image, callback).executeAsync();
    }

    /**
     * Creates a new Request configured to upload a photo to the user's default photo album. The photo
     * will be read from the specified stream.
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param session  the Session to use, or null; if non-null, the session must be in an opened state
     * @param file     the file containing the photo to upload
     * @param callback a callback that will be called when the request is completed to handle success or error conditions
     * @return a RequestAsyncTask that is executing the request
     */
    public static RequestAsyncTask executeUploadPhotoRequestAsync(Session session, File file, Callback callback)
            throws FileNotFoundException {
        return newUploadPhotoRequest(session, file, callback).executeAsync();
    }

    /**
     * Creates a new Request configured to retrieve a particular graph path.
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param graphPath
     *            the graph path to retrieve
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a RequestAsyncTask that is executing the request
     */
    public static RequestAsyncTask executeGraphPathRequestAsync(Session session, String graphPath,
            Callback callback) {
        return newGraphPathRequest(session, graphPath, callback).executeAsync();
    }

    /**
     * Creates a new Request that is configured to perform a search for places near a specified location via the Graph
     * API.
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param location
     *            the location around which to search; only the latitude and longitude components of the location are
     *            meaningful
     * @param radiusInMeters
     *            the radius around the location to search, specified in meters
     * @param resultsLimit
     *            the maximum number of results to return
     * @param searchText
     *            optional text to search for as part of the name or type of an object
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a RequestAsyncTask that is executing the request
     *
     * @throws FacebookException If neither location nor searchText is specified
     */
    public static RequestAsyncTask executePlacesSearchRequestAsync(Session session, Location location,
            int radiusInMeters, int resultsLimit, String searchText, GraphPlaceListCallback callback) {
        return newPlacesSearchRequest(session, location, radiusInMeters, resultsLimit, searchText, callback)
                .executeAsync();
    }

    /**
     * Creates a new Request configured to post a status update to a user's feed.
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param message
     *            the text of the status update
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions
     * @return a RequestAsyncTask that is executing the request
     */
    public static RequestAsyncTask executeStatusUpdateRequestAsync(Session session, String message,
            Callback callback) {
        return newStatusUpdateRequest(session, message, callback).executeAsync();
    }

    /**
     * Executes this request and returns the response.
     * <p/>
     * This should only be called if you have transitioned off the UI thread.
     *
     * @return the Response object representing the results of the request
     *
     * @throws FacebookException
     *            If there was an error in the protocol used to communicate with the service
     * @throws IllegalArgumentException
     */
    public final Response executeAndWait() {
        return Request.executeAndWait(this);
    }

    /**
     * Executes this request and returns the response.
     * <p/>
     * This should only be called from the UI thread.
     *
     * @return a RequestAsyncTask that is executing the request
     *
     * @throws IllegalArgumentException
     */
    public final RequestAsyncTask executeAsync() {
        return Request.executeBatchAsync(this);
    }

    /**
     * Serializes one or more requests but does not execute them. The resulting HttpURLConnection can be executed
     * explicitly by the caller.
     *
     * @param requests
     *            one or more Requests to serialize
     * @return an HttpURLConnection which is ready to execute
     *
     * @throws FacebookException
     *            If any of the requests in the batch are badly constructed or if there are problems
     *            contacting the service
     * @throws IllegalArgumentException if the passed in array is zero-length
     * @throws NullPointerException if the passed in array or any of its contents are null
     */
    public static HttpURLConnection toHttpConnection(Request... requests) {
        return toHttpConnection(Arrays.asList(requests));
    }

    /**
     * Serializes one or more requests but does not execute them. The resulting HttpURLConnection can be executed
     * explicitly by the caller.
     *
     * @param requests
     *            one or more Requests to serialize
     * @return an HttpURLConnection which is ready to execute
     *
     * @throws FacebookException
     *            If any of the requests in the batch are badly constructed or if there are problems
     *            contacting the service
     * @throws IllegalArgumentException if the passed in collection is empty
     * @throws NullPointerException if the passed in collection or any of its contents are null
     */
    public static HttpURLConnection toHttpConnection(Collection<Request> requests) {
        Validate.notEmptyAndContainsNoNulls(requests, "requests");

        return toHttpConnection(new RequestBatch(requests));
    }

    /**
     * Serializes one or more requests but does not execute them. The resulting HttpURLConnection can be executed
     * explicitly by the caller.
     *
     * @param requests
     *            a RequestBatch to serialize
     * @return an HttpURLConnection which is ready to execute
     *
     * @throws FacebookException
     *            If any of the requests in the batch are badly constructed or if there are problems
     *            contacting the service
     * @throws IllegalArgumentException
     */
    public static HttpURLConnection toHttpConnection(RequestBatch requests) {

        for (Request request : requests) {
            request.validate();
        }

        URL url = null;
        try {
            if (requests.size() == 1) {
                // Single request case.
                Request request = requests.get(0);
                // In the non-batch case, the URL we use really is the same one returned by getUrlForSingleRequest.
                url = new URL(request.getUrlForSingleRequest());
            } else {
                // Batch case -- URL is just the graph API base, individual request URLs are serialized
                // as relative_url parameters within each batch entry.
                url = new URL(ServerProtocol.GRAPH_URL);
            }
        } catch (MalformedURLException e) {
            throw new FacebookException("could not construct URL for request", e);
        }

        HttpURLConnection connection;
        try {
            connection = createConnection(url);

            serializeToUrlConnection(requests, connection);
        } catch (IOException e) {
            throw new FacebookException("could not construct request body", e);
        } catch (JSONException e) {
            throw new FacebookException("could not construct request body", e);
        }

        return connection;
    }

    /**
     * Executes a single request on the current thread and returns the response.
     * <p/>
     * This should only be used if you have transitioned off the UI thread.
     *
     * @param request
     *            the Request to execute
     *
     * @return the Response object representing the results of the request
     *
     * @throws FacebookException
     *            If there was an error in the protocol used to communicate with the service
     */
    public static Response executeAndWait(Request request) {
        List<Response> responses = executeBatchAndWait(request);

        if (responses == null || responses.size() != 1) {
            throw new FacebookException("invalid state: expected a single response");
        }

        return responses.get(0);
    }

    /**
     * Executes requests on the current thread as a single batch and returns the responses.
     * <p/>
     * This should only be used if you have transitioned off the UI thread.
     *
     * @param requests
     *            the Requests to execute
     *
     * @return a list of Response objects representing the results of the requests; responses are returned in the same
     *         order as the requests were specified.
     *
     * @throws NullPointerException
     *            In case of a null request
     * @throws FacebookException
     *            If there was an error in the protocol used to communicate with the service
     */
    public static List<Response> executeBatchAndWait(Request... requests) {
        Validate.notNull(requests, "requests");

        return executeBatchAndWait(Arrays.asList(requests));
    }

    /**
     * Executes requests as a single batch on the current thread and returns the responses.
     * <p/>
     * This should only be used if you have transitioned off the UI thread.
     *
     * @param requests
     *            the Requests to execute
     *
     * @return a list of Response objects representing the results of the requests; responses are returned in the same
     *         order as the requests were specified.
     *
     * @throws FacebookException
     *            If there was an error in the protocol used to communicate with the service
     */
    public static List<Response> executeBatchAndWait(Collection<Request> requests) {
        return executeBatchAndWait(new RequestBatch(requests));
    }

    /**
     * Executes requests on the current thread as a single batch and returns the responses.
     * <p/>
     * This should only be used if you have transitioned off the UI thread.
     *
     * @param requests
     *            the batch of Requests to execute
     *
     * @return a list of Response objects representing the results of the requests; responses are returned in the same
     *         order as the requests were specified.
     *
     * @throws FacebookException
     *            If there was an error in the protocol used to communicate with the service
     * @throws IllegalArgumentException if the passed in RequestBatch is empty
     * @throws NullPointerException if the passed in RequestBatch or any of its contents are null
     */
    public static List<Response> executeBatchAndWait(RequestBatch requests) {
        Validate.notEmptyAndContainsNoNulls(requests, "requests");

        HttpURLConnection connection = null;
        try {
            connection = toHttpConnection(requests);
        } catch (Exception ex) {
            List<Response> responses = Response.constructErrorResponses(requests.getRequests(), null,
                    new FacebookException(ex));
            runCallbacks(requests, responses);
            return responses;
        }

        List<Response> responses = executeConnectionAndWait(connection, requests);
        return responses;
    }

    /**
     * Executes requests as a single batch asynchronously. This function will return immediately, and the requests will
     * be processed on a separate thread. In order to process results of a request, or determine whether a request
     * succeeded or failed, a callback must be specified (see the {@link #setCallback(Callback) setCallback} method).
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param requests
     *            the Requests to execute
     * @return a RequestAsyncTask that is executing the request
     *
     * @throws NullPointerException
     *            If a null request is passed in
     */
    public static RequestAsyncTask executeBatchAsync(Request... requests) {
        Validate.notNull(requests, "requests");

        return executeBatchAsync(Arrays.asList(requests));
    }

    /**
     * Executes requests as a single batch asynchronously. This function will return immediately, and the requests will
     * be processed on a separate thread. In order to process results of a request, or determine whether a request
     * succeeded or failed, a callback must be specified (see the {@link #setCallback(Callback) setCallback} method).
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param requests
     *            the Requests to execute
     * @return a RequestAsyncTask that is executing the request
     *
     * @throws IllegalArgumentException if the passed in collection is empty
     * @throws NullPointerException if the passed in collection or any of its contents are null
     */
    public static RequestAsyncTask executeBatchAsync(Collection<Request> requests) {
        return executeBatchAsync(new RequestBatch(requests));
    }

    /**
     * Executes requests as a single batch asynchronously. This function will return immediately, and the requests will
     * be processed on a separate thread. In order to process results of a request, or determine whether a request
     * succeeded or failed, a callback must be specified (see the {@link #setCallback(Callback) setCallback} method).
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param requests
     *            the RequestBatch to execute
     * @return a RequestAsyncTask that is executing the request
     *
     * @throws IllegalArgumentException if the passed in RequestBatch is empty
     * @throws NullPointerException if the passed in RequestBatch or any of its contents are null
     */
    public static RequestAsyncTask executeBatchAsync(RequestBatch requests) {
        Validate.notEmptyAndContainsNoNulls(requests, "requests");

        RequestAsyncTask asyncTask = new RequestAsyncTask(requests);
        asyncTask.executeOnSettingsExecutor();
        return asyncTask;
    }

    /**
     * Executes requests that have already been serialized into an HttpURLConnection. No validation is done that the
     * contents of the connection actually reflect the serialized requests, so it is the caller's responsibility to
     * ensure that it will correctly generate the desired responses.
     * <p/>
     * This should only be called if you have transitioned off the UI thread.
     *
     * @param connection
     *            the HttpURLConnection that the requests were serialized into
     * @param requests
     *            the requests represented by the HttpURLConnection
     * @return a list of Responses corresponding to the requests
     *
     * @throws FacebookException
     *            If there was an error in the protocol used to communicate with the service
     */
    public static List<Response> executeConnectionAndWait(HttpURLConnection connection,
            Collection<Request> requests) {
        return executeConnectionAndWait(connection, new RequestBatch(requests));
    }

    /**
     * Executes requests that have already been serialized into an HttpURLConnection. No validation is done that the
     * contents of the connection actually reflect the serialized requests, so it is the caller's responsibility to
     * ensure that it will correctly generate the desired responses.
     * <p/>
     * This should only be called if you have transitioned off the UI thread.
     *
     * @param connection
     *            the HttpURLConnection that the requests were serialized into
     * @param requests
     *            the RequestBatch represented by the HttpURLConnection
     * @return a list of Responses corresponding to the requests
     *
     * @throws FacebookException
     *            If there was an error in the protocol used to communicate with the service
     */
    public static List<Response> executeConnectionAndWait(HttpURLConnection connection, RequestBatch requests) {
        List<Response> responses = Response.fromHttpConnection(connection, requests);

        Utility.disconnectQuietly(connection);

        int numRequests = requests.size();
        if (numRequests != responses.size()) {
            throw new FacebookException(
                    String.format("Received %d responses while expecting %d", responses.size(), numRequests));
        }

        runCallbacks(requests, responses);

        // See if any of these sessions needs its token to be extended. We do this after issuing the request so as to
        // reduce network contention.
        HashSet<Session> sessions = new HashSet<Session>();
        for (Request request : requests) {
            if (request.session != null) {
                sessions.add(request.session);
            }
        }
        for (Session session : sessions) {
            session.extendAccessTokenIfNeeded();
        }

        return responses;
    }

    /**
     * Asynchronously executes requests that have already been serialized into an HttpURLConnection. No validation is
     * done that the contents of the connection actually reflect the serialized requests, so it is the caller's
     * responsibility to ensure that it will correctly generate the desired responses. This function will return
     * immediately, and the requests will be processed on a separate thread. In order to process results of a request,
     * or determine whether a request succeeded or failed, a callback must be specified (see the
     * {@link #setCallback(Callback) setCallback} method).
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param connection
     *            the HttpURLConnection that the requests were serialized into
     * @param requests
     *            the requests represented by the HttpURLConnection
     * @return a RequestAsyncTask that is executing the request
     */
    public static RequestAsyncTask executeConnectionAsync(HttpURLConnection connection, RequestBatch requests) {
        return executeConnectionAsync(null, connection, requests);
    }

    /**
     * Asynchronously executes requests that have already been serialized into an HttpURLConnection. No validation is
     * done that the contents of the connection actually reflect the serialized requests, so it is the caller's
     * responsibility to ensure that it will correctly generate the desired responses. This function will return
     * immediately, and the requests will be processed on a separate thread. In order to process results of a request,
     * or determine whether a request succeeded or failed, a callback must be specified (see the
     * {@link #setCallback(Callback) setCallback} method)
     * <p/>
     * This should only be called from the UI thread.
     *
     * @param callbackHandler
     *            a Handler that will be used to post calls to the callback for each request; if null, a Handler will be
     *            instantiated on the calling thread
     * @param connection
     *            the HttpURLConnection that the requests were serialized into
     * @param requests
     *            the requests represented by the HttpURLConnection
     * @return a RequestAsyncTask that is executing the request
     */
    public static RequestAsyncTask executeConnectionAsync(Handler callbackHandler, HttpURLConnection connection,
            RequestBatch requests) {
        Validate.notNull(connection, "connection");

        RequestAsyncTask asyncTask = new RequestAsyncTask(connection, requests);
        requests.setCallbackHandler(callbackHandler);
        asyncTask.executeOnSettingsExecutor();
        return asyncTask;
    }

    /**
     * Returns a string representation of this Request, useful for debugging.
     *
     * @return the debugging information
     */
    @Override
    public String toString() {
        return new StringBuilder().append("{Request: ").append(" session: ").append(session).append(", graphPath: ")
                .append(graphPath).append(", graphObject: ").append(graphObject).append(", restMethod: ")
                .append(restMethod).append(", httpMethod: ").append(httpMethod).append(", parameters: ")
                .append(parameters).append("}").toString();
    }

    static void runCallbacks(final RequestBatch requests, List<Response> responses) {
        int numRequests = requests.size();

        // Compile the list of callbacks to call and then run them either on this thread or via the Handler we received
        final ArrayList<Pair<Callback, Response>> callbacks = new ArrayList<Pair<Callback, Response>>();
        for (int i = 0; i < numRequests; ++i) {
            Request request = requests.get(i);
            if (request.callback != null) {
                callbacks.add(new Pair<Callback, Response>(request.callback, responses.get(i)));
            }
        }

        if (callbacks.size() > 0) {
            Runnable runnable = new Runnable() {
                public void run() {
                    for (Pair<Callback, Response> pair : callbacks) {
                        pair.first.onCompleted(pair.second);
                    }

                    List<RequestBatch.Callback> batchCallbacks = requests.getCallbacks();
                    for (RequestBatch.Callback batchCallback : batchCallbacks) {
                        batchCallback.onBatchCompleted(requests);
                    }
                }
            };

            Handler callbackHandler = requests.getCallbackHandler();
            if (callbackHandler == null) {
                // Run on this thread.
                runnable.run();
            } else {
                // Post to the handler.
                callbackHandler.post(runnable);
            }
        }
    }

    static HttpURLConnection createConnection(URL url) throws IOException {
        HttpURLConnection connection;
        connection = (HttpURLConnection) url.openConnection();

        connection.setRequestProperty(USER_AGENT_HEADER, getUserAgent());
        connection.setRequestProperty(CONTENT_TYPE_HEADER, getMimeContentType());

        connection.setChunkedStreamingMode(0);
        return connection;
    }

    private void addCommonParameters() {
        if (this.session != null) {
            if (!this.session.isOpened()) {
                throw new FacebookException("Session provided to a Request in un-opened state.");
            } else if (!this.parameters.containsKey(ACCESS_TOKEN_PARAM)) {
                String accessToken = this.session.getAccessToken();
                Logger.registerAccessToken(accessToken);
                this.parameters.putString(ACCESS_TOKEN_PARAM, accessToken);
            }
        }
        this.parameters.putString(SDK_PARAM, SDK_ANDROID);
        this.parameters.putString(FORMAT_PARAM, FORMAT_JSON);
    }

    private String appendParametersToBaseUrl(String baseUrl) {
        Uri.Builder uriBuilder = new Uri.Builder().encodedPath(baseUrl);

        Set<String> keys = this.parameters.keySet();
        for (String key : keys) {
            Object value = this.parameters.get(key);

            if (value == null) {
                value = "";
            }

            if (isSupportedParameterType(value)) {
                value = parameterToString(value);
            } else {
                if (httpMethod == HttpMethod.GET) {
                    throw new IllegalArgumentException(String.format(
                            "Unsupported parameter type for GET request: %s", value.getClass().getSimpleName()));
                }
                continue;
            }

            uriBuilder.appendQueryParameter(key, value.toString());
        }

        return uriBuilder.toString();
    }

    final String getUrlForBatchedRequest() {
        if (overriddenURL != null) {
            throw new FacebookException("Can't override URL for a batch request");
        }

        String baseUrl;
        if (this.restMethod != null) {
            baseUrl = ServerProtocol.BATCHED_REST_METHOD_URL_BASE + this.restMethod;
        } else {
            baseUrl = this.graphPath;
        }

        addCommonParameters();
        return appendParametersToBaseUrl(baseUrl);
    }

    final String getUrlForSingleRequest() {
        if (overriddenURL != null) {
            return overriddenURL.toString();
        }

        String baseUrl;
        if (this.restMethod != null) {
            baseUrl = ServerProtocol.REST_URL_BASE + this.restMethod;
        } else {
            baseUrl = ServerProtocol.GRAPH_URL_BASE + this.graphPath;
        }

        addCommonParameters();
        return appendParametersToBaseUrl(baseUrl);
    }

    private void serializeToBatch(JSONArray batch, Bundle attachments) throws JSONException, IOException {
        JSONObject batchEntry = new JSONObject();

        if (this.batchEntryName != null) {
            batchEntry.put(BATCH_ENTRY_NAME_PARAM, this.batchEntryName);
            batchEntry.put(BATCH_ENTRY_OMIT_RESPONSE_ON_SUCCESS_PARAM, this.batchEntryOmitResultOnSuccess);
        }
        if (this.batchEntryDependsOn != null) {
            batchEntry.put(BATCH_ENTRY_DEPENDS_ON_PARAM, this.batchEntryDependsOn);
        }

        String relativeURL = getUrlForBatchedRequest();
        batchEntry.put(BATCH_RELATIVE_URL_PARAM, relativeURL);
        batchEntry.put(BATCH_METHOD_PARAM, httpMethod);
        if (this.session != null) {
            String accessToken = this.session.getAccessToken();
            Logger.registerAccessToken(accessToken);
        }

        // Find all of our attachments. Remember their names and put them in the attachment map.
        ArrayList<String> attachmentNames = new ArrayList<String>();
        Set<String> keys = this.parameters.keySet();
        for (String key : keys) {
            Object value = this.parameters.get(key);
            if (isSupportedAttachmentType(value)) {
                // Make the name unique across this entire batch.
                String name = String.format("%s%d", ATTACHMENT_FILENAME_PREFIX, attachments.size());
                attachmentNames.add(name);
                Utility.putObjectInBundle(attachments, name, value);
            }
        }

        if (!attachmentNames.isEmpty()) {
            String attachmentNamesString = TextUtils.join(",", attachmentNames);
            batchEntry.put(ATTACHED_FILES_PARAM, attachmentNamesString);
        }

        if (this.graphObject != null) {
            // Serialize the graph object into the "body" parameter.
            final ArrayList<String> keysAndValues = new ArrayList<String>();
            processGraphObject(this.graphObject, relativeURL, new KeyValueSerializer() {
                @Override
                public void writeString(String key, String value) throws IOException {
                    keysAndValues.add(String.format("%s=%s", key, URLEncoder.encode(value, "UTF-8")));
                }
            });
            String bodyValue = TextUtils.join("&", keysAndValues);
            batchEntry.put(BATCH_BODY_PARAM, bodyValue);
        }

        batch.put(batchEntry);
    }

    private void validate() {
        if (graphPath != null && restMethod != null) {
            throw new IllegalArgumentException(
                    "Only one of a graph path or REST method may be specified per request.");
        }
    }

    final static void serializeToUrlConnection(RequestBatch requests, HttpURLConnection connection)
            throws IOException, JSONException {
        Logger logger = new Logger(LoggingBehavior.REQUESTS, "Request");

        int numRequests = requests.size();

        HttpMethod connectionHttpMethod = (numRequests == 1) ? requests.get(0).httpMethod : HttpMethod.POST;
        connection.setRequestMethod(connectionHttpMethod.name());

        URL url = connection.getURL();
        logger.append("Request:\n");
        logger.appendKeyValue("Id", requests.getId());
        logger.appendKeyValue("URL", url);
        logger.appendKeyValue("Method", connection.getRequestMethod());
        logger.appendKeyValue("User-Agent", connection.getRequestProperty("User-Agent"));
        logger.appendKeyValue("Content-Type", connection.getRequestProperty("Content-Type"));

        connection.setConnectTimeout(requests.getTimeout());
        connection.setReadTimeout(requests.getTimeout());

        // If we have a single non-POST request, don't try to serialize anything or HttpURLConnection will
        // turn it into a POST.
        boolean isPost = (connectionHttpMethod == HttpMethod.POST);
        if (!isPost) {
            logger.log();
            return;
        }

        connection.setDoOutput(true);

        BufferedOutputStream outputStream = new BufferedOutputStream(connection.getOutputStream());
        try {
            Serializer serializer = new Serializer(outputStream, logger);

            if (numRequests == 1) {
                Request request = requests.get(0);

                logger.append("  Parameters:\n");
                serializeParameters(request.parameters, serializer);

                logger.append("  Attachments:\n");
                serializeAttachments(request.parameters, serializer);

                if (request.graphObject != null) {
                    processGraphObject(request.graphObject, url.getPath(), serializer);
                }
            } else {
                String batchAppID = getBatchAppId(requests);
                if (Utility.isNullOrEmpty(batchAppID)) {
                    throw new FacebookException("At least one request in a batch must have an open Session, or a "
                            + "default app ID must be specified.");
                }

                serializer.writeString(BATCH_APP_ID_PARAM, batchAppID);

                // We write out all the requests as JSON, remembering which file attachments they have, then
                // write out the attachments.
                Bundle attachments = new Bundle();
                serializeRequestsAsJSON(serializer, requests, attachments);

                logger.append("  Attachments:\n");
                serializeAttachments(attachments, serializer);
            }
        } finally {
            outputStream.close();
        }

        logger.log();
    }

    private static void processGraphObject(GraphObject graphObject, String path, KeyValueSerializer serializer)
            throws IOException {
        // In general, graph objects are passed by reference (ID/URL). But if this is an OG Action,
        // we need to pass the entire values of the contents of the 'image' property, as they
        // contain important metadata beyond just a URL. We don't have a 100% foolproof way of knowing
        // if we are posting an OG Action, given that batched requests can have parameter substitution,
        // but passing the OG Action type as a substituted parameter is unlikely.
        // It looks like an OG Action if it's posted to me/namespace:action[?other=stuff].
        boolean isOGAction = false;
        if (path.startsWith("me/") || path.startsWith("/me/")) {
            int colonLocation = path.indexOf(":");
            int questionMarkLocation = path.indexOf("?");
            isOGAction = colonLocation > 3 && (questionMarkLocation == -1 || colonLocation < questionMarkLocation);
        }

        Set<Entry<String, Object>> entries = graphObject.asMap().entrySet();
        for (Entry<String, Object> entry : entries) {
            boolean passByValue = isOGAction && entry.getKey().equalsIgnoreCase("image");
            processGraphObjectProperty(entry.getKey(), entry.getValue(), serializer, passByValue);
        }
    }

    private static void processGraphObjectProperty(String key, Object value, KeyValueSerializer serializer,
            boolean passByValue) throws IOException {
        Class<?> valueClass = value.getClass();
        if (GraphObject.class.isAssignableFrom(valueClass)) {
            value = ((GraphObject) value).getInnerJSONObject();
            valueClass = value.getClass();
        } else if (GraphObjectList.class.isAssignableFrom(valueClass)) {
            value = ((GraphObjectList<?>) value).getInnerJSONArray();
            valueClass = value.getClass();
        }

        if (JSONObject.class.isAssignableFrom(valueClass)) {
            JSONObject jsonObject = (JSONObject) value;
            if (passByValue) {
                // We need to pass all properties of this object in key[propertyName] format.
                @SuppressWarnings("unchecked")
                Iterator<String> keys = jsonObject.keys();
                while (keys.hasNext()) {
                    String propertyName = keys.next();
                    String subKey = String.format("%s[%s]", key, propertyName);
                    processGraphObjectProperty(subKey, jsonObject.opt(propertyName), serializer, passByValue);
                }
            } else {
                // Normal case is passing objects by reference, so just pass the ID or URL, if any, as the value
                // for "key"
                if (jsonObject.has("id")) {
                    processGraphObjectProperty(key, jsonObject.optString("id"), serializer, passByValue);
                } else if (jsonObject.has("url")) {
                    processGraphObjectProperty(key, jsonObject.optString("url"), serializer, passByValue);
                }
            }
        } else if (JSONArray.class.isAssignableFrom(valueClass)) {
            JSONArray jsonArray = (JSONArray) value;
            int length = jsonArray.length();
            for (int i = 0; i < length; ++i) {
                String subKey = String.format("%s[%d]", key, i);
                processGraphObjectProperty(subKey, jsonArray.opt(i), serializer, passByValue);
            }
        } else if (String.class.isAssignableFrom(valueClass) || Number.class.isAssignableFrom(valueClass)
                || Boolean.class.isAssignableFrom(valueClass)) {
            serializer.writeString(key, value.toString());
        } else if (Date.class.isAssignableFrom(valueClass)) {
            Date date = (Date) value;
            // The "Events Timezone" platform migration affects what date/time formats Facebook accepts and returns.
            // Apps created after 8/1/12 (or apps that have explicitly enabled the migration) should send/receive
            // dates in ISO-8601 format. Pre-migration apps can send as Unix timestamps. Since the future is ISO-8601,
            // that is what we support here. Apps that need pre-migration behavior can explicitly send these as
            // integer timestamps rather than Dates.
            final SimpleDateFormat iso8601DateFormat = new SimpleDateFormat(ISO_8601_FORMAT_STRING, Locale.US);
            serializer.writeString(key, iso8601DateFormat.format(date));
        }
    }

    private static void serializeParameters(Bundle bundle, Serializer serializer) throws IOException {
        Set<String> keys = bundle.keySet();

        for (String key : keys) {
            Object value = bundle.get(key);
            if (isSupportedParameterType(value)) {
                serializer.writeObject(key, value);
            }
        }
    }

    private static void serializeAttachments(Bundle bundle, Serializer serializer) throws IOException {
        Set<String> keys = bundle.keySet();

        for (String key : keys) {
            Object value = bundle.get(key);
            if (isSupportedAttachmentType(value)) {
                serializer.writeObject(key, value);
            }
        }
    }

    private static void serializeRequestsAsJSON(Serializer serializer, Collection<Request> requests,
            Bundle attachments) throws JSONException, IOException {
        JSONArray batch = new JSONArray();
        for (Request request : requests) {
            request.serializeToBatch(batch, attachments);
        }

        String batchAsString = batch.toString();
        serializer.writeString(BATCH_PARAM, batchAsString);
    }

    private static String getMimeContentType() {
        return String.format("multipart/form-data; boundary=%s", MIME_BOUNDARY);
    }

    private static volatile String userAgent;

    private static String getUserAgent() {
        if (userAgent == null) {
            userAgent = String.format("%s.%s", USER_AGENT_BASE, FacebookSdkVersion.BUILD);
        }

        return userAgent;
    }

    private static String getBatchAppId(RequestBatch batch) {
        if (!Utility.isNullOrEmpty(batch.getBatchApplicationId())) {
            return batch.getBatchApplicationId();
        }

        for (Request request : batch) {
            Session session = request.session;
            if (session != null) {
                return session.getApplicationId();
            }
        }
        return Request.defaultBatchApplicationId;
    }

    private static <T extends GraphObject> List<T> typedListFromResponse(Response response, Class<T> clazz) {
        GraphMultiResult multiResult = response.getGraphObjectAs(GraphMultiResult.class);
        if (multiResult == null) {
            return null;
        }

        GraphObjectList<GraphObject> data = multiResult.getData();
        if (data == null) {
            return null;
        }

        return data.castToListOf(clazz);
    }

    private static boolean isSupportedAttachmentType(Object value) {
        return value instanceof Bitmap || value instanceof byte[] || value instanceof ParcelFileDescriptor;
    }

    private static boolean isSupportedParameterType(Object value) {
        return value instanceof String || value instanceof Boolean || value instanceof Number
                || value instanceof Date;
    }

    private static String parameterToString(Object value) {
        if (value instanceof String) {
            return (String) value;
        } else if (value instanceof Boolean || value instanceof Number) {
            return value.toString();
        } else if (value instanceof Date) {
            final SimpleDateFormat iso8601DateFormat = new SimpleDateFormat(ISO_8601_FORMAT_STRING, Locale.US);
            return iso8601DateFormat.format(value);
        }
        throw new IllegalArgumentException("Unsupported parameter type.");
    }

    private interface KeyValueSerializer {
        void writeString(String key, String value) throws IOException;
    }

    private static class Serializer implements KeyValueSerializer {
        private final BufferedOutputStream outputStream;
        private final Logger logger;
        private boolean firstWrite = true;

        public Serializer(BufferedOutputStream outputStream, Logger logger) {
            this.outputStream = outputStream;
            this.logger = logger;
        }

        public void writeObject(String key, Object value) throws IOException {
            if (isSupportedParameterType(value)) {
                writeString(key, parameterToString(value));
            } else if (value instanceof Bitmap) {
                writeBitmap(key, (Bitmap) value);
            } else if (value instanceof byte[]) {
                writeBytes(key, (byte[]) value);
            } else if (value instanceof ParcelFileDescriptor) {
                writeFile(key, (ParcelFileDescriptor) value);
            } else {
                throw new IllegalArgumentException("value is not a supported type: String, Bitmap, byte[]");
            }
        }

        public void writeString(String key, String value) throws IOException {
            writeContentDisposition(key, null, null);
            writeLine("%s", value);
            writeRecordBoundary();
            if (logger != null) {
                logger.appendKeyValue("    " + key, value);
            }
        }

        public void writeBitmap(String key, Bitmap bitmap) throws IOException {
            writeContentDisposition(key, key, "image/png");
            // Note: quality parameter is ignored for PNG
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
            writeLine("");
            writeRecordBoundary();
            logger.appendKeyValue("    " + key, "<Image>");
        }

        public void writeBytes(String key, byte[] bytes) throws IOException {
            writeContentDisposition(key, key, "content/unknown");
            this.outputStream.write(bytes);
            writeLine("");
            writeRecordBoundary();
            logger.appendKeyValue("    " + key, String.format("<Data: %d>", bytes.length));
        }

        public void writeFile(String key, ParcelFileDescriptor descriptor) throws IOException {
            writeContentDisposition(key, key, "content/unknown");

            ParcelFileDescriptor.AutoCloseInputStream inputStream = null;
            BufferedInputStream bufferedInputStream = null;
            int totalBytes = 0;
            try {
                inputStream = new ParcelFileDescriptor.AutoCloseInputStream(descriptor);
                bufferedInputStream = new BufferedInputStream(inputStream);

                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = bufferedInputStream.read(buffer)) != -1) {
                    this.outputStream.write(buffer, 0, bytesRead);
                    totalBytes += bytesRead;
                }
            } finally {
                if (bufferedInputStream != null) {
                    bufferedInputStream.close();
                }
                if (inputStream != null) {
                    inputStream.close();
                }
            }
            writeLine("");
            writeRecordBoundary();
            logger.appendKeyValue("    " + key, String.format("<Data: %d>", totalBytes));
        }

        public void writeRecordBoundary() throws IOException {
            writeLine("--%s", MIME_BOUNDARY);
        }

        public void writeContentDisposition(String name, String filename, String contentType) throws IOException {
            write("Content-Disposition: form-data; name=\"%s\"", name);
            if (filename != null) {
                write("; filename=\"%s\"", filename);
            }
            writeLine(""); // newline after Content-Disposition
            if (contentType != null) {
                writeLine("%s: %s", CONTENT_TYPE_HEADER, contentType);
            }
            writeLine(""); // blank line before content
        }

        public void write(String format, Object... args) throws IOException {
            if (firstWrite) {
                // Prepend all of our output with a boundary string.
                this.outputStream.write("--".getBytes());
                this.outputStream.write(MIME_BOUNDARY.getBytes());
                this.outputStream.write("\r\n".getBytes());
                firstWrite = false;
            }
            this.outputStream.write(String.format(format, args).getBytes());
        }

        public void writeLine(String format, Object... args) throws IOException {
            write(format, args);
            write("\r\n");
        }

    }

    /**
     * Specifies the interface that consumers of the Request class can implement in order to be notified when a
     * particular request completes, either successfully or with an error.
     */
    public interface Callback {
        /**
         * The method that will be called when a request completes.
         *
         * @param response
         *            the Response of this request, which may include error information if the request was unsuccessful
         */
        void onCompleted(Response response);
    }

    /**
     * Specifies the interface that consumers of
     * {@link Request#executeMeRequestAsync(Session, com.trk.aboutme.facebook.Request.GraphUserCallback)}
     * can use to be notified when the request completes, either successfully or with an error.
     */
    public interface GraphUserCallback {
        /**
         * The method that will be called when the request completes.
         *
         * @param user     the GraphObject representing the returned user, or null
         * @param response the Response of this request, which may include error information if the request was unsuccessful
         */
        void onCompleted(GraphUser user, Response response);
    }

    /**
     * Specifies the interface that consumers of
     * {@link Request#executeMyFriendsRequestAsync(Session, com.trk.aboutme.facebook.Request.GraphUserListCallback)}
     * can use to be notified when the request completes, either successfully or with an error.
     */
    public interface GraphUserListCallback {
        /**
         * The method that will be called when the request completes.
         *
         * @param users    the list of GraphObjects representing the returned friends, or null
         * @param response the Response of this request, which may include error information if the request was unsuccessful
         */
        void onCompleted(List<GraphUser> users, Response response);
    }

    /**
     * Specifies the interface that consumers of
     * {@link Request#executePlacesSearchRequestAsync(Session, android.location.Location, int, int, String, com.trk.aboutme.facebook.Request.GraphPlaceListCallback)}
     * can use to be notified when the request completes, either successfully or with an error.
     */
    public interface GraphPlaceListCallback {
        /**
         * The method that will be called when the request completes.
         *
         * @param places   the list of GraphObjects representing the returned places, or null
         * @param response the Response of this request, which may include error information if the request was unsuccessful
         */
        void onCompleted(List<GraphPlace> places, Response response);
    }
}