Android Open Source - CCSocialNetwork Request






From Project

Back to project page CCSocialNetwork.

License

The source code is released under:

GNU Lesser General Public License

If you think the Android project CCSocialNetwork listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/**
 * Copyright 2010-present Facebook.//from w ww  .j a v a  2s .c  o m
 *
 * 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.graphics.Bitmap;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Pair;
import com.facebook.internal.ServerProtocol;
import com.facebook.model.*;
import com.facebook.internal.Logger;
import com.facebook.internal.Utility;
import com.facebook.internal.Validate;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

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

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

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

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

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

    private static final String MIME_BOUNDARY = "3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f";

    private static String defaultBatchApplicationId;

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

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

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

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

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

        setHttpMethod(httpMethod);

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

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

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

        setHttpMethod(HttpMethod.GET);

        this.parameters = new Bundle();
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return toHttpConnection(new RequestBatch(requests));
    }


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

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

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

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

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

        return connection;
    }

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

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

        return responses.get(0);
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

        Utility.disconnectQuietly(connection);

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

        runCallbacks(requests, responses);

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

        return responses;
    }

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

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

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

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

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

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

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

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

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

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

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

        connection.setChunkedStreamingMode(0);
        return connection;
    }


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

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

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

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

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

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

        return uriBuilder.toString();
    }

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

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

        addCommonParameters();
        return appendParametersToBaseUrl(baseUrl);
    }

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

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

        addCommonParameters();
        return appendParametersToBaseUrl(baseUrl);
    }


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

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

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

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

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

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

        batch.put(batchEntry);
    }

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

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

        int numRequests = requests.size();

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

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

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

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

        connection.setDoOutput(true);

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

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

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

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

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

                serializer.writeString(BATCH_APP_ID_PARAM, batchAppID);

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

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

        logger.log();
    }

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

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

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

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

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

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

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

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

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

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

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

    private static volatile String userAgent;

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

        return userAgent;
    }

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

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

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

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

        return data.castToListOf(clazz);
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    }

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

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




Java Source Code List

com.facebook.AccessTokenSource.java
com.facebook.AccessToken.java
com.facebook.AuthorizationClient.java
com.facebook.FacebookAuthorizationException.java
com.facebook.FacebookDialogException.java
com.facebook.FacebookException.java
com.facebook.FacebookGraphObjectException.java
com.facebook.FacebookOperationCanceledException.java
com.facebook.FacebookRequestError.java
com.facebook.FacebookSdkVersion.java
com.facebook.FacebookServiceException.java
com.facebook.GetTokenClient.java
com.facebook.HttpMethod.java
com.facebook.InsightsLogger.java
com.facebook.LegacyHelper.java
com.facebook.LoggingBehavior.java
com.facebook.LoginActivity.java
com.facebook.NativeProtocol.java
com.facebook.NonCachingTokenCachingStrategy.java
com.facebook.RequestAsyncTask.java
com.facebook.RequestBatch.java
com.facebook.Request.java
com.facebook.Response.java
com.facebook.SessionDefaultAudience.java
com.facebook.SessionLoginBehavior.java
com.facebook.SessionState.java
com.facebook.Session.java
com.facebook.Settings.java
com.facebook.SharedPreferencesTokenCachingStrategy.java
com.facebook.TestSession.java
com.facebook.TokenCachingStrategy.java
com.facebook.UiLifecycleHelper.java
com.facebook.android.AsyncFacebookRunner.java
com.facebook.android.DialogError.java
com.facebook.android.FacebookError.java
com.facebook.android.Facebook.java
com.facebook.android.FbDialog.java
com.facebook.android.Util.java
com.facebook.internal.CacheableRequestBatch.java
com.facebook.internal.FileLruCache.java
com.facebook.internal.Logger.java
com.facebook.internal.ServerProtocol.java
com.facebook.internal.SessionAuthorizationType.java
com.facebook.internal.SessionTracker.java
com.facebook.internal.Utility.java
com.facebook.internal.Validate.java
com.facebook.internal.package-info.java
com.facebook.model.GraphLocation.java
com.facebook.model.GraphMultiResult.java
com.facebook.model.GraphObjectList.java
com.facebook.model.GraphObject.java
com.facebook.model.GraphPlace.java
com.facebook.model.GraphUser.java
com.facebook.model.JsonUtil.java
com.facebook.model.OpenGraphAction.java
com.facebook.model.PropertyName.java
com.facebook.widget.FacebookFragment.java
com.facebook.widget.FriendPickerFragment.java
com.facebook.widget.GraphObjectAdapter.java
com.facebook.widget.GraphObjectCursor.java
com.facebook.widget.GraphObjectPagingLoader.java
com.facebook.widget.ImageDownloader.java
com.facebook.widget.ImageRequest.java
com.facebook.widget.ImageResponseCache.java
com.facebook.widget.ImageResponse.java
com.facebook.widget.LoginButton.java
com.facebook.widget.PickerFragment.java
com.facebook.widget.PlacePickerFragment.java
com.facebook.widget.ProfilePictureView.java
com.facebook.widget.SimpleGraphObjectCursor.java
com.facebook.widget.UrlRedirectCache.java
com.facebook.widget.UserSettingsFragment.java
com.facebook.widget.WebDialog.java
com.facebook.widget.WorkQueue.java
org.example.SocialNetwork.CCSocialNetwork.java
org.example.SocialNetwork.EmailSender.java
org.example.SocialNetwork.InternetConnection.java