content: /**
   * 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 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;
  
      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;
  
      private static Pattern versionPattern = Pattern.compile("^v\\d+\\.\\d+/.*");
  
      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;
      private Object tag;
      private String version;
  
      /**
       * 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 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) {
          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.
       *
       * @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.
       *
       * @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. 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 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;
      }
  
      /**
       * 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) {
          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 make a call to the Facebook REST API.
       * <p/>
       * This should only be called from the UI thread.
       *
       * This method is deprecated. Prefer to call Request.newRestRequest(...).executeAsync();
       *
       * @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
       */
      @Deprecated
      public static RequestAsyncTask executeRestRequestAsync(Session session, String restMethod, Bundle parameters,
              HttpMethod httpMethod) {
          return newRestRequest(session, restMethod, parameters, httpMethod).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) {
  
          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.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(", 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.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 (!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;
          if (this.restMethod != null) {
              baseUrl = getRestPathWithVersion();
          } else {
              baseUrl = getGraphPathWithVersion();
          }
  
          addCommonParameters();
          return appendParametersToBaseUrl(baseUrl);
      }
  
      final String getUrlForSingleRequest() {
          if (overriddenURL != null) {
              return overriddenURL.toString();
          }
  
          String baseUrl;
          if (this.restMethod != null) {
              baseUrl = String.format("%s/%s", ServerProtocol.getRestUrlBase(), getRestPathWithVersion());
          } else {
              String graphBaseUrlBase;
              if (this.getHttpMethod() == HttpMethod.POST && graphPath != null && graphPath.endsWith(VIDEOS_SUFFIX)) {
                  graphBaseUrlBase = ServerProtocol.getGraphVideoUrlBase();
              } else {
                  graphBaseUrlBase = ServerProtocol.getGraphUrlBase();
              }
              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 String getRestPathWithVersion() {
          Matcher matcher = versionPattern.matcher(this.restMethod);
          if (matcher.matches()) {
              return this.restMethod;
          }
          return String.format("%s/%s/%s", this.version, ServerProtocol.REST_METHOD_BASE, this.restMethod);
      }
  
      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 void validate() {
          if (graphPath != null && restMethod != null) {
              throw new IllegalArgumentException("Only one of a graph path or REST method may be specified per request.");
          }
      }
  
      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 {
              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 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, 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();
          }
      }
  }
  