Blame | Last modification | View Log | RSS feed
/*** Copyright 2010-present Facebook.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package com.facebook;import android.content.Context;import android.graphics.Bitmap;import android.location.Location;import android.net.Uri;import android.os.*;import android.text.TextUtils;import android.util.Log;import android.util.Pair;import com.facebook.internal.*;import com.facebook.model.*;import org.json.JSONArray;import org.json.JSONException;import org.json.JSONObject;import java.io.*;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;import java.net.URLEncoder;import java.text.SimpleDateFormat;import java.util.*;import java.util.Map.Entry;import java.util.regex.Matcher;import java.util.regex.Pattern;/*** A single request to be sent to the Facebook Platform through the <a* href="https://developers.facebook.com/docs/reference/api/">Graph API</a>. The Request class provides functionality* relating to serializing and deserializing requests and responses, making calls in batches (with a single round-trip* to the service) and making calls asynchronously.** The particular service endpoint that a request targets is determined by a graph path (see the* {@link #setGraphPath(String) setGraphPath} method).** A Request can be executed either anonymously or representing an authenticated user. In the former case, no Session* needs to be specified, while in the latter, a Session that is in an opened state must be provided. If requests are* executed in a batch, a Facebook application ID must be associated with the batch, either by supplying a Session for* at least one of the requests in the batch (the first one found in the batch will be used) or by calling the* {@link #setDefaultBatchApplicationId(String) setDefaultBatchApplicationId} method.** After completion of a request, its Session, if any, will be checked to determine if its Facebook access token needs* to be extended; if so, a request to extend it will be issued in the background.*/public class Request {/*** The maximum number of requests that can be submitted in a single batch. This limit is enforced on the service* side by the Facebook platform, not by the Request class.*/public static final int MAXIMUM_BATCH_SIZE = 50;public static final String TAG = Request.class.getSimpleName();private static final String ME = "me";private static final String MY_FRIENDS = "me/friends";private static final String MY_PHOTOS = "me/photos";private static final String MY_VIDEOS = "me/videos";private static final String VIDEOS_SUFFIX = "/videos";private static final String SEARCH = "search";private static final String MY_FEED = "me/feed";private static final String MY_STAGING_RESOURCES = "me/staging_resources";private static final String MY_OBJECTS_FORMAT = "me/objects/%s";private static final String MY_ACTION_FORMAT = "me/%s";private static final String USER_AGENT_BASE = "FBAndroidSDK";private static final String USER_AGENT_HEADER = "User-Agent";private static final String CONTENT_TYPE_HEADER = "Content-Type";private static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language";// Parameter names/valuesprivate static final String PICTURE_PARAM = "picture";private static final String FORMAT_PARAM = "format";private static final String FORMAT_JSON = "json";private static final String SDK_PARAM = "sdk";private static final String SDK_ANDROID = "android";private static final String ACCESS_TOKEN_PARAM = "access_token";private static final String BATCH_ENTRY_NAME_PARAM = "name";private static final String BATCH_ENTRY_OMIT_RESPONSE_ON_SUCCESS_PARAM = "omit_response_on_success";private static final String BATCH_ENTRY_DEPENDS_ON_PARAM = "depends_on";private static final String BATCH_APP_ID_PARAM = "batch_app_id";private static final String BATCH_RELATIVE_URL_PARAM = "relative_url";private static final String BATCH_BODY_PARAM = "body";private static final String BATCH_METHOD_PARAM = "method";private static final String BATCH_PARAM = "batch";private static final String ATTACHMENT_FILENAME_PREFIX = "file";private static final String ATTACHED_FILES_PARAM = "attached_files";private static final String ISO_8601_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ssZ";private static final String STAGING_PARAM = "file";private static final String OBJECT_PARAM = "object";private static final String MIME_BOUNDARY = "3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f";private static String defaultBatchApplicationId;// Group 1 in the pattern is the path without the version infoprivate static Pattern versionPattern = Pattern.compile("^/?v\\d+\\.\\d+/(.*)");private Session session;private HttpMethod httpMethod;private String graphPath;private GraphObject graphObject;private String batchEntryName;private String batchEntryDependsOn;private boolean batchEntryOmitResultOnSuccess = true;private Bundle parameters;private Callback callback;private String overriddenURL;private Object tag;private String version;private boolean skipClientToken = false;/*** Constructs a request without a session, graph path, or any other parameters.*/public Request() {this(null, null, null, null, null);}/*** Constructs a request with a Session to retrieve a particular graph path. A Session need not be provided, in which* case the request is sent without an access token and thus is not executed in the context of any particular user.* Only certain graph requests can be expected to succeed in this case. If a Session is provided, it must be in an* opened state or the request will fail.** @param session* the Session to use, or null* @param graphPath* the graph path to retrieve*/public Request(Session session, String graphPath) {this(session, graphPath, null, null, null);}/*** Constructs a request with a specific Session, graph path, parameters, and HTTP method. A Session need not be* provided, in which case the request is sent without an access token and thus is not executed in the context of* any particular user. Only certain graph requests can be expected to succeed in this case. If a Session is* provided, it must be in an opened state or the request will fail.** Depending on the httpMethod parameter, the object at the graph path may be retrieved, created, or deleted.** @param session* the Session to use, or null* @param graphPath* the graph path to retrieve, create, or delete* @param parameters* additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers,* Bitmaps, Dates, or Byte arrays.* @param httpMethod* the {@link HttpMethod} to use for the request, or null for default (HttpMethod.GET)*/public Request(Session session, String graphPath, Bundle parameters, HttpMethod httpMethod) {this(session, graphPath, parameters, httpMethod, null);}/*** Constructs a request with a specific Session, graph path, parameters, and HTTP method. A Session need not be* provided, in which case the request is sent without an access token and thus is not executed in the context of* any particular user. Only certain graph requests can be expected to succeed in this case. If a Session is* provided, it must be in an opened state or the request will fail.** Depending on the httpMethod parameter, the object at the graph path may be retrieved, created, or deleted.** @param session* the Session to use, or null* @param graphPath* the graph path to retrieve, create, or delete* @param parameters* additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers,* Bitmaps, Dates, or Byte arrays.* @param httpMethod* the {@link HttpMethod} to use for the request, or null for default (HttpMethod.GET)* @param callback* a callback that will be called when the request is completed to handle success or error conditions*/public Request(Session session, String graphPath, Bundle parameters, HttpMethod httpMethod, Callback callback) {this(session, graphPath, parameters, httpMethod, callback, null);}/*** Constructs a request with a specific Session, graph path, parameters, and HTTP method. A Session need not be* provided, in which case the request is sent without an access token and thus is not executed in the context of* any particular user. Only certain graph requests can be expected to succeed in this case. If a Session is* provided, it must be in an opened state or the request will fail.** Depending on the httpMethod parameter, the object at the graph path may be retrieved, created, or deleted.** @param session* the Session to use, or null* @param graphPath* the graph path to retrieve, create, or delete* @param parameters* additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers,* Bitmaps, Dates, or Byte arrays.* @param httpMethod* the {@link HttpMethod} to use for the request, or null for default (HttpMethod.GET)* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @param version* the version of the Graph API*/public Request(Session session, String graphPath, Bundle parameters, HttpMethod httpMethod, Callback callback, String version) {this.session = session;this.graphPath = graphPath;this.callback = callback;this.version = version;setHttpMethod(httpMethod);if (parameters != null) {this.parameters = new Bundle(parameters);} else {this.parameters = new Bundle();}if (this.version == null) {this.version = ServerProtocol.getAPIVersion();}}Request(Session session, URL overriddenURL) {this.session = session;this.overriddenURL = overriddenURL.toString();setHttpMethod(HttpMethod.GET);this.parameters = new Bundle();}/*** Creates a new Request configured to post a GraphObject to a particular graph path, to either create or update the* object at that path.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param graphPath* the graph path to retrieve, create, or delete* @param graphObject* the GraphObject to create or update* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/public static Request newPostRequest(Session session, String graphPath, GraphObject graphObject, Callback callback) {Request request = new Request(session, graphPath, null, HttpMethod.POST , callback);request.setGraphObject(graphObject);return request;}/*** Creates a new Request configured to retrieve a user's own profile.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/public static Request newMeRequest(Session session, final GraphUserCallback callback) {Callback wrapper = new Callback() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic void onCompleted(Response response) {if (callback != null) {callback.onCompleted(typedListFromResponse(response, GraphPlace.class), response);}}};return new Request(session, SEARCH, parameters, HttpMethod.GET, wrapper);}/*** Creates a new Request configured to post a status update to a user's feed.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param message* the text of the status update* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/public static Request newStatusUpdateRequest(Session session, String message, Callback callback) {return newStatusUpdateRequest(session, message, (String)null, null, callback);}/*** Creates a new Request configured to post a status update to a user's feed.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param message* the text of the status update* @param placeId* an optional place id to associate with the post* @param tagIds* an optional list of user ids to tag in the post* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/private static Request newStatusUpdateRequest(Session session, String message, String placeId, List<String> tagIds,Callback callback) {Bundle parameters = new Bundle();parameters.putString("message", message);if (placeId != null) {parameters.putString("place", placeId);}if (tagIds != null && tagIds.size() > 0) {String tags = TextUtils.join(",", tagIds);parameters.putString("tags", tags);}return new Request(session, MY_FEED, parameters, HttpMethod.POST, callback);}/*** Creates a new Request configured to post a status update to a user's feed.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param message* the text of the status update* @param place* an optional place to associate with the post* @param tags* an optional list of users to tag in the post* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/public static Request newStatusUpdateRequest(Session session, String message, GraphPlace place,List<GraphUser> tags, Callback callback) {List<String> tagIds = null;if (tags != null) {tagIds = new ArrayList<String>(tags.size());for (GraphUser tag: tags) {tagIds.add(tag.getId());}}String placeId = place == null ? null : place.getId();return newStatusUpdateRequest(session, message, placeId, tagIds, callback);}/*** Creates a new Request configured to retrieve an App User ID for the app's Facebook user. Callers* will send this ID back to their own servers, collect up a set to create a Facebook Custom Audience with,* and then use the resultant Custom Audience to target ads.* <p/>* The GraphObject in the response will include an "custom_audience_third_party_id" property, with the value* being the ID retrieved. This ID is an encrypted encoding of the Facebook user's ID and the* invoking Facebook app ID. Multiple calls with the same user will return different IDs, thus these IDs cannot be* used to correlate behavior across devices or applications, and are only meaningful when sent back to Facebook* for creating Custom Audiences.* <p/>* The ID retrieved represents the Facebook user identified in the following way: if the specified session* (or activeSession if the specified session is `null`) is open, the ID will represent the user associated with* the activeSession; otherwise the ID will represent the user logged into the native Facebook app on the device.* A `null` ID will be provided into the callback if a) there is no native Facebook app, b) no one is logged into* it, or c) the app has previously called* {@link Settings#setLimitEventAndDataUsage(android.content.Context, boolean)} with `true` for this user.* <b>You must call this method from a background thread for it to work properly.</b>** @param session* the Session to issue the Request on, or null; if non-null, the session must be in an opened state.* If there is no logged-in Facebook user, null is the expected choice.* @param context* the Application context from which the app ID will be pulled, and from which the 'attribution ID'* for the Facebook user is determined. If there has been no app ID set, an exception will be thrown.* @param callback* a callback that will be called when the request is completed to handle success or error conditions.* The GraphObject in the Response will contain a "custom_audience_third_party_id" property that* represents the user as described above.* @return a Request that is ready to execute*/public static Request newCustomAudienceThirdPartyIdRequest(Session session, Context context, Callback callback) {return newCustomAudienceThirdPartyIdRequest(session, context, null, callback);}/*** Creates a new Request configured to retrieve an App User ID for the app's Facebook user. Callers* will send this ID back to their own servers, collect up a set to create a Facebook Custom Audience with,* and then use the resultant Custom Audience to target ads.* <p/>* The GraphObject in the response will include an "custom_audience_third_party_id" property, with the value* being the ID retrieved. This ID is an encrypted encoding of the Facebook user's ID and the* invoking Facebook app ID. Multiple calls with the same user will return different IDs, thus these IDs cannot be* used to correlate behavior across devices or applications, and are only meaningful when sent back to Facebook* for creating Custom Audiences.* <p/>* The ID retrieved represents the Facebook user identified in the following way: if the specified session* (or activeSession if the specified session is `null`) is open, the ID will represent the user associated with* the activeSession; otherwise the ID will represent the user logged into the native Facebook app on the device.* A `null` ID will be provided into the callback if a) there is no native Facebook app, b) no one is logged into* it, or c) the app has previously called* {@link Settings#setLimitEventAndDataUsage(android.content.Context, boolean)} ;} with `true` for this user.* <b>You must call this method from a background thread for it to work properly.</b>** @param session* the Session to issue the Request on, or null; if non-null, the session must be in an opened state.* If there is no logged-in Facebook user, null is the expected choice.* @param context* the Application context from which the app ID will be pulled, and from which the 'attribution ID'* for the Facebook user is determined. If there has been no app ID set, an exception will be thrown.* @param applicationId* explicitly specified Facebook App ID. If null, and there's a valid session, then the application ID* from the session will be used, otherwise the application ID from metadata will be used.* @param callback* a callback that will be called when the request is completed to handle success or error conditions.* The GraphObject in the Response will contain a "custom_audience_third_party_id" property that* represents the user as described above.* @return a Request that is ready to execute*/public static Request newCustomAudienceThirdPartyIdRequest(Session session,Context context, String applicationId, Callback callback) {// if provided session or activeSession is opened, use it.if (session == null) {session = Session.getActiveSession();}if (session != null && !session.isOpened()) {session = null;}if (applicationId == null) {if (session != null) {applicationId = session.getApplicationId();} else {applicationId = Utility.getMetadataApplicationId(context);}}if (applicationId == null) {throw new FacebookException("Facebook App ID cannot be determined");}String endpoint = applicationId + "/custom_audience_third_party_id";AttributionIdentifiers attributionIdentifiers = AttributionIdentifiers.getAttributionIdentifiers(context);Bundle parameters = new Bundle();if (session == null) {// Only use the attributionID if we don't have an open session. If we do have an open session, then// the user token will be used to identify the user, and is more reliable than the attributionID.String udid = attributionIdentifiers.getAttributionId() != null? attributionIdentifiers.getAttributionId(): attributionIdentifiers.getAndroidAdvertiserId();if (attributionIdentifiers.getAttributionId() != null) {parameters.putString("udid", udid);}}// Server will choose to not provide the App User ID in the event that event usage has been limited for// this user for this app.if (Settings.getLimitEventAndDataUsage(context) || attributionIdentifiers.isTrackingLimited()) {parameters.putString("limit_event_usage", "1");}return new Request(session, endpoint, parameters, HttpMethod.GET, callback);}/*** Creates a new Request configured to upload an image to create a staging resource. Staging resources* allow you to post binary data such as images, in preparation for a post of an Open Graph object or action* which references the image. The URI returned when uploading a staging resource may be passed as the image* property for an Open Graph object or action.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param image* the image to upload* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/public static Request newUploadStagingResourceWithImageRequest(Session session,Bitmap image, Callback callback) {Bundle parameters = new Bundle(1);parameters.putParcelable(STAGING_PARAM, image);return new Request(session, MY_STAGING_RESOURCES, parameters, HttpMethod.POST, callback);}/*** Creates a new Request configured to upload an image to create a staging resource. Staging resources* allow you to post binary data such as images, in preparation for a post of an Open Graph object or action* which references the image. The URI returned when uploading a staging resource may be passed as the image* property for an Open Graph object or action.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param file* the file containing the image to upload* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/public static Request newUploadStagingResourceWithImageRequest(Session session,File file, Callback callback) throws FileNotFoundException {ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);ParcelFileDescriptorWithMimeType descriptorWithMimeType = new ParcelFileDescriptorWithMimeType(descriptor, "image/png");Bundle parameters = new Bundle(1);parameters.putParcelable(STAGING_PARAM, descriptorWithMimeType);return new Request(session, MY_STAGING_RESOURCES, parameters, HttpMethod.POST, callback);}/*** Creates a new Request configured to create a user owned Open Graph object.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param openGraphObject* the Open Graph object to create; must not be null, and must have a non-empty type and title* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/public static Request newPostOpenGraphObjectRequest(Session session,OpenGraphObject openGraphObject, Callback callback) {if (openGraphObject == null) {throw new FacebookException("openGraphObject cannot be null");}if (Utility.isNullOrEmpty(openGraphObject.getType())) {throw new FacebookException("openGraphObject must have non-null 'type' property");}if (Utility.isNullOrEmpty(openGraphObject.getTitle())) {throw new FacebookException("openGraphObject must have non-null 'title' property");}String path = String.format(MY_OBJECTS_FORMAT, openGraphObject.getType());Bundle bundle = new Bundle();bundle.putString(OBJECT_PARAM, openGraphObject.getInnerJSONObject().toString());return new Request(session, path, bundle, HttpMethod.POST, callback);}/*** Creates a new Request configured to create a user owned Open Graph object.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param type* the fully-specified Open Graph object type (e.g., my_app_namespace:my_object_name); must not be null* @param title* the title of the Open Graph object; must not be null* @param imageUrl* the link to an image to be associated with the Open Graph object; may be null* @param url* the url to be associated with the Open Graph object; may be null* @param description* the description to be associated with the object; may be null* @param objectProperties* any additional type-specific properties for the Open Graph object; may be null* @param callback* a callback that will be called when the request is completed to handle success or error conditions;* may be null* @return a Request that is ready to execute*/public static Request newPostOpenGraphObjectRequest(Session session, String type, String title, String imageUrl,String url, String description, GraphObject objectProperties, Callback callback) {OpenGraphObject openGraphObject = OpenGraphObject.Factory.createForPost(OpenGraphObject.class, type, title,imageUrl, url, description);if (objectProperties != null) {openGraphObject.setData(objectProperties);}return newPostOpenGraphObjectRequest(session, openGraphObject, callback);}/*** Creates a new Request configured to publish an Open Graph action.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param openGraphAction* the Open Graph object to create; must not be null, and must have a non-empty 'type'* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/public static Request newPostOpenGraphActionRequest(Session session, OpenGraphAction openGraphAction,Callback callback) {if (openGraphAction == null) {throw new FacebookException("openGraphAction cannot be null");}if (Utility.isNullOrEmpty(openGraphAction.getType())) {throw new FacebookException("openGraphAction must have non-null 'type' property");}String path = String.format(MY_ACTION_FORMAT, openGraphAction.getType());return newPostRequest(session, path, openGraphAction, callback);}/*** Creates a new Request configured to delete a resource through the Graph API.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param id* the id of the object to delete* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/public static Request newDeleteObjectRequest(Session session, String id, Callback callback) {return new Request(session, id, null, HttpMethod.DELETE, callback);}/*** Creates a new Request configured to update a user owned Open Graph object.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param openGraphObject* the Open Graph object to update, which must have a valid 'id' property* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/public static Request newUpdateOpenGraphObjectRequest(Session session, OpenGraphObject openGraphObject,Callback callback) {if (openGraphObject == null) {throw new FacebookException("openGraphObject cannot be null");}String path = openGraphObject.getId();if (path == null) {throw new FacebookException("openGraphObject must have an id");}Bundle bundle = new Bundle();bundle.putString(OBJECT_PARAM, openGraphObject.getInnerJSONObject().toString());return new Request(session, path, bundle, HttpMethod.POST, callback);}/*** Creates a new Request configured to update a user owned Open Graph object.** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param id* the id of the Open Graph object* @param title* the title of the Open Graph object* @param imageUrl* the link to an image to be associated with the Open Graph object* @param url* the url to be associated with the Open Graph object* @param description* the description to be associated with the object* @param objectProperties* any additional type-specific properties for the Open Graph object* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a Request that is ready to execute*/public static Request newUpdateOpenGraphObjectRequest(Session session, String id, String title, String imageUrl,String url, String description, GraphObject objectProperties, Callback callback) {OpenGraphObject openGraphObject = OpenGraphObject.Factory.createForPost(OpenGraphObject.class, null, title,imageUrl, url, description);openGraphObject.setId(id);openGraphObject.setData(objectProperties);return newUpdateOpenGraphObjectRequest(session, openGraphObject, callback);}/*** Returns the GraphObject, if any, associated with this request.** @return the GraphObject associated with this requeset, or null if there is none*/public final GraphObject getGraphObject() {return this.graphObject;}/*** Sets the GraphObject associated with this request. This is meaningful only for POST requests.** @param graphObject* the GraphObject to upload along with this request*/public final void setGraphObject(GraphObject graphObject) {this.graphObject = graphObject;}/*** Returns the graph path of this request, if any.** @return the graph path of this request, or null if there is none*/public final String getGraphPath() {return this.graphPath;}/*** Sets the graph path of this request.** @param graphPath* the graph path for this request*/public final void setGraphPath(String graphPath) {this.graphPath = graphPath;}/*** Returns the {@link HttpMethod} to use for this request.** @return the HttpMethod*/public final HttpMethod getHttpMethod() {return this.httpMethod;}/*** Sets the {@link HttpMethod} to use for this request.** @param httpMethod* the HttpMethod, or null for the default (HttpMethod.GET).*/public final void setHttpMethod(HttpMethod httpMethod) {if (overriddenURL != null && httpMethod != HttpMethod.GET) {throw new FacebookException("Can't change HTTP method on request with overridden URL.");}this.httpMethod = (httpMethod != null) ? httpMethod : HttpMethod.GET;}/*** Returns the version of the API that this request will use. By default this is the current API at the time* the SDK is released.** @return the version that this request will use*/public final String getVersion() {return this.version;}/*** Set the version to use for this request. By default the version will be the current API at the time the SDK* is released. Only use this if you need to explicitly override.** @param version The version to use. Should look like "v2.0"*/public final void setVersion(String version) {this.version = version;}/*** This is an internal function that is not meant to be used by developers.*/public final void setSkipClientToken(boolean skipClientToken) {this.skipClientToken = skipClientToken;}/*** Returns the parameters for this request.** @return the parameters*/public final Bundle getParameters() {return this.parameters;}/*** Sets the parameters for this request.** @param parameters* the parameters*/public final void setParameters(Bundle parameters) {this.parameters = parameters;}/*** Returns the Session associated with this request.** @return the Session associated with this request, or null if none has been specified*/public final Session getSession() {return this.session;}/*** Sets the Session to use for this request. The Session does not need to be opened at the time it is specified, but* it must be opened by the time the request is executed.** @param session* the Session to use for this request*/public final void setSession(Session session) {this.session = session;}/*** Returns the name of this request's entry in a batched request.** @return the name of this request's batch entry, or null if none has been specified*/public final String getBatchEntryName() {return this.batchEntryName;}/*** Sets the name of this request's entry in a batched request. This value is only used if this request is submitted* as part of a batched request. It can be used to specified dependencies between requests. See <a* href="https://developers.facebook.com/docs/reference/api/batch/">Batch Requests</a> in the Graph API* documentation for more details.** @param batchEntryName* the name of this request's entry in a batched request, which must be unique within a particular batch* of requests*/public final void setBatchEntryName(String batchEntryName) {this.batchEntryName = batchEntryName;}/*** Returns the name of the request that this request entry explicitly depends on in a batched request.** @return the name of this request's dependency, or null if none has been specified*/public final String getBatchEntryDependsOn() {return this.batchEntryDependsOn;}/*** Sets the name of the request entry that this request explicitly depends on in a batched request. This value is* only used if this request is submitted as part of a batched request. It can be used to specified dependencies* between requests. See <a href="https://developers.facebook.com/docs/reference/api/batch/">Batch Requests</a> in* the Graph API documentation for more details.** @param batchEntryDependsOn* the name of the request entry that this entry depends on in a batched request*/public final void setBatchEntryDependsOn(String batchEntryDependsOn) {this.batchEntryDependsOn = batchEntryDependsOn;}/*** Returns whether or not this batch entry will return a response if it is successful. Only applies if another* request entry in the batch specifies this entry as a dependency.** @return the name of this request's dependency, or null if none has been specified*/public final boolean getBatchEntryOmitResultOnSuccess() {return this.batchEntryOmitResultOnSuccess;}/*** Sets whether or not this batch entry will return a response if it is successful. Only applies if another* request entry in the batch specifies this entry as a dependency. See* <a href="https://developers.facebook.com/docs/reference/api/batch/">Batch Requests</a> in the Graph API* documentation for more details.** @param batchEntryOmitResultOnSuccess* the name of the request entry that this entry depends on in a batched request*/public final void setBatchEntryOmitResultOnSuccess(boolean batchEntryOmitResultOnSuccess) {this.batchEntryOmitResultOnSuccess = batchEntryOmitResultOnSuccess;}/*** Gets the default Facebook application ID that will be used to submit batched requests if none of those requests* specifies a Session. Batched requests require an application ID, so either at least one request in a batch must* specify a Session or the application ID must be specified explicitly.** @return the Facebook application ID to use for batched requests if none can be determined*/public static final String getDefaultBatchApplicationId() {return Request.defaultBatchApplicationId;}/*** Sets the default application ID that will be used to submit batched requests if none of those requests specifies* a Session. Batched requests require an application ID, so either at least one request in a batch must specify a* Session or the application ID must be specified explicitly.** @param applicationId* the Facebook application ID to use for batched requests if none can be determined*/public static final void setDefaultBatchApplicationId(String applicationId) {defaultBatchApplicationId = applicationId;}/*** Returns the callback which will be called when the request finishes.** @return the callback*/public final Callback getCallback() {return callback;}/*** Sets the callback which will be called when the request finishes.** @param callback* the callback*/public final void setCallback(Callback callback) {this.callback = callback;}/*** Sets the tag on the request; this is an application-defined object that can be used to distinguish* between different requests. Its value has no effect on the execution of the request.** @param tag an object to serve as a tag, or null*/public final void setTag(Object tag) {this.tag = tag;}/*** Gets the tag on the request; this is an application-defined object that can be used to distinguish* between different requests. Its value has no effect on the execution of the request.** @return an object that serves as a tag, or null*/public final Object getTag() {return tag;}/*** Starts a new Request configured to post a GraphObject to a particular graph path, to either create or update the* object at that path.* <p/>* This should only be called from the UI thread.** This method is deprecated. Prefer to call Request.newPostRequest(...).executeAsync();** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param graphPath* the graph path to retrieve, create, or delete* @param graphObject* the GraphObject to create or update* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a RequestAsyncTask that is executing the request*/@Deprecatedpublic static RequestAsyncTask executePostRequestAsync(Session session, String graphPath, GraphObject graphObject,Callback callback) {return newPostRequest(session, graphPath, graphObject, callback).executeAsync();}/*** Starts a new Request configured to retrieve a user's own profile.* <p/>* This should only be called from the UI thread.** This method is deprecated. Prefer to call Request.newMeRequest(...).executeAsync();** @param session* the Session to use, or null; if non-null, the session must be in an opened state* @param callback* a callback that will be called when the request is completed to handle success or error conditions* @return a RequestAsyncTask that is executing the request*/@Deprecatedpublic 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*/@Deprecatedpublic 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*/@Deprecatedpublic 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*/@Deprecatedpublic 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*/@Deprecatedpublic 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*/@Deprecatedpublic 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*/@Deprecatedpublic static RequestAsyncTask executeStatusUpdateRequestAsync(Session session, String message, Callback callback) {return newStatusUpdateRequest(session, message, callback).executeAsync();}/*** Executes this request and returns the response.* <p/>* This should only be called if you have transitioned off the UI thread.** @return the Response object representing the results of the request** @throws FacebookException* If there was an error in the protocol used to communicate with the service* @throws IllegalArgumentException*/public final Response executeAndWait() {return Request.executeAndWait(this);}/*** Executes this request and returns the response.* <p/>* This should only be called from the UI thread.** @return a RequestAsyncTask that is executing the request** @throws IllegalArgumentException*/public final RequestAsyncTask executeAsync() {return Request.executeBatchAsync(this);}/*** Serializes one or more requests but does not execute them. The resulting HttpURLConnection can be executed* explicitly by the caller.** @param requests* one or more Requests to serialize* @return an HttpURLConnection which is ready to execute** @throws FacebookException* If any of the requests in the batch are badly constructed or if there are problems* contacting the service* @throws IllegalArgumentException if the passed in array is zero-length* @throws NullPointerException if the passed in array or any of its contents are null*/public static HttpURLConnection toHttpConnection(Request... requests) {return toHttpConnection(Arrays.asList(requests));}/*** Serializes one or more requests but does not execute them. The resulting HttpURLConnection can be executed* explicitly by the caller.** @param requests* one or more Requests to serialize* @return an HttpURLConnection which is ready to execute** @throws FacebookException* If any of the requests in the batch are badly constructed or if there are problems* contacting the service* @throws IllegalArgumentException if the passed in collection is empty* @throws NullPointerException if the passed in collection or any of its contents are null*/public static HttpURLConnection toHttpConnection(Collection<Request> requests) {Validate.notEmptyAndContainsNoNulls(requests, "requests");return toHttpConnection(new RequestBatch(requests));}/*** Serializes one or more requests but does not execute them. The resulting HttpURLConnection can be executed* explicitly by the caller.** @param requests* a RequestBatch to serialize* @return an HttpURLConnection which is ready to execute** @throws FacebookException* If any of the requests in the batch are badly constructed or if there are problems* contacting the service* @throws IllegalArgumentException*/public static HttpURLConnection toHttpConnection(RequestBatch requests) {URL url = null;try {if (requests.size() == 1) {// Single request case.Request request = requests.get(0);// In the non-batch case, the URL we use really is the same one returned by getUrlForSingleRequest.url = new URL(request.getUrlForSingleRequest());} else {// Batch case -- URL is just the graph API base, individual request URLs are serialized// as relative_url parameters within each batch entry.url = new URL(ServerProtocol.getGraphUrlBase());}} catch (MalformedURLException e) {throw new FacebookException("could not construct URL for request", e);}HttpURLConnection connection;try {connection = createConnection(url);serializeToUrlConnection(requests, connection);} catch (IOException e) {throw new FacebookException("could not construct request body", e);} catch (JSONException e) {throw new FacebookException("could not construct request body", e);}return connection;}/*** Executes a single request on the current thread and returns the response.* <p/>* This should only be used if you have transitioned off the UI thread.** @param request* the Request to execute** @return the Response object representing the results of the request** @throws FacebookException* If there was an error in the protocol used to communicate with the service*/public static Response executeAndWait(Request request) {List<Response> responses = executeBatchAndWait(request);if (responses == null || responses.size() != 1) {throw new FacebookException("invalid state: expected a single response");}return responses.get(0);}/*** Executes requests on the current thread as a single batch and returns the responses.* <p/>* This should only be used if you have transitioned off the UI thread.** @param requests* the Requests to execute** @return a list of Response objects representing the results of the requests; responses are returned in the same* order as the requests were specified.** @throws NullPointerException* In case of a null request* @throws FacebookException* If there was an error in the protocol used to communicate with the service*/public static List<Response> executeBatchAndWait(Request... requests) {Validate.notNull(requests, "requests");return executeBatchAndWait(Arrays.asList(requests));}/*** Executes requests as a single batch on the current thread and returns the responses.* <p/>* This should only be used if you have transitioned off the UI thread.** @param requests* the Requests to execute** @return a list of Response objects representing the results of the requests; responses are returned in the same* order as the requests were specified.** @throws FacebookException* If there was an error in the protocol used to communicate with the service*/public static List<Response> executeBatchAndWait(Collection<Request> requests) {return executeBatchAndWait(new RequestBatch(requests));}/*** Executes requests on the current thread as a single batch and returns the responses.* <p/>* This should only be used if you have transitioned off the UI thread.** @param requests* the batch of Requests to execute** @return a list of Response objects representing the results of the requests; responses are returned in the same* order as the requests were specified.** @throws FacebookException* If there was an error in the protocol used to communicate with the service* @throws IllegalArgumentException if the passed in RequestBatch is empty* @throws NullPointerException if the passed in RequestBatch or any of its contents are null*/public static List<Response> executeBatchAndWait(RequestBatch requests) {Validate.notEmptyAndContainsNoNulls(requests, "requests");HttpURLConnection connection = null;try {connection = toHttpConnection(requests);} catch (Exception ex) {List<Response> responses = Response.constructErrorResponses(requests.getRequests(), null, new FacebookException(ex));runCallbacks(requests, responses);return responses;}List<Response> responses = executeConnectionAndWait(connection, requests);return responses;}/*** Executes requests as a single batch asynchronously. This function will return immediately, and the requests will* be processed on a separate thread. In order to process results of a request, or determine whether a request* succeeded or failed, a callback must be specified (see the {@link #setCallback(Callback) setCallback} method).* <p/>* This should only be called from the UI thread.** @param requests* the Requests to execute* @return a RequestAsyncTask that is executing the request** @throws NullPointerException* If a null request is passed in*/public static RequestAsyncTask executeBatchAsync(Request... requests) {Validate.notNull(requests, "requests");return executeBatchAsync(Arrays.asList(requests));}/*** Executes requests as a single batch asynchronously. This function will return immediately, and the requests will* be processed on a separate thread. In order to process results of a request, or determine whether a request* succeeded or failed, a callback must be specified (see the {@link #setCallback(Callback) setCallback} method).* <p/>* This should only be called from the UI thread.** @param requests* the Requests to execute* @return a RequestAsyncTask that is executing the request** @throws IllegalArgumentException if the passed in collection is empty* @throws NullPointerException if the passed in collection or any of its contents are null*/public static RequestAsyncTask executeBatchAsync(Collection<Request> requests) {return executeBatchAsync(new RequestBatch(requests));}/*** Executes requests as a single batch asynchronously. This function will return immediately, and the requests will* be processed on a separate thread. In order to process results of a request, or determine whether a request* succeeded or failed, a callback must be specified (see the {@link #setCallback(Callback) setCallback} method).* <p/>* This should only be called from the UI thread.** @param requests* the RequestBatch to execute* @return a RequestAsyncTask that is executing the request** @throws IllegalArgumentException if the passed in RequestBatch is empty* @throws NullPointerException if the passed in RequestBatch or any of its contents are null*/public static RequestAsyncTask executeBatchAsync(RequestBatch requests) {Validate.notEmptyAndContainsNoNulls(requests, "requests");RequestAsyncTask asyncTask = new RequestAsyncTask(requests);asyncTask.executeOnSettingsExecutor();return asyncTask;}/*** Executes requests that have already been serialized into an HttpURLConnection. No validation is done that the* contents of the connection actually reflect the serialized requests, so it is the caller's responsibility to* ensure that it will correctly generate the desired responses.* <p/>* This should only be called if you have transitioned off the UI thread.** @param connection* the HttpURLConnection that the requests were serialized into* @param requests* the requests represented by the HttpURLConnection* @return a list of Responses corresponding to the requests** @throws FacebookException* If there was an error in the protocol used to communicate with the service*/public static List<Response> executeConnectionAndWait(HttpURLConnection connection, Collection<Request> requests) {return executeConnectionAndWait(connection, new RequestBatch(requests));}/*** Executes requests that have already been serialized into an HttpURLConnection. No validation is done that the* contents of the connection actually reflect the serialized requests, so it is the caller's responsibility to* ensure that it will correctly generate the desired responses.* <p/>* This should only be called if you have transitioned off the UI thread.** @param connection* the HttpURLConnection that the requests were serialized into* @param requests* the RequestBatch represented by the HttpURLConnection* @return a list of Responses corresponding to the requests** @throws FacebookException* If there was an error in the protocol used to communicate with the service*/public static List<Response> executeConnectionAndWait(HttpURLConnection connection, RequestBatch requests) {List<Response> responses = Response.fromHttpConnection(connection, requests);Utility.disconnectQuietly(connection);int numRequests = requests.size();if (numRequests != responses.size()) {throw new FacebookException(String.format("Received %d responses while expecting %d", responses.size(),numRequests));}runCallbacks(requests, responses);// See if any of these sessions needs its token to be extended. We do this after issuing the request so as to// reduce network contention.HashSet<Session> sessions = new HashSet<Session>();for (Request request : requests) {if (request.session != null) {sessions.add(request.session);}}for (Session session : sessions) {session.extendAccessTokenIfNeeded();}return responses;}/*** Asynchronously executes requests that have already been serialized into an HttpURLConnection. No validation is* done that the contents of the connection actually reflect the serialized requests, so it is the caller's* responsibility to ensure that it will correctly generate the desired responses. This function will return* immediately, and the requests will be processed on a separate thread. In order to process results of a request,* or determine whether a request succeeded or failed, a callback must be specified (see the* {@link #setCallback(Callback) setCallback} method).* <p/>* This should only be called from the UI thread.** @param connection* the HttpURLConnection that the requests were serialized into* @param requests* the requests represented by the HttpURLConnection* @return a RequestAsyncTask that is executing the request*/public static RequestAsyncTask executeConnectionAsync(HttpURLConnection connection, RequestBatch requests) {return executeConnectionAsync(null, connection, requests);}/*** Asynchronously executes requests that have already been serialized into an HttpURLConnection. No validation is* done that the contents of the connection actually reflect the serialized requests, so it is the caller's* responsibility to ensure that it will correctly generate the desired responses. This function will return* immediately, and the requests will be processed on a separate thread. In order to process results of a request,* or determine whether a request succeeded or failed, a callback must be specified (see the* {@link #setCallback(Callback) setCallback} method)* <p/>* This should only be called from the UI thread.** @param callbackHandler* a Handler that will be used to post calls to the callback for each request; if null, a Handler will be* instantiated on the calling thread* @param connection* the HttpURLConnection that the requests were serialized into* @param requests* the requests represented by the HttpURLConnection* @return a RequestAsyncTask that is executing the request*/public static RequestAsyncTask executeConnectionAsync(Handler callbackHandler, HttpURLConnection connection,RequestBatch requests) {Validate.notNull(connection, "connection");RequestAsyncTask asyncTask = new RequestAsyncTask(connection, requests);requests.setCallbackHandler(callbackHandler);asyncTask.executeOnSettingsExecutor();return asyncTask;}/*** Returns a string representation of this Request, useful for debugging.** @return the debugging information*/@Overridepublic String toString() {return new StringBuilder().append("{Request: ").append(" session: ").append(session).append(", graphPath: ").append(graphPath).append(", graphObject: ").append(graphObject).append(", httpMethod: ").append(httpMethod).append(", parameters: ").append(parameters).append("}").toString();}static void runCallbacks(final RequestBatch requests, List<Response> responses) {int numRequests = requests.size();// Compile the list of callbacks to call and then run them either on this thread or via the Handler we receivedfinal ArrayList<Pair<Callback, Response>> callbacks = new ArrayList<Pair<Callback, Response>>();for (int i = 0; i < numRequests; ++i) {Request request = requests.get(i);if (request.callback != null) {callbacks.add(new Pair<Callback, Response>(request.callback, responses.get(i)));}}if (callbacks.size() > 0) {Runnable runnable = new Runnable() {public void run() {for (Pair<Callback, Response> pair : callbacks) {pair.first.onCompleted(pair.second);}List<RequestBatch.Callback> batchCallbacks = requests.getCallbacks();for (RequestBatch.Callback batchCallback : batchCallbacks) {batchCallback.onBatchCompleted(requests);}}};Handler callbackHandler = requests.getCallbackHandler();if (callbackHandler == null) {// Run on this thread.runnable.run();} else {// Post to the handler.callbackHandler.post(runnable);}}}static HttpURLConnection createConnection(URL url) throws IOException {HttpURLConnection connection;connection = (HttpURLConnection) url.openConnection();connection.setRequestProperty(USER_AGENT_HEADER, getUserAgent());connection.setRequestProperty(CONTENT_TYPE_HEADER, getMimeContentType());connection.setRequestProperty(ACCEPT_LANGUAGE_HEADER, Locale.getDefault().toString());connection.setChunkedStreamingMode(0);return connection;}private void addCommonParameters() {if (this.session != null) {if (!this.session.isOpened()) {throw new FacebookException("Session provided to a Request in un-opened state.");} else if (!this.parameters.containsKey(ACCESS_TOKEN_PARAM)) {String accessToken = this.session.getAccessToken();Logger.registerAccessToken(accessToken);this.parameters.putString(ACCESS_TOKEN_PARAM, accessToken);}} else if (!skipClientToken && !this.parameters.containsKey(ACCESS_TOKEN_PARAM)) {String appID = Settings.getApplicationId();String clientToken = Settings.getClientToken();if (!Utility.isNullOrEmpty(appID) && !Utility.isNullOrEmpty(clientToken)) {String accessToken = appID + "|" + clientToken;this.parameters.putString(ACCESS_TOKEN_PARAM, accessToken);} else {Log.d(TAG,"Warning: Sessionless Request needs token but missing either application ID or client token.");}}this.parameters.putString(SDK_PARAM, SDK_ANDROID);this.parameters.putString(FORMAT_PARAM, FORMAT_JSON);}private String appendParametersToBaseUrl(String baseUrl) {Uri.Builder uriBuilder = new Uri.Builder().encodedPath(baseUrl);Set<String> keys = this.parameters.keySet();for (String key : keys) {Object value = this.parameters.get(key);if (value == null) {value = "";}if (isSupportedParameterType(value)) {value = parameterToString(value);} else {if (httpMethod == HttpMethod.GET) {throw new IllegalArgumentException(String.format("Unsupported parameter type for GET request: %s",value.getClass().getSimpleName()));}continue;}uriBuilder.appendQueryParameter(key, value.toString());}return uriBuilder.toString();}final String getUrlForBatchedRequest() {if (overriddenURL != null) {throw new FacebookException("Can't override URL for a batch request");}String baseUrl = getGraphPathWithVersion();addCommonParameters();return appendParametersToBaseUrl(baseUrl);}final String getUrlForSingleRequest() {if (overriddenURL != null) {return overriddenURL.toString();}String graphBaseUrlBase;if (this.getHttpMethod() == HttpMethod.POST && graphPath != null && graphPath.endsWith(VIDEOS_SUFFIX)) {graphBaseUrlBase = ServerProtocol.getGraphVideoUrlBase();} else {graphBaseUrlBase = ServerProtocol.getGraphUrlBase();}String baseUrl = String.format("%s/%s", graphBaseUrlBase, getGraphPathWithVersion());addCommonParameters();return appendParametersToBaseUrl(baseUrl);}private String getGraphPathWithVersion() {Matcher matcher = versionPattern.matcher(this.graphPath);if (matcher.matches()) {return this.graphPath;}return String.format("%s/%s", this.version, this.graphPath);}private static class Attachment {private final Request request;private final Object value;public Attachment(Request request, Object value) {this.request = request;this.value = value;}public Request getRequest() {return request;}public Object getValue() {return value;}}private void serializeToBatch(JSONArray batch, Map<String, Attachment> attachments) throws JSONException, IOException {JSONObject batchEntry = new JSONObject();if (this.batchEntryName != null) {batchEntry.put(BATCH_ENTRY_NAME_PARAM, this.batchEntryName);batchEntry.put(BATCH_ENTRY_OMIT_RESPONSE_ON_SUCCESS_PARAM, this.batchEntryOmitResultOnSuccess);}if (this.batchEntryDependsOn != null) {batchEntry.put(BATCH_ENTRY_DEPENDS_ON_PARAM, this.batchEntryDependsOn);}String relativeURL = getUrlForBatchedRequest();batchEntry.put(BATCH_RELATIVE_URL_PARAM, relativeURL);batchEntry.put(BATCH_METHOD_PARAM, httpMethod);if (this.session != null) {String accessToken = this.session.getAccessToken();Logger.registerAccessToken(accessToken);}// Find all of our attachments. Remember their names and put them in the attachment map.ArrayList<String> attachmentNames = new ArrayList<String>();Set<String> keys = this.parameters.keySet();for (String key : keys) {Object value = this.parameters.get(key);if (isSupportedAttachmentType(value)) {// Make the name unique across this entire batch.String name = String.format("%s%d", ATTACHMENT_FILENAME_PREFIX, attachments.size());attachmentNames.add(name);attachments.put(name, new Attachment(this, value));}}if (!attachmentNames.isEmpty()) {String attachmentNamesString = TextUtils.join(",", attachmentNames);batchEntry.put(ATTACHED_FILES_PARAM, attachmentNamesString);}if (this.graphObject != null) {// Serialize the graph object into the "body" parameter.final ArrayList<String> keysAndValues = new ArrayList<String>();processGraphObject(this.graphObject, relativeURL, new KeyValueSerializer() {@Overridepublic void writeString(String key, String value) throws IOException {keysAndValues.add(String.format("%s=%s", key, URLEncoder.encode(value, "UTF-8")));}});String bodyValue = TextUtils.join("&", keysAndValues);batchEntry.put(BATCH_BODY_PARAM, bodyValue);}batch.put(batchEntry);}private static boolean hasOnProgressCallbacks(RequestBatch requests) {for (RequestBatch.Callback callback : requests.getCallbacks()) {if (callback instanceof RequestBatch.OnProgressCallback) {return true;}}for (Request request : requests) {if (request.getCallback() instanceof OnProgressCallback) {return true;}}return false;}final static void serializeToUrlConnection(RequestBatch requests, HttpURLConnection connection)throws IOException, JSONException {Logger logger = new Logger(LoggingBehavior.REQUESTS, "Request");int numRequests = requests.size();HttpMethod connectionHttpMethod = (numRequests == 1) ? requests.get(0).httpMethod : HttpMethod.POST;connection.setRequestMethod(connectionHttpMethod.name());URL url = connection.getURL();logger.append("Request:\n");logger.appendKeyValue("Id", requests.getId());logger.appendKeyValue("URL", url);logger.appendKeyValue("Method", connection.getRequestMethod());logger.appendKeyValue("User-Agent", connection.getRequestProperty("User-Agent"));logger.appendKeyValue("Content-Type", connection.getRequestProperty("Content-Type"));connection.setConnectTimeout(requests.getTimeout());connection.setReadTimeout(requests.getTimeout());// If we have a single non-POST request, don't try to serialize anything or HttpURLConnection will// turn it into a POST.boolean isPost = (connectionHttpMethod == HttpMethod.POST);if (!isPost) {logger.log();return;}connection.setDoOutput(true);OutputStream outputStream = null;try {if (hasOnProgressCallbacks(requests)) {ProgressNoopOutputStream countingStream = null;countingStream = new ProgressNoopOutputStream(requests.getCallbackHandler());processRequest(requests, null, numRequests, url, countingStream);int max = countingStream.getMaxProgress();Map<Request, RequestProgress> progressMap = countingStream.getProgressMap();BufferedOutputStream buffered = new BufferedOutputStream(connection.getOutputStream());outputStream = new ProgressOutputStream(buffered, requests, progressMap, max);}else {outputStream = new BufferedOutputStream(connection.getOutputStream());}processRequest(requests, logger, numRequests, url, outputStream);}finally {if (outputStream != null) {outputStream.close();}}logger.log();}private static void processRequest(RequestBatch requests, Logger logger, int numRequests, URL url, OutputStream outputStream)throws IOException, JSONException{Serializer serializer = new Serializer(outputStream, logger);if (numRequests == 1) {Request request = requests.get(0);Map<String, Attachment> attachments = new HashMap<String, Attachment>();for(String key : request.parameters.keySet()) {Object value = request.parameters.get(key);if (isSupportedAttachmentType(value)) {attachments.put(key, new Attachment(request, value));}}if (logger != null) {logger.append(" Parameters:\n");}serializeParameters(request.parameters, serializer, request);if (logger != null) {logger.append(" Attachments:\n");}serializeAttachments(attachments, serializer);if (request.graphObject != null) {processGraphObject(request.graphObject, url.getPath(), serializer);}} else {String batchAppID = getBatchAppId(requests);if (Utility.isNullOrEmpty(batchAppID)) {throw new FacebookException("At least one request in a batch must have an open Session, or a "+ "default app ID must be specified.");}serializer.writeString(BATCH_APP_ID_PARAM, batchAppID);// We write out all the requests as JSON, remembering which file attachments they have, then// write out the attachments.Map<String, Attachment> attachments = new HashMap<String, Attachment>();serializeRequestsAsJSON(serializer, requests, attachments);if (logger != null) {logger.append(" Attachments:\n");}serializeAttachments(attachments, serializer);}}private static boolean isMeRequest(String path) {Matcher matcher = versionPattern.matcher(path);if (matcher.matches()) {// Group 1 contains the path aside from versionpath = matcher.group(1);}if (path.startsWith("me/") || path.startsWith("/me/")) {return true;}return false;}private static void processGraphObject(GraphObject graphObject, String path, KeyValueSerializer serializer)throws IOException {// In general, graph objects are passed by reference (ID/URL). But if this is an OG Action,// we need to pass the entire values of the contents of the 'image' property, as they// contain important metadata beyond just a URL. We don't have a 100% foolproof way of knowing// if we are posting an OG Action, given that batched requests can have parameter substitution,// but passing the OG Action type as a substituted parameter is unlikely.// It looks like an OG Action if it's posted to me/namespace:action[?other=stuff].boolean isOGAction = false;if (isMeRequest(path)) {int colonLocation = path.indexOf(":");int questionMarkLocation = path.indexOf("?");isOGAction = colonLocation > 3 && (questionMarkLocation == -1 || colonLocation < questionMarkLocation);}Set<Entry<String, Object>> entries = graphObject.asMap().entrySet();for (Entry<String, Object> entry : entries) {boolean passByValue = isOGAction && entry.getKey().equalsIgnoreCase("image");processGraphObjectProperty(entry.getKey(), entry.getValue(), serializer, passByValue);}}private static void processGraphObjectProperty(String key, Object value, KeyValueSerializer serializer,boolean passByValue) throws IOException {Class<?> valueClass = value.getClass();if (GraphObject.class.isAssignableFrom(valueClass)) {value = ((GraphObject) value).getInnerJSONObject();valueClass = value.getClass();} else if (GraphObjectList.class.isAssignableFrom(valueClass)) {value = ((GraphObjectList<?>) value).getInnerJSONArray();valueClass = value.getClass();}if (JSONObject.class.isAssignableFrom(valueClass)) {JSONObject jsonObject = (JSONObject) value;if (passByValue) {// We need to pass all properties of this object in key[propertyName] format.@SuppressWarnings("unchecked")Iterator<String> keys = jsonObject.keys();while (keys.hasNext()) {String propertyName = keys.next();String subKey = String.format("%s[%s]", key, propertyName);processGraphObjectProperty(subKey, jsonObject.opt(propertyName), serializer, passByValue);}} else {// Normal case is passing objects by reference, so just pass the ID or URL, if any, as the value// for "key"if (jsonObject.has("id")) {processGraphObjectProperty(key, jsonObject.optString("id"), serializer, passByValue);} else if (jsonObject.has("url")) {processGraphObjectProperty(key, jsonObject.optString("url"), serializer, passByValue);} else if (jsonObject.has(NativeProtocol.OPEN_GRAPH_CREATE_OBJECT_KEY)) {processGraphObjectProperty(key, jsonObject.toString(), serializer, passByValue);}}} else if (JSONArray.class.isAssignableFrom(valueClass)) {JSONArray jsonArray = (JSONArray) value;int length = jsonArray.length();for (int i = 0; i < length; ++i) {String subKey = String.format("%s[%d]", key, i);processGraphObjectProperty(subKey, jsonArray.opt(i), serializer, passByValue);}} else if (String.class.isAssignableFrom(valueClass) ||Number.class.isAssignableFrom(valueClass) ||Boolean.class.isAssignableFrom(valueClass)) {serializer.writeString(key, value.toString());} else if (Date.class.isAssignableFrom(valueClass)) {Date date = (Date) value;// The "Events Timezone" platform migration affects what date/time formats Facebook accepts and returns.// Apps created after 8/1/12 (or apps that have explicitly enabled the migration) should send/receive// dates in ISO-8601 format. Pre-migration apps can send as Unix timestamps. Since the future is ISO-8601,// that is what we support here. Apps that need pre-migration behavior can explicitly send these as// integer timestamps rather than Dates.final SimpleDateFormat iso8601DateFormat = new SimpleDateFormat(ISO_8601_FORMAT_STRING, Locale.US);serializer.writeString(key, iso8601DateFormat.format(date));}}private static void serializeParameters(Bundle bundle, Serializer serializer, Request request) throws IOException {Set<String> keys = bundle.keySet();for (String key : keys) {Object value = bundle.get(key);if (isSupportedParameterType(value)) {serializer.writeObject(key, value, request);}}}private static void serializeAttachments(Map<String, Attachment> attachments, Serializer serializer) throws IOException {Set<String> keys = attachments.keySet();for (String key : keys) {Attachment attachment = attachments.get(key);if (isSupportedAttachmentType(attachment.getValue())) {serializer.writeObject(key, attachment.getValue(), attachment.getRequest());}}}private static void serializeRequestsAsJSON(Serializer serializer, Collection<Request> requests, Map<String, Attachment> attachments)throws JSONException, IOException {JSONArray batch = new JSONArray();for (Request request : requests) {request.serializeToBatch(batch, attachments);}serializer.writeRequestsAsJson(BATCH_PARAM, batch, requests);}private static String getMimeContentType() {return String.format("multipart/form-data; boundary=%s", MIME_BOUNDARY);}private static volatile String userAgent;private static String getUserAgent() {if (userAgent == null) {userAgent = String.format("%s.%s", USER_AGENT_BASE, FacebookSdkVersion.BUILD);}return userAgent;}private static String getBatchAppId(RequestBatch batch) {if (!Utility.isNullOrEmpty(batch.getBatchApplicationId())) {return batch.getBatchApplicationId();}for (Request request : batch) {Session session = request.session;if (session != null) {return session.getApplicationId();}}return Request.defaultBatchApplicationId;}private static <T extends GraphObject> List<T> typedListFromResponse(Response response, Class<T> clazz) {GraphMultiResult multiResult = response.getGraphObjectAs(GraphMultiResult.class);if (multiResult == null) {return null;}GraphObjectList<GraphObject> data = multiResult.getData();if (data == null) {return null;}return data.castToListOf(clazz);}private static boolean isSupportedAttachmentType(Object value) {return value instanceof Bitmap || value instanceof byte[] || value instanceof ParcelFileDescriptor ||value instanceof ParcelFileDescriptorWithMimeType;}private static boolean isSupportedParameterType(Object value) {return value instanceof String || value instanceof Boolean || value instanceof Number ||value instanceof Date;}private static String parameterToString(Object value) {if (value instanceof String) {return (String) value;} else if (value instanceof Boolean || value instanceof Number) {return value.toString();} else if (value instanceof Date) {final SimpleDateFormat iso8601DateFormat = new SimpleDateFormat(ISO_8601_FORMAT_STRING, Locale.US);return iso8601DateFormat.format(value);}throw new IllegalArgumentException("Unsupported parameter type.");}private interface KeyValueSerializer {void writeString(String key, String value) throws IOException;}private static class Serializer implements KeyValueSerializer {private final OutputStream outputStream;private final Logger logger;private boolean firstWrite = true;public Serializer(OutputStream outputStream, Logger logger) {this.outputStream = outputStream;this.logger = logger;}public void writeObject(String key, Object value, Request request) throws IOException {if (outputStream instanceof RequestOutputStream) {((RequestOutputStream) outputStream).setCurrentRequest(request);}if (isSupportedParameterType(value)) {writeString(key, parameterToString(value));} else if (value instanceof Bitmap) {writeBitmap(key, (Bitmap) value);} else if (value instanceof byte[]) {writeBytes(key, (byte[]) value);} else if (value instanceof ParcelFileDescriptor) {writeFile(key, (ParcelFileDescriptor) value, null);} else if (value instanceof ParcelFileDescriptorWithMimeType) {writeFile(key, (ParcelFileDescriptorWithMimeType) value);} else {throw new IllegalArgumentException("value is not a supported type: String, Bitmap, byte[]");}}public void writeRequestsAsJson(String key, JSONArray requestJsonArray, Collection<Request> requests)throws IOException, JSONException {if (! (outputStream instanceof RequestOutputStream)) {writeString(key, requestJsonArray.toString());return;}RequestOutputStream requestOutputStream = (RequestOutputStream) outputStream;writeContentDisposition(key, null, null);write("[");int i = 0;for (Request request : requests) {JSONObject requestJson = requestJsonArray.getJSONObject(i);requestOutputStream.setCurrentRequest(request);if (i > 0) {write(",%s", requestJson.toString());} else {write("%s", requestJson.toString());}i++;}write("]");if (logger != null) {logger.appendKeyValue(" " + key, requestJsonArray.toString());}}public void writeString(String key, String value) throws IOException {writeContentDisposition(key, null, null);writeLine("%s", value);writeRecordBoundary();if (logger != null) {logger.appendKeyValue(" " + key, value);}}public void writeBitmap(String key, Bitmap bitmap) throws IOException {writeContentDisposition(key, key, "image/png");// Note: quality parameter is ignored for PNGbitmap.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-Dispositionif (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();}}}