/**
 * 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.facebook;

import android.content.Context;
import android.graphics.Bitmap;
import android.location.Location;
import android.net.Uri;
import android.os.*;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.facebook.internal.*;
import com.facebook.model.*;
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;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A single request to be sent to the Facebook Platform through the <a
 * href="https://developers.facebook.com/docs/reference/api/">Graph 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 a graph path (see the
 * {@link #setGraphPath(String) setGraphPath} method).
 *
 * 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;

    public static final String TAG = Request.class.getSimpleName();

    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 VIDEOS_SUFFIX = "/videos";
    private static final String SEARCH = "search";
    private static final String MY_FEED = "me/feed";
    private static final String MY_STAGING_RESOURCES = "me/staging_resources";
    private static final String MY_OBJECTS_FORMAT = "me/objects/%s";
    private static final String MY_ACTION_FORMAT = "me/%s";

    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";
    private static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language";

    // 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 ISO_8601_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ssZ";
    private static final String STAGING_PARAM = "file";
    private static final String OBJECT_PARAM = "object";

    private static final String MIME_BOUNDARY = "3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f";

    private static String defaultBatchApplicationId;

    // Group 1 in the pattern is the path without the version info
    private static Pattern versionPattern = Pattern.compile("^/?v\\d+\\.\\d+/(.*)");

    private Session session;
    private HttpMethod httpMethod;
    private String graphPath;
    private GraphObject graphObject;
    private String batchEntryName;
    private String batchEntryDependsOn;
    private boolean batchEntryOmitResultOnSuccess = true;
    private Bundle parameters;
    private Callback callback;
    private String overriddenURL;
    private Object tag;
    private String version;
    private boolean skipClientToken = false;

    /**
     * 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, graphPath, parameters, httpMethod, callback, 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
     * @param version
     *            the version of the Graph API
     */
    public Request(Session session, String graphPath, Bundle parameters, HttpMethod httpMethod, Callback callback, String version) {
        this.session = session;
        this.graphPath = graphPath;
        this.callback = callback;
        this.version = version;

        setHttpMethod(httpMethod);

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

        if (this.version == null) {
            this.version = ServerProtocol.getAPIVersion();
        }
    }

    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 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) {
        return newStatusUpdateRequest(session, message, (String)null, null, callback);
    }

    /**
     * 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 placeId
     *            an optional place id to associate with the post
     * @param tagIds
     *            an optional list of user ids to tag in the post
     * @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
     */
    private static Request newStatusUpdateRequest(Session session, String message, String placeId, List<String> tagIds,
            Callback callback) {

        Bundle parameters = new Bundle();
        parameters.putString("message", message);

        if (placeId != null) {
            parameters.putString("place", placeId);
        }

        if (tagIds != null && tagIds.size() > 0) {
            String tags = TextUtils.join(",", tagIds);
            parameters.putString("tags", tags);
        }

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

    /**
     * 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 place
     *            an optional place to associate with the post
     * @param tags
     *            an optional list of users to tag in the post
     * @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, GraphPlace place,
            List<GraphUser> tags, Callback callback) {

        List<String> tagIds = null;
        if (tags != null) {
            tagIds = new ArrayList<String>(tags.size());
            for (GraphUser tag: tags) {
                tagIds.add(tag.getId());
            }
        }
        String placeId = place == null ? null : place.getId();
        return newStatusUpdateRequest(session, message, placeId, tagIds, callback);
    }

    /**
     * Creates a new Request configured to retrieve an App User ID for the app's Facebook user.  Callers
     * will send this ID back to their own servers, collect up a set to create a Facebook Custom Audience with,
     * and then use the resultant Custom Audience to target ads.
     * <p/>
     * The GraphObject in the response will include an "custom_audience_third_party_id" property, with the value
     * being the ID retrieved.  This ID is an encrypted encoding of the Facebook user's ID and the
     * invoking Facebook app ID.  Multiple calls with the same user will return different IDs, thus these IDs cannot be
     * used to correlate behavior across devices or applications, and are only meaningful when sent back to Facebook
     * for creating Custom Audiences.
     * <p/>
     * The ID retrieved represents the Facebook user identified in the following way: if the specified session
     * (or activeSession if the specified session is `null`) is open, the ID will represent the user associated with
     * the activeSession; otherwise the ID will represent the user logged into the native Facebook app on the device.
     * A `null` ID will be provided into the callback if a) there is no native Facebook app, b) no one is logged into
     * it, or c) the app has previously called
     * {@link Settings#setLimitEventAndDataUsage(android.content.Context, boolean)} with `true` for this user.
     * <b>You must call this method from a background thread for it to work properly.</b>
     *
     * @param session
     *            the Session to issue the Request on, or null; if non-null, the session must be in an opened state.
     *            If there is no logged-in Facebook user, null is the expected choice.
     * @param context
     *            the Application context from which the app ID will be pulled, and from which the 'attribution ID'
     *            for the Facebook user is determined.  If there has been no app ID set, an exception will be thrown.
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions.
     *            The GraphObject in the Response will contain a "custom_audience_third_party_id" property that
     *            represents the user as described above.
     * @return a Request that is ready to execute
     */
    public static Request newCustomAudienceThirdPartyIdRequest(Session session, Context context, Callback callback) {
        return newCustomAudienceThirdPartyIdRequest(session, context, null, callback);
    }

    /**
     * Creates a new Request configured to retrieve an App User ID for the app's Facebook user.  Callers
     * will send this ID back to their own servers, collect up a set to create a Facebook Custom Audience with,
     * and then use the resultant Custom Audience to target ads.
     * <p/>
     * The GraphObject in the response will include an "custom_audience_third_party_id" property, with the value
     * being the ID retrieved.  This ID is an encrypted encoding of the Facebook user's ID and the
     * invoking Facebook app ID.  Multiple calls with the same user will return different IDs, thus these IDs cannot be
     * used to correlate behavior across devices or applications, and are only meaningful when sent back to Facebook
     * for creating Custom Audiences.
     * <p/>
     * The ID retrieved represents the Facebook user identified in the following way: if the specified session
     * (or activeSession if the specified session is `null`) is open, the ID will represent the user associated with
     * the activeSession; otherwise the ID will represent the user logged into the native Facebook app on the device.
     * A `null` ID will be provided into the callback if a) there is no native Facebook app, b) no one is logged into
     * it, or c) the app has previously called
     * {@link Settings#setLimitEventAndDataUsage(android.content.Context, boolean)} ;} with `true` for this user.
     * <b>You must call this method from a background thread for it to work properly.</b>
     *
     * @param session
     *            the Session to issue the Request on, or null; if non-null, the session must be in an opened state.
     *            If there is no logged-in Facebook user, null is the expected choice.
     * @param context
     *            the Application context from which the app ID will be pulled, and from which the 'attribution ID'
     *            for the Facebook user is determined.  If there has been no app ID set, an exception will be thrown.
     * @param applicationId
     *            explicitly specified Facebook App ID.  If null, and there's a valid session, then the application ID
     *            from the session will be used, otherwise the application ID from metadata will be used.
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions.
     *            The GraphObject in the Response will contain a "custom_audience_third_party_id" property that
     *            represents the user as described above.
     * @return a Request that is ready to execute
     */
    public static Request newCustomAudienceThirdPartyIdRequest(Session session,
            Context context, String applicationId, Callback callback) {

        // if provided session or activeSession is opened, use it.
        if (session == null) {
            session = Session.getActiveSession();
        }

        if (session != null && !session.isOpened()) {
            session = null;
        }

        if (applicationId == null) {
            if (session != null) {
                applicationId = session.getApplicationId();
            } else {
                applicationId = Utility.getMetadataApplicationId(context);
            }
        }

        if (applicationId == null) {
            throw new FacebookException("Facebook App ID cannot be determined");
        }

        String endpoint = applicationId + "/custom_audience_third_party_id";
        AttributionIdentifiers attributionIdentifiers = AttributionIdentifiers.getAttributionIdentifiers(context);
        Bundle parameters = new Bundle();

        if (session == null) {
            // Only use the attributionID if we don't have an open session.  If we do have an open session, then
            // the user token will be used to identify the user, and is more reliable than the attributionID.
            String udid = attributionIdentifiers.getAttributionId() != null
                ? attributionIdentifiers.getAttributionId()
                : attributionIdentifiers.getAndroidAdvertiserId();
            if (attributionIdentifiers.getAttributionId() != null) {
                parameters.putString("udid", udid);
            }
        }

        // Server will choose to not provide the App User ID in the event that event usage has been limited for
        // this user for this app.
        if (Settings.getLimitEventAndDataUsage(context) || attributionIdentifiers.isTrackingLimited()) {
            parameters.putString("limit_event_usage", "1");
        }

        return new Request(session, endpoint, parameters, HttpMethod.GET, callback);
    }

    /**
     * Creates a new Request configured to upload an image to create a staging resource. Staging resources
     * allow you to post binary data such as images, in preparation for a post of an Open Graph object or action
     * which references the image. The URI returned when uploading a staging resource may be passed as the image
     * property for an Open Graph object or action.
     *
     * @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 newUploadStagingResourceWithImageRequest(Session session,
            Bitmap image, Callback callback) {
        Bundle parameters = new Bundle(1);
        parameters.putParcelable(STAGING_PARAM, image);

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

    /**
     * Creates a new Request configured to upload an image to create a staging resource. Staging resources
     * allow you to post binary data such as images, in preparation for a post of an Open Graph object or action
     * which references the image. The URI returned when uploading a staging resource may be passed as the image
     * property for an Open Graph object or action.
     *
     * @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 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 newUploadStagingResourceWithImageRequest(Session session,
            File file, Callback callback) throws FileNotFoundException {
        ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
        ParcelFileDescriptorWithMimeType descriptorWithMimeType = new ParcelFileDescriptorWithMimeType(descriptor, "image/png");
        Bundle parameters = new Bundle(1);
        parameters.putParcelable(STAGING_PARAM, descriptorWithMimeType);

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

    /**
     * Creates a new Request configured to create a user owned Open Graph object.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param openGraphObject
     *            the Open Graph object to create; must not be null, and must have a non-empty type and title
     * @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 newPostOpenGraphObjectRequest(Session session,
            OpenGraphObject openGraphObject, Callback callback) {
        if (openGraphObject == null) {
            throw new FacebookException("openGraphObject cannot be null");
        }
        if (Utility.isNullOrEmpty(openGraphObject.getType())) {
            throw new FacebookException("openGraphObject must have non-null 'type' property");
        }
        if (Utility.isNullOrEmpty(openGraphObject.getTitle())) {
            throw new FacebookException("openGraphObject must have non-null 'title' property");
        }

        String path = String.format(MY_OBJECTS_FORMAT, openGraphObject.getType());
        Bundle bundle = new Bundle();
        bundle.putString(OBJECT_PARAM, openGraphObject.getInnerJSONObject().toString());
        return new Request(session, path, bundle, HttpMethod.POST, callback);
    }

    /**
     * Creates a new Request configured to create a user owned Open Graph object.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param type
     *            the fully-specified Open Graph object type (e.g., my_app_namespace:my_object_name); must not be null
     * @param title
     *            the title of the Open Graph object; must not be null
     * @param imageUrl
     *            the link to an image to be associated with the Open Graph object; may be null
     * @param url
     *            the url to be associated with the Open Graph object; may be null
     * @param description
     *            the description to be associated with the object; may be null
     * @param objectProperties
     *            any additional type-specific properties for the Open Graph object; may be null
     * @param callback
     *            a callback that will be called when the request is completed to handle success or error conditions;
     *            may be null
     * @return a Request that is ready to execute
     */
    public static Request newPostOpenGraphObjectRequest(Session session, String type, String title, String imageUrl,
            String url, String description, GraphObject objectProperties, Callback callback) {
        OpenGraphObject openGraphObject = OpenGraphObject.Factory.createForPost(OpenGraphObject.class, type, title,
                imageUrl, url, description);
        if (objectProperties != null) {
            openGraphObject.setData(objectProperties);
        }

        return newPostOpenGraphObjectRequest(session, openGraphObject, callback);
    }

    /**
     * Creates a new Request configured to publish an Open Graph action.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param openGraphAction
     *            the Open Graph object to create; must not be null, and must have a non-empty 'type'
     * @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 newPostOpenGraphActionRequest(Session session, OpenGraphAction openGraphAction,
            Callback callback) {
        if (openGraphAction == null) {
            throw new FacebookException("openGraphAction cannot be null");
        }
        if (Utility.isNullOrEmpty(openGraphAction.getType())) {
            throw new FacebookException("openGraphAction must have non-null 'type' property");
        }

        String path = String.format(MY_ACTION_FORMAT, openGraphAction.getType());
        return newPostRequest(session, path, openGraphAction, callback);
    }

    /**
     * Creates a new Request configured to delete a resource through the Graph API.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param id
     *            the id of the object to delete
     * @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 newDeleteObjectRequest(Session session, String id, Callback callback) {
        return new Request(session, id, null, HttpMethod.DELETE, callback);
    }

    /**
     * Creates a new Request configured to update a user owned Open Graph object.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param openGraphObject
     *            the Open Graph object to update, which must have a valid 'id' property
     * @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 newUpdateOpenGraphObjectRequest(Session session, OpenGraphObject openGraphObject,
            Callback callback) {
        if (openGraphObject == null) {
            throw new FacebookException("openGraphObject cannot be null");
        }

        String path = openGraphObject.getId();
        if (path == null) {
            throw new FacebookException("openGraphObject must have an id");
        }

        Bundle bundle = new Bundle();
        bundle.putString(OBJECT_PARAM, openGraphObject.getInnerJSONObject().toString());
        return new Request(session, path, bundle, HttpMethod.POST, callback);
    }

    /**
     * Creates a new Request configured to update a user owned Open Graph object.
     *
     * @param session
     *            the Session to use, or null; if non-null, the session must be in an opened state
     * @param id
     *            the id of the Open Graph object
     * @param title
     *            the title of the Open Graph object
     * @param imageUrl
     *            the link to an image to be associated with the Open Graph object
     * @param url
     *            the url to be associated with the Open Graph object
     * @param description
     *            the description to be associated with the object
     * @param objectProperties
     *            any additional type-specific properties for the Open Graph 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
     */
    public static Request newUpdateOpenGraphObjectRequest(Session session, String id, String title, String imageUrl,
            String url, String description, GraphObject objectProperties, Callback callback) {
        OpenGraphObject openGraphObject = OpenGraphObject.Factory.createForPost(OpenGraphObject.class, null, title,
                imageUrl, url, description);
        openGraphObject.setId(id);
        openGraphObject.setData(objectProperties);

        return newUpdateOpenGraphObjectRequest(session, openGraphObject, 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.
     *
     * @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 version of the API that this request will use.  By default this is the current API at the time
     * the SDK is released.
     *
     * @return the version that this request will use
     */
    public final String getVersion() {
        return this.version;
    }

    /**
     * Set the version to use for this request.  By default the version will be the current API at the time the SDK
     * is released.  Only use this if you need to explicitly override.
     *
     * @param version The version to use.  Should look like "v2.0"
     */
    public final void setVersion(String version) {
        this.version = version;
    }

    /**
     * This is an internal function that is not meant to be used by developers.
     */
    public final void setSkipClientToken(boolean skipClientToken) {
        this.skipClientToken = skipClientToken;
    }

    /**
     * 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 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) {
        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;
    }

    /**
     * Sets the tag on the request; this is an application-defined object that can be used to distinguish
     * between different requests. Its value has no effect on the execution of the request.
     *
     * @param tag an object to serve as a tag, or null
     */
    public final void setTag(Object tag) {
        this.tag = tag;
    }

    /**
     * Gets the tag on the request; this is an application-defined object that can be used to distinguish
     * between different requests. Its value has no effect on the execution of the request.
     *
     * @return an object that serves as a tag, or null
     */
    public final Object getTag() {
        return tag;
    }

    /**
     * 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.
     *
     * This method is deprecated. Prefer to call Request.newPostRequest(...).executeAsync();
     *
     * @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
     */
    @Deprecated
    public static RequestAsyncTask executePostRequestAsync(Session session, String graphPath, GraphObject graphObject,
            Callback callback) {
        return newPostRequest(session, graphPath, graphObject, callback).executeAsync();
    }

    /**
     * Starts a new Request configured to retrieve a user's own profile.
     * <p/>
     * This should only be called from the UI thread.
     *
     * This method is deprecated. Prefer to call Request.newMeRequest(...).executeAsync();
     *
     * @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
     */
    @Deprecated
    public static RequestAsyncTask executeMeRequestAsync(Session session, GraphUserCallback callback) {
        return newMeRequest(session, callback).executeAsync();
    }

    /**
     * Starts a new Request configured to retrieve a user's friend list.
     * <p/>
     * This should only be called from the UI thread.
     *
     * This method is deprecated. Prefer to call Request.newMyFriendsRequest(...).executeAsync();
     *
     * @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
     */
    @Deprecated
    public static RequestAsyncTask executeMyFriendsRequestAsync(Session session, GraphUserListCallback callback) {
        return newMyFriendsRequest(session, callback).executeAsync();
    }

    /**
     * Starts 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.
     *
     * This method is deprecated. Prefer to call Request.newUploadPhotoRequest(...).executeAsync();
     *
     * @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
     */
    @Deprecated
    public static RequestAsyncTask executeUploadPhotoRequestAsync(Session session, Bitmap image, Callback callback) {
        return newUploadPhotoRequest(session, image, callback).executeAsync();
    }

    /**
     * Starts 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.
     *
     * This method is deprecated. Prefer to call Request.newUploadPhotoRequest(...).executeAsync();
     *
     * @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
     */
    @Deprecated
    public static RequestAsyncTask executeUploadPhotoRequestAsync(Session session, File file,
            Callback callback) throws FileNotFoundException {
        return newUploadPhotoRequest(session, file, callback).executeAsync();
    }

    /**
     * Starts a new Request configured to retrieve a particular graph path.
     * <p/>
     * This should only be called from the UI thread.
     *
     * This method is deprecated. Prefer to call Request.newGraphPathRequest(...).executeAsync();
     *
     * @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
     */
    @Deprecated
    public static RequestAsyncTask executeGraphPathRequestAsync(Session session, String graphPath, Callback callback) {
        return newGraphPathRequest(session, graphPath, callback).executeAsync();
    }

    /**
     * Starts 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.
     *
     * This method is deprecated. Prefer to call Request.newPlacesSearchRequest(...).executeAsync();
     *
     * @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
     */
    @Deprecated
    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();
    }

    /**
     * Starts a new Request configured to post a status update to a user's feed.
     * <p/>
     * This should only be called from the UI thread.
     *
     * This method is deprecated. Prefer to call Request.newStatusUpdateRequest(...).executeAsync();
     *
     * @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
     */
    @Deprecated
    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) {

        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.getGraphUrlBase());
            }
        } 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(", 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.setRequestProperty(ACCEPT_LANGUAGE_HEADER, Locale.getDefault().toString());

        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);
            }
        } else if (!skipClientToken && !this.parameters.containsKey(ACCESS_TOKEN_PARAM)) {
            String appID = Settings.getApplicationId();
            String clientToken = Settings.getClientToken();
            if (!Utility.isNullOrEmpty(appID) && !Utility.isNullOrEmpty(clientToken)) {
                String accessToken = appID + "|" + clientToken;
                this.parameters.putString(ACCESS_TOKEN_PARAM, accessToken);
            } else {
                Log.d(TAG,
                        "Warning: Sessionless Request needs token but missing either application ID or client token.");
            }
        }
        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 = getGraphPathWithVersion();
        addCommonParameters();
        return appendParametersToBaseUrl(baseUrl);
    }

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

        String graphBaseUrlBase;
        if (this.getHttpMethod() == HttpMethod.POST && graphPath != null && graphPath.endsWith(VIDEOS_SUFFIX)) {
            graphBaseUrlBase = ServerProtocol.getGraphVideoUrlBase();
        } else {
            graphBaseUrlBase = ServerProtocol.getGraphUrlBase();
        }
        String baseUrl = String.format("%s/%s", graphBaseUrlBase, getGraphPathWithVersion());

        addCommonParameters();
        return appendParametersToBaseUrl(baseUrl);
    }

    private String getGraphPathWithVersion() {
        Matcher matcher = versionPattern.matcher(this.graphPath);
        if (matcher.matches()) {
            return this.graphPath;
        }
        return String.format("%s/%s", this.version, this.graphPath);
    }

    private static class Attachment {
        private final Request request;
        private final Object value;

        public Attachment(Request request, Object value) {
            this.request = request;
            this.value = value;
        }

        public Request getRequest() {
            return request;
        }

        public Object getValue() {
            return value;
        }
    }

    private void serializeToBatch(JSONArray batch, Map<String, Attachment> 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);
                attachments.put(name, new Attachment(this, 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 static boolean hasOnProgressCallbacks(RequestBatch requests) {
        for (RequestBatch.Callback callback : requests.getCallbacks()) {
            if (callback instanceof RequestBatch.OnProgressCallback) {
                return true;
            }
        }

        for (Request request : requests) {
            if (request.getCallback() instanceof OnProgressCallback) {
                return true;
            }
        }

        return false;
    }

    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);

        OutputStream outputStream = null;
        try {
            if (hasOnProgressCallbacks(requests)) {
                ProgressNoopOutputStream countingStream = null;
                countingStream = new ProgressNoopOutputStream(requests.getCallbackHandler());
                processRequest(requests, null, numRequests, url, countingStream);

                int max = countingStream.getMaxProgress();
                Map<Request, RequestProgress> progressMap = countingStream.getProgressMap();

                BufferedOutputStream buffered = new BufferedOutputStream(connection.getOutputStream());
                outputStream = new ProgressOutputStream(buffered, requests, progressMap, max);
            }
            else {
                outputStream = new BufferedOutputStream(connection.getOutputStream());
            }

            processRequest(requests, logger, numRequests, url, outputStream);
        }
        finally {
            if (outputStream != null) {
                outputStream.close();
            }
        }

        logger.log();
    }

    private static void processRequest(RequestBatch requests, Logger logger, int numRequests, URL url, OutputStream outputStream)
            throws IOException, JSONException
    {
        Serializer serializer = new Serializer(outputStream, logger);

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

            Map<String, Attachment> attachments = new HashMap<String, Attachment>();
            for(String key : request.parameters.keySet()) {
                Object value = request.parameters.get(key);
                if (isSupportedAttachmentType(value)) {
                    attachments.put(key, new Attachment(request, value));
                }
            }

            if (logger != null) {
                logger.append("  Parameters:\n");
            }
            serializeParameters(request.parameters, serializer, request);

            if (logger != null) {
                logger.append("  Attachments:\n");
            }
            serializeAttachments(attachments, 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.
            Map<String, Attachment> attachments = new HashMap<String, Attachment>();
            serializeRequestsAsJSON(serializer, requests, attachments);

            if (logger != null) {
                logger.append("  Attachments:\n");
            }
            serializeAttachments(attachments, serializer);
        }
    }

    private static boolean isMeRequest(String path) {
        Matcher matcher = versionPattern.matcher(path);
        if (matcher.matches()) {
            // Group 1 contains the path aside from version
            path = matcher.group(1);
        }
        if (path.startsWith("me/") || path.startsWith("/me/")) {
            return true;
        }
        return false;
    }

    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 (isMeRequest(path)) {
            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 (jsonObject.has(NativeProtocol.OPEN_GRAPH_CREATE_OBJECT_KEY)) {
                    processGraphObjectProperty(key, jsonObject.toString(), 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, Request request) throws IOException {
        Set<String> keys = bundle.keySet();

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

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

        for (String key : keys) {
            Attachment attachment = attachments.get(key);
            if (isSupportedAttachmentType(attachment.getValue())) {
                serializer.writeObject(key, attachment.getValue(), attachment.getRequest());
            }
        }
    }

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

        serializer.writeRequestsAsJson(BATCH_PARAM, batch, requests);
    }

    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 ||
                value instanceof ParcelFileDescriptorWithMimeType;
    }

    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 OutputStream outputStream;
        private final Logger logger;
        private boolean firstWrite = true;

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

        public void writeObject(String key, Object value, Request request) throws IOException {
            if (outputStream instanceof RequestOutputStream) {
                ((RequestOutputStream) outputStream).setCurrentRequest(request);
            }

            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, null);
            } else if (value instanceof ParcelFileDescriptorWithMimeType) {
                writeFile(key, (ParcelFileDescriptorWithMimeType) value);
            } else {
                throw new IllegalArgumentException("value is not a supported type: String, Bitmap, byte[]");
            }
        }

        public void writeRequestsAsJson(String key, JSONArray requestJsonArray, Collection<Request> requests)
                throws IOException, JSONException {
            if (! (outputStream instanceof RequestOutputStream)) {
                writeString(key, requestJsonArray.toString());
                return;
            }

            RequestOutputStream requestOutputStream = (RequestOutputStream) outputStream;
            writeContentDisposition(key, null, null);
            write("[");
            int i = 0;
            for (Request request : requests) {
                JSONObject requestJson = requestJsonArray.getJSONObject(i);
                requestOutputStream.setCurrentRequest(request);
                if (i > 0) {
                    write(",%s", requestJson.toString());
                } else {
                    write("%s", requestJson.toString());
                }
                i++;
            }
            write("]");
            if (logger != null) {
                logger.appendKeyValue("    " + key, requestJsonArray.toString());
            }
        }

        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();
            if (logger != null) {
                logger.appendKeyValue("    " + key, "<Image>");
            }
        }

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

        public void writeFile(String key, ParcelFileDescriptorWithMimeType descriptorWithMimeType) throws IOException {
            writeFile(key, descriptorWithMimeType.getFileDescriptor(), descriptorWithMimeType.getMimeType());
        }

        public void writeFile(String key, ParcelFileDescriptor descriptor, String mimeType) throws IOException {
            if (mimeType == null) {
                mimeType = "content/unknown";
            }
            writeContentDisposition(key, key, mimeType);

            int totalBytes = 0;

            if (outputStream instanceof ProgressNoopOutputStream) {
                // If we are only counting bytes then skip reading the file
                ((ProgressNoopOutputStream) outputStream).addProgress(descriptor.getStatSize());
            }
            else {
                ParcelFileDescriptor.AutoCloseInputStream inputStream = null;
                BufferedInputStream bufferedInputStream = null;
                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();
            if (logger != null) {
                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 the Request class can implement in order to be notified when a
     * progress is made on a particular request. The frequency of the callbacks can be controlled using
     * {@link com.facebook.Settings#setOnProgressThreshold(long)}
     */
    public interface OnProgressCallback extends Callback {
        /**
         * The method that will be called when progress is made.
         *
         * @param current
         *            the current value of the progress of the request.
         * @param max
         *            the maximum value (target) value that the progress will have.
         */
        void onProgress(long current, long max);
    }

    /**
     * Specifies the interface that consumers of
     * {@link Request#executeMeRequestAsync(Session, com.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.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.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);
    }

    private static class ParcelFileDescriptorWithMimeType implements Parcelable {
        private final String mimeType;
        private final ParcelFileDescriptor fileDescriptor;

        public String getMimeType() {
            return mimeType;
        }

        public ParcelFileDescriptor getFileDescriptor() {
            return fileDescriptor;
        }

        public int describeContents() {
            return CONTENTS_FILE_DESCRIPTOR;
        }

        public void writeToParcel(Parcel out, int flags) {
            out.writeString(mimeType);
            out.writeFileDescriptor(fileDescriptor.getFileDescriptor());
        }

        @SuppressWarnings("unused")
        public static final Parcelable.Creator<ParcelFileDescriptorWithMimeType> CREATOR
                = new Parcelable.Creator<ParcelFileDescriptorWithMimeType>() {
            public ParcelFileDescriptorWithMimeType createFromParcel(Parcel in) {
                return new ParcelFileDescriptorWithMimeType(in);
            }

            public ParcelFileDescriptorWithMimeType[] newArray(int size) {
                return new ParcelFileDescriptorWithMimeType[size];
            }
        };

        public ParcelFileDescriptorWithMimeType(ParcelFileDescriptor fileDescriptor, String mimeType) {
            this.mimeType = mimeType;
            this.fileDescriptor = fileDescriptor;
        }

        private ParcelFileDescriptorWithMimeType(Parcel in) {
            mimeType = in.readString();
            fileDescriptor = in.readFileDescriptor();
        }
    }
}
