/** * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, * copy, modify, and distribute this software in source code or binary form for use * in connection with the web services and APIs provided by Facebook. * * As with any software that integrates with the Facebook platform, your use of * this software is subject to the Facebook Developer Principles and Policies * [http://developers.facebook.com/policy/]. This copyright notice shall be * included in all copies or substantial portions of the software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ 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.share.internal.OpenGraphJSONUtility; import com.facebook.share.model.ShareOpenGraphObject; import com.facebook.share.model.SharePhoto; 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.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPOutputStream; /** * <p> * 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. * </p> * <p> * The particular service endpoint that a request targets is determined by a graph path (see the * {@link #setGraphPath(String) setGraphPath} method). * </p> * <p> * A Request can be executed either anonymously or representing an authenticated user. In the former * case, no AccessToken needs to be specified, while in the latter, an AccessToken must be provided. * If requests are executed in a batch, a Facebook application ID must be associated with the batch, * either by setting the application ID in the AndroidManifest.xml or via FacebookSdk or by calling * the {@link #setDefaultBatchApplicationId(String) setDefaultBatchApplicationId} method. * </p> * <p> * After completion of a request, the AccessToken, if not null and taken from AccessTokenManager, * 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. * </p> */ public class GraphRequest { /** * 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 = GraphRequest.class.getSimpleName(); private static final String VIDEOS_SUFFIX = "/videos"; 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 SEARCH = "search"; 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"; private static final String CONTENT_ENCODING_HEADER = "Content-Encoding"; // Parameter names/values 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"; public 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 DEBUG_PARAM = "debug"; private static final String DEBUG_SEVERITY_INFO = "info"; private static final String DEBUG_SEVERITY_WARNING = "warning"; private static final String DEBUG_KEY = "__debug__"; private static final String DEBUG_MESSAGES_KEY = "messages"; private static final String DEBUG_MESSAGE_KEY = "message"; private static final String DEBUG_MESSAGE_TYPE_KEY = "type"; private static final String DEBUG_MESSAGE_LINK_KEY = "link"; private static final String PICTURE_PARAM = "picture"; private static final String CAPTION_PARAM = "caption"; public static final String FIELDS_PARAM = "fields"; private static final String MIME_BOUNDARY = "3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f"; private static final String GRAPH_PATH_FORMAT = "%s/%s"; private static String defaultBatchApplicationId; // Group 1 in the pattern is the path without the version info private static Pattern versionPattern = Pattern.compile("^/?v\\d+\\.\\d+/(.*)"); private AccessToken accessToken; private HttpMethod httpMethod; private String graphPath; private JSONObject 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 an access token, graph path, or any other parameters. */ public GraphRequest() { this(null, null, null, null, null); } /** * Constructs a request with an access token to retrieve a particular graph path. * An access token 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. * * @param accessToken the access token to use, or null * @param graphPath the graph path to retrieve */ public GraphRequest(AccessToken accessToken, String graphPath) { this(accessToken, graphPath, null, null, null); } /** * Constructs a request with a specific AccessToken, graph path, parameters, and HTTP method. An * access token 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. * <p/> * Depending on the httpMethod parameter, the object at the graph path may be retrieved, * created, or deleted. * * @param accessToken the access token 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 GraphRequest( AccessToken accessToken, String graphPath, Bundle parameters, HttpMethod httpMethod) { this(accessToken, graphPath, parameters, httpMethod, null); } /** * Constructs a request with a specific access token, graph path, parameters, and HTTP method. * An access token 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. * <p/> * Depending on the httpMethod parameter, the object at the graph path may be retrieved, * created, or deleted. * * @param accessToken the access token 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 GraphRequest( AccessToken accessToken, String graphPath, Bundle parameters, HttpMethod httpMethod, Callback callback) { this(accessToken, graphPath, parameters, httpMethod, callback, null); } /** * Constructs a request with a specific access token, graph path, parameters, and HTTP method. * An access token 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. * <p/> * Depending on the httpMethod parameter, the object at the graph path may be retrieved, * created, or deleted. * * @param accessToken the access token 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 GraphRequest( AccessToken accessToken, String graphPath, Bundle parameters, HttpMethod httpMethod, Callback callback, String version) { this.accessToken = accessToken; this.graphPath = graphPath; this.version = version; setCallback(callback); setHttpMethod(httpMethod); if (parameters != null) { this.parameters = new Bundle(parameters); } else { this.parameters = new Bundle(); } if (this.version == null) { this.version = FacebookSdk.getGraphApiVersion(); } } GraphRequest(AccessToken accessToken, URL overriddenURL) { this.accessToken = accessToken; this.overriddenURL = overriddenURL.toString(); setHttpMethod(HttpMethod.GET); this.parameters = new Bundle(); } /** * Creates a new Request configured to delete a resource through the Graph API. * * @param accessToken the access token to use, or null * @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 GraphRequest newDeleteObjectRequest( AccessToken accessToken, String id, Callback callback) { return new GraphRequest(accessToken, id, null, HttpMethod.DELETE, callback); } /** * Creates a new Request configured to retrieve a user's own profile. * * @param accessToken the access token to use, or null * @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 GraphRequest newMeRequest( AccessToken accessToken, final GraphJSONObjectCallback callback) { Callback wrapper = new Callback() { @Override public void onCompleted(GraphResponse response) { if (callback != null) { callback.onCompleted(response.getJSONObject(), response); } } }; return new GraphRequest(accessToken, ME, null, null, wrapper); } /** * 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 accessToken the access token to use, or null * @param graphPath the graph path to retrieve, create, or delete * @param graphObject the graph object 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 GraphRequest newPostRequest( AccessToken accessToken, String graphPath, JSONObject graphObject, Callback callback) { GraphRequest request = new GraphRequest( accessToken, graphPath, null, HttpMethod.POST, callback); request.setGraphObject(graphObject); return request; } /** * Creates a new Request configured to retrieve a user's friend list. * * @param accessToken the access token to use, or null * @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 GraphRequest newMyFriendsRequest( AccessToken accessToken, final GraphJSONArrayCallback callback) { Callback wrapper = new Callback() { @Override public void onCompleted(GraphResponse response) { if (callback != null) { JSONObject result = response.getJSONObject(); JSONArray data = result != null ? result.optJSONArray("data") : null; callback.onCompleted(data, response); } } }; return new GraphRequest(accessToken, MY_FRIENDS, null, null, wrapper); } /** * Creates a new Request configured to retrieve a particular graph path. * * @param accessToken the access token to use, or null * @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 GraphRequest newGraphPathRequest( AccessToken accessToken, String graphPath, Callback callback) { return new GraphRequest(accessToken, 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 accessToken the access token to use, or null * @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 GraphRequest newPlacesSearchRequest( AccessToken accessToken, Location location, int radiusInMeters, int resultsLimit, String searchText, final GraphJSONArrayCallback 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(GraphResponse response) { if (callback != null) { JSONObject result = response.getJSONObject(); JSONArray data = result != null ? result.optJSONArray("data") : null; callback.onCompleted(data, response); } } }; return new GraphRequest(accessToken, SEARCH, parameters, HttpMethod.GET, wrapper); } /** * Creates a new Request configured to upload a photo to the specified graph path. * * @param accessToken the access token to use, or null * @param graphPath the graph path to use, defaults to me/photos * @param image the bitmap image to upload * @param caption the user generated caption for the photo, can be null * @param params the parameters, can be null * @param callback a callback that will be called when the request is completed to handle * success or error conditions, can be null * @return a Request that is ready to execute */ public static GraphRequest newUploadPhotoRequest( AccessToken accessToken, String graphPath, Bitmap image, String caption, Bundle params, Callback callback) { graphPath = getDefaultPhotoPathIfNull(graphPath); Bundle parameters = new Bundle(); if (params != null) { parameters.putAll(params); } parameters.putParcelable(PICTURE_PARAM, image); if (caption != null && !caption.isEmpty()) { parameters.putString(CAPTION_PARAM, caption); } return new GraphRequest(accessToken, graphPath, parameters, HttpMethod.POST, callback); } /** * Creates a new Request configured to upload a photo to the specified graph path. The * photo will be read from the specified file. * * @param accessToken the access token to use, or null * @param graphPath the graph path to use, defaults to me/photos * @param file the file containing the photo to upload * @param caption the user generated caption for the photo, can be null * @param params the parameters, can be null * @param callback a callback that will be called when the request is completed to handle * success or error conditions, can be null * @return a Request that is ready to execute * @throws java.io.FileNotFoundException if the file doesn't exist */ public static GraphRequest newUploadPhotoRequest( AccessToken accessToken, String graphPath, File file, String caption, Bundle params, Callback callback ) throws FileNotFoundException { graphPath = getDefaultPhotoPathIfNull(graphPath); ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); Bundle parameters = new Bundle(); if (params != null) { parameters.putAll(params); } parameters.putParcelable(PICTURE_PARAM, descriptor); if (caption != null && !caption.isEmpty()) { parameters.putString(CAPTION_PARAM, caption); } return new GraphRequest(accessToken, graphPath, parameters, HttpMethod.POST, callback); } /** * Creates a new Request configured to upload a photo to the specified graph path. The * photo will be read from the specified Uri. * * @param accessToken the access token to use, or null * @param graphPath the graph path to use, defaults to me/photos * @param photoUri the file:// or content:// Uri to the photo on device * @param caption the user generated caption for the photo, can be null * @param params the parameters, can be null * @param callback a callback that will be called when the request is completed to handle * success or error conditions, can be null * @return a Request that is ready to execute * @throws FileNotFoundException if the Uri does not exist */ public static GraphRequest newUploadPhotoRequest( AccessToken accessToken, String graphPath, Uri photoUri, String caption, Bundle params, Callback callback) throws FileNotFoundException { graphPath = getDefaultPhotoPathIfNull(graphPath); if (Utility.isFileUri(photoUri)) { return newUploadPhotoRequest( accessToken, graphPath, new File(photoUri.getPath()), caption, params, callback); } else if (!Utility.isContentUri(photoUri)) { throw new FacebookException("The photo Uri must be either a file:// or content:// Uri"); } Bundle parameters = new Bundle(); if (params != null) { parameters.putAll(params); } parameters.putParcelable(PICTURE_PARAM, photoUri); if (caption != null && !caption.isEmpty()) { parameters.putString(CAPTION_PARAM, caption); } return new GraphRequest(accessToken, graphPath, parameters, HttpMethod.POST, 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 a "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 access token (or active access token if `null`) is valid, the ID will represent the * user associated with the active access token; 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 FacebookSdk#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 accessToken the access token to issue the Request on, or null 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, the application ID from * the access token will be used, if any; if not, 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 GraphRequest newCustomAudienceThirdPartyIdRequest(AccessToken accessToken, Context context, String applicationId, Callback callback) { if (applicationId == null && accessToken != null) { applicationId = accessToken.getApplicationId(); } if (applicationId == null) { 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 (accessToken == null) { if (attributionIdentifiers == null) { throw new FacebookException( "There is no access token and attribution identifiers could not be " + "retrieved"); } // Only use the attributionID if we don't have an access token. If we do, 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 (FacebookSdk.getLimitEventAndDataUsage(context) || (attributionIdentifiers != null && attributionIdentifiers.isTrackingLimited())) { parameters.putString("limit_event_usage", "1"); } return new GraphRequest(accessToken, endpoint, parameters, HttpMethod.GET, 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 a "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 access token (or active access token if `null`) is valid, the ID will represent the * user associated with the active access token; 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 FacebookSdk#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 accessToken the access token to issue the Request on, or null 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 GraphRequest newCustomAudienceThirdPartyIdRequest( AccessToken accessToken, Context context, Callback callback) { return newCustomAudienceThirdPartyIdRequest(accessToken, context, null, callback); } /** * Returns the GraphObject, if any, associated with this request. * * @return the GraphObject associated with this request, or null if there is none */ public final JSONObject 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(JSONObject 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 access token associated with this request. * * @return the access token associated with this request, or null if none has been specified */ public final AccessToken getAccessToken() { return this.accessToken; } /** * Sets the access token to use for this request. * * @param accessToken the access token to use for this request */ public final void setAccessToken(AccessToken accessToken) { this.accessToken = accessToken; } /** * Returns the name of this requests entry in a batched request. * * @return the name of this requests 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 requests 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 requests 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 requests 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. * Batched requests require an application ID, so either at least one request in a batch must * provide an access token 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 GraphRequest.defaultBatchApplicationId; } /** * Sets the default application ID that will be used to submit batched requests if none of those * requests specifies an access token. Batched requests require an application ID, so either at * least one request in a batch must specify an access token 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(final Callback callback) { // Wrap callback to parse debug response if Graph Debug Mode is Enabled. if (FacebookSdk.isLoggingBehaviorEnabled(LoggingBehavior.GRAPH_API_DEBUG_INFO) || FacebookSdk.isLoggingBehaviorEnabled(LoggingBehavior.GRAPH_API_DEBUG_WARNING)) { Callback wrapper = new Callback() { @Override public void onCompleted(GraphResponse response) { JSONObject responseObject = response.getJSONObject(); JSONObject debug = responseObject != null ? responseObject.optJSONObject(DEBUG_KEY) : null; JSONArray debugMessages = debug != null ? debug.optJSONArray(DEBUG_MESSAGES_KEY) : null; if (debugMessages != null) { for (int i = 0; i < debugMessages.length(); ++i) { JSONObject debugMessageObject = debugMessages.optJSONObject(i); String debugMessage = debugMessageObject != null ? debugMessageObject.optString(DEBUG_MESSAGE_KEY) : null; String debugMessageType = debugMessageObject != null ? debugMessageObject.optString(DEBUG_MESSAGE_TYPE_KEY) : null; String debugMessageLink = debugMessageObject != null ? debugMessageObject.optString(DEBUG_MESSAGE_LINK_KEY) : null; if (debugMessage != null && debugMessageType != null) { LoggingBehavior behavior = LoggingBehavior.GRAPH_API_DEBUG_INFO; if (debugMessageType.equals("warning")) { behavior = LoggingBehavior.GRAPH_API_DEBUG_WARNING; } if (!Utility.isNullOrEmpty(debugMessageLink)) { debugMessage += " Link: " + debugMessageLink; } Logger.log(behavior, TAG, debugMessage); } } } if (callback != null) { callback.onCompleted(response); } } }; this.callback = wrapper; } else { 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; } /** * Executes this request on the current thread and blocks while waiting for 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 GraphResponse executeAndWait() { return GraphRequest.executeAndWait(this); } /** * Executes the request asynchronously. This function will return immediately, * and the request will be processed on a separate thread. In order to process result 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. * * @return a RequestAsyncTask that is executing the request * @throws IllegalArgumentException */ public final GraphRequestAsyncTask executeAsync() { return GraphRequest.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(GraphRequest... 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<GraphRequest> requests) { Validate.notEmptyAndContainsNoNulls(requests, "requests"); return toHttpConnection(new GraphRequestBatch(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(GraphRequestBatch requests) { validateFieldsParamForGetRequests(requests); URL url; try { if (requests.size() == 1) { // Single request case. GraphRequest 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 = null; try { connection = createConnection(url); serializeToUrlConnection(requests, connection); } catch (IOException | JSONException e) { Utility.disconnectQuietly(connection); throw new FacebookException("could not construct request body", e); } return connection; } /** * Executes a single request on the current thread and blocks while waiting for 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 GraphResponse executeAndWait(GraphRequest request) { List<GraphResponse> 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 blocks while waiting for the * response. * <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<GraphResponse> executeBatchAndWait(GraphRequest... requests) { Validate.notNull(requests, "requests"); return executeBatchAndWait(Arrays.asList(requests)); } /** * Executes requests as a single batch on the current thread and blocks while waiting for 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<GraphResponse> executeBatchAndWait(Collection<GraphRequest> requests) { return executeBatchAndWait(new GraphRequestBatch(requests)); } /** * Executes requests on the current thread as a single batch and blocks while waiting for 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<GraphResponse> executeBatchAndWait(GraphRequestBatch requests) { Validate.notEmptyAndContainsNoNulls(requests, "requests"); HttpURLConnection connection = null; try { try { connection = toHttpConnection(requests); } catch (Exception ex) { List<GraphResponse> responses = GraphResponse.constructErrorResponses( requests.getRequests(), null, new FacebookException(ex)); runCallbacks(requests, responses); return responses; } List<GraphResponse> responses = executeConnectionAndWait(connection, requests); return responses; } finally { Utility.disconnectQuietly(connection); } } /** * 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 GraphRequestAsyncTask executeBatchAsync(GraphRequest... 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 GraphRequestAsyncTask executeBatchAsync(Collection<GraphRequest> requests) { return executeBatchAsync(new GraphRequestBatch(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 GraphRequestAsyncTask executeBatchAsync(GraphRequestBatch requests) { Validate.notEmptyAndContainsNoNulls(requests, "requests"); GraphRequestAsyncTask asyncTask = new GraphRequestAsyncTask(requests); asyncTask.executeOnExecutor(FacebookSdk.getExecutor()); 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<GraphResponse> executeConnectionAndWait( HttpURLConnection connection, Collection<GraphRequest> requests) { return executeConnectionAndWait(connection, new GraphRequestBatch(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<GraphResponse> executeConnectionAndWait( HttpURLConnection connection, GraphRequestBatch requests) { List<GraphResponse> responses = GraphResponse.fromHttpConnection(connection, requests); Utility.disconnectQuietly(connection); int numRequests = requests.size(); if (numRequests != responses.size()) { throw new FacebookException( String.format(Locale.US, "Received %d responses while expecting %d", responses.size(), numRequests)); } runCallbacks(requests, responses); // Try extending the current access token in case it's needed. AccessTokenManager.getInstance().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 GraphRequestAsyncTask executeConnectionAsync( HttpURLConnection connection, GraphRequestBatch 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 GraphRequestAsyncTask executeConnectionAsync( Handler callbackHandler, HttpURLConnection connection, GraphRequestBatch requests) { Validate.notNull(connection, "connection"); GraphRequestAsyncTask asyncTask = new GraphRequestAsyncTask(connection, requests); requests.setCallbackHandler(callbackHandler); asyncTask.executeOnExecutor(FacebookSdk.getExecutor()); 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(" accessToken: ") .append(accessToken == null ? "null" : accessToken) .append(", graphPath: ") .append(graphPath) .append(", graphObject: ") .append(graphObject) .append(", httpMethod: ") .append(httpMethod) .append(", parameters: ") .append(parameters) .append("}") .toString(); } static void runCallbacks(final GraphRequestBatch requests, List<GraphResponse> 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, GraphResponse>> callbacks = new ArrayList<Pair<Callback, GraphResponse>>(); for (int i = 0; i < numRequests; ++i) { GraphRequest request = requests.get(i); if (request.callback != null) { callbacks.add( new Pair<Callback, GraphResponse>(request.callback, responses.get(i))); } } if (callbacks.size() > 0) { Runnable runnable = new Runnable() { public void run() { for (Pair<Callback, GraphResponse> pair : callbacks) { pair.first.onCompleted(pair.second); } List<GraphRequestBatch.Callback> batchCallbacks = requests.getCallbacks(); for (GraphRequestBatch.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); } } } private static String getDefaultPhotoPathIfNull(String graphPath) { return graphPath == null ? MY_PHOTOS : graphPath; } private static HttpURLConnection createConnection(URL url) throws IOException { HttpURLConnection connection; connection = (HttpURLConnection) url.openConnection(); connection.setRequestProperty(USER_AGENT_HEADER, getUserAgent()); connection.setRequestProperty(ACCEPT_LANGUAGE_HEADER, Locale.getDefault().toString()); connection.setChunkedStreamingMode(0); return connection; } private void addCommonParameters() { if (this.accessToken != null) { if (!this.parameters.containsKey(ACCESS_TOKEN_PARAM)) { String token = accessToken.getToken(); Logger.registerAccessToken(token); this.parameters.putString(ACCESS_TOKEN_PARAM, token); } } else if (!skipClientToken && !this.parameters.containsKey(ACCESS_TOKEN_PARAM)) { String appID = FacebookSdk.getApplicationId(); String clientToken = FacebookSdk.getClientToken(); if (!Utility.isNullOrEmpty(appID) && !Utility.isNullOrEmpty(clientToken)) { String accessToken = appID + "|" + clientToken; this.parameters.putString(ACCESS_TOKEN_PARAM, accessToken); } else { Log.d(TAG, "Warning: Request without access token missing application ID or" + " client token."); } } this.parameters.putString(SDK_PARAM, SDK_ANDROID); this.parameters.putString(FORMAT_PARAM, FORMAT_JSON); if (FacebookSdk.isLoggingBehaviorEnabled(LoggingBehavior.GRAPH_API_DEBUG_INFO)) { this.parameters.putString(DEBUG_PARAM, DEBUG_SEVERITY_INFO); } else if (FacebookSdk.isLoggingBehaviorEnabled(LoggingBehavior.GRAPH_API_DEBUG_WARNING)) { this.parameters.putString(DEBUG_PARAM, DEBUG_SEVERITY_WARNING); } } private String appendParametersToBaseUrl(String baseUrl) { Uri.Builder uriBuilder = Uri.parse(baseUrl).buildUpon(); 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( Locale.US, "Unsupported parameter type for GET request: %s", value.getClass().getSimpleName())); } continue; } uriBuilder.appendQueryParameter(key, value.toString()); } return uriBuilder.toString(); } final String getRelativeUrlForBatchedRequest() { if (overriddenURL != null) { throw new FacebookException("Can't override URL for a batch request"); } String baseUrl = String.format("%s/%s", ServerProtocol.getGraphUrlBase(), getGraphPathWithVersion()); addCommonParameters(); String fullUrl = appendParametersToBaseUrl(baseUrl); Uri uri = Uri.parse(fullUrl); String relativeUrl = String.format("%s?%s", uri.getPath(), uri.getQuery()); return relativeUrl; } 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 GraphRequest request; private final Object value; public Attachment(GraphRequest request, Object value) { this.request = request; this.value = value; } public GraphRequest 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 = getRelativeUrlForBatchedRequest(); batchEntry.put(BATCH_RELATIVE_URL_PARAM, relativeURL); batchEntry.put(BATCH_METHOD_PARAM, httpMethod); if (this.accessToken != null) { String token = this.accessToken.getToken(); Logger.registerAccessToken(token); } // 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( Locale.ROOT, "%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( Locale.US, "%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(GraphRequestBatch requests) { for (GraphRequestBatch.Callback callback : requests.getCallbacks()) { if (callback instanceof GraphRequestBatch.OnProgressCallback) { return true; } } for (GraphRequest request : requests) { if (request.getCallback() instanceof OnProgressCallback) { return true; } } return false; } private static void setConnectionContentType( HttpURLConnection connection, boolean shouldUseGzip) { if (shouldUseGzip) { connection.setRequestProperty(CONTENT_TYPE_HEADER, "application/x-www-form-urlencoded"); connection.setRequestProperty(CONTENT_ENCODING_HEADER, "gzip"); } else { connection.setRequestProperty(CONTENT_TYPE_HEADER, getMimeContentType()); } } private static boolean isGzipCompressible(GraphRequestBatch requests) { for (GraphRequest request : requests) { for (String key : request.parameters.keySet()) { Object value = request.parameters.get(key); if (isSupportedAttachmentType(value)) { return false; } } } return true; } final static boolean shouldWarnOnMissingFieldsParam(GraphRequest request) { String version = request.getVersion(); if (Utility.isNullOrEmpty(version)) { // null implies latest version return true; } if (version.startsWith("v")) { version = version.substring(1); } String [] versionParts = version.split("\\."); // We should warn on missing "fields" params for API 2.4 and above return versionParts.length >= 2 && Integer.parseInt(versionParts[0]) > 2 || (Integer.parseInt(versionParts[0]) >= 2 && Integer.parseInt(versionParts[1]) >= 4); } final static void validateFieldsParamForGetRequests(GraphRequestBatch requests) { // validate that the GET requests all have a "fields" param for (GraphRequest request : requests) { if (HttpMethod.GET.equals(request.getHttpMethod()) && shouldWarnOnMissingFieldsParam(request)) { Bundle params = request.getParameters(); if (!params.containsKey(FIELDS_PARAM) || Utility.isNullOrEmpty(params.getString(FIELDS_PARAM))) { Logger.log( LoggingBehavior.DEVELOPER_ERRORS, Log.WARN, "Request", "starting with Graph API v2.4, GET requests for /%s should contain an" + " explicit \"fields\" parameter.", request.getGraphPath() ); } } } } final static void serializeToUrlConnection( GraphRequestBatch requests, HttpURLConnection connection ) throws IOException, JSONException { Logger logger = new Logger(LoggingBehavior.REQUESTS, "Request"); int numRequests = requests.size(); boolean shouldUseGzip = isGzipCompressible(requests); HttpMethod connectionHttpMethod = (numRequests == 1) ? requests.get(0).httpMethod : HttpMethod.POST; connection.setRequestMethod(connectionHttpMethod.name()); setConnectionContentType(connection, shouldUseGzip); 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 { outputStream = new BufferedOutputStream(connection.getOutputStream()); if (shouldUseGzip) { outputStream = new GZIPOutputStream(outputStream); } if (hasOnProgressCallbacks(requests)) { ProgressNoopOutputStream countingStream = null; countingStream = new ProgressNoopOutputStream(requests.getCallbackHandler()); processRequest(requests, null, numRequests, url, countingStream, shouldUseGzip); int max = countingStream.getMaxProgress(); Map<GraphRequest, RequestProgress> progressMap = countingStream.getProgressMap(); outputStream = new ProgressOutputStream(outputStream, requests, progressMap, max); } processRequest(requests, logger, numRequests, url, outputStream, shouldUseGzip); } finally { if (outputStream != null) { outputStream.close(); } } logger.log(); } private static void processRequest(GraphRequestBatch requests, Logger logger, int numRequests, URL url, OutputStream outputStream, boolean shouldUseGzip) throws IOException, JSONException { Serializer serializer = new Serializer(outputStream, logger, shouldUseGzip); if (numRequests == 1) { GraphRequest 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( "App ID was not specified at the request or Settings."); } serializer.writeString(BATCH_APP_ID_PARAM, batchAppID); // We write out all the requests as JSON, remembering which file attachments they have, // then write out the attachments. Map<String, Attachment> attachments = new HashMap<String, Attachment>(); serializeRequestsAsJSON(serializer, requests, attachments); if (logger != null) { logger.append(" Attachments:\n"); } serializeAttachments(attachments, serializer); } } private static boolean isMeRequest(String path) { Matcher matcher = versionPattern.matcher(path); if (matcher.matches()) { // Group 1 contains the path aside from version path = matcher.group(1); } if (path.startsWith("me/") || path.startsWith("/me/")) { return true; } return false; } private static void processGraphObject( JSONObject 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); } Iterator<String> keyIterator = graphObject.keys(); while (keyIterator.hasNext()) { String key = keyIterator.next(); Object value = graphObject.opt(key); boolean passByValue = isOGAction && key.equalsIgnoreCase("image"); processGraphObjectProperty(key, value, serializer, passByValue); } } /** * Create an User Owned Open Graph object * * Use this method to create an open graph object, which can then be posted utilizing the same * GraphRequest methods as other GraphRequests. * * @param openGraphObject The open graph object to create. Only SharePhotos with the imageUrl * set are accepted through this helper method. * @return GraphRequest for creating the given openGraphObject * @throws FacebookException thrown in the case of a JSONException or in the case of invalid * format for SharePhoto (missing imageUrl) */ public static GraphRequest createOpenGraphObject(final ShareOpenGraphObject openGraphObject) throws FacebookException { String type = openGraphObject.getString("type"); if (type == null) { type = openGraphObject.getString("og:type"); } if (type == null) { throw new FacebookException("Open graph object type cannot be null"); } try { JSONObject stagedObject = (JSONObject) OpenGraphJSONUtility.toJSONValue( openGraphObject, new OpenGraphJSONUtility.PhotoJSONProcessor() { @Override public JSONObject toJSONObject(SharePhoto photo) { Uri photoUri = photo.getImageUrl(); JSONObject photoJSONObject = new JSONObject(); try { photoJSONObject.put( NativeProtocol.IMAGE_URL_KEY, photoUri.toString()); } catch (Exception e) { throw new FacebookException("Unable to attach images", e); } return photoJSONObject; } }); String ogType = type; Bundle parameters = new Bundle(); parameters.putString("object", stagedObject.toString()); String graphPath = String.format( Locale.ROOT, GRAPH_PATH_FORMAT, ME, "objects/" + ogType); return new GraphRequest( AccessToken.getCurrentAccessToken(), graphPath, parameters, HttpMethod.POST); } catch(JSONException e){ throw new FacebookException(e.getMessage()); } } private static void processGraphObjectProperty( String key, Object value, KeyValueSerializer serializer, boolean passByValue ) throws IOException { Class<?> 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(Locale.ROOT, "%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, GraphRequest 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<GraphRequest> requests, Map<String, Attachment> attachments ) throws JSONException, IOException { JSONArray batch = new JSONArray(); for (GraphRequest 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); // For the unity sdk we need to append the unity user agent String customUserAgent = InternalSettings.getCustomUserAgent(); if (!Utility.isNullOrEmpty(customUserAgent)) { userAgent = String.format( Locale.ROOT, "%s/%s", userAgent, customUserAgent); } } return userAgent; } private static String getBatchAppId(GraphRequestBatch batch) { if (!Utility.isNullOrEmpty(batch.getBatchApplicationId())) { return batch.getBatchApplicationId(); } for (GraphRequest request : batch) { AccessToken accessToken = request.accessToken; if (accessToken != null) { String applicationId = accessToken.getApplicationId(); if (applicationId != null) { return applicationId; } } } if (!Utility.isNullOrEmpty(GraphRequest.defaultBatchApplicationId)) { return GraphRequest.defaultBatchApplicationId; } return FacebookSdk.getApplicationId(); } private static boolean isSupportedAttachmentType(Object value) { return value instanceof Bitmap || value instanceof byte[] || value instanceof Uri || value instanceof ParcelFileDescriptor || value instanceof ParcelableResourceWithMimeType; } 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; private boolean useUrlEncode = false; public Serializer(OutputStream outputStream, Logger logger, boolean useUrlEncode) { this.outputStream = outputStream; this.logger = logger; this.useUrlEncode = useUrlEncode; } public void writeObject(String key, Object value, GraphRequest 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 Uri) { writeContentUri(key, (Uri) value, null); } else if (value instanceof ParcelFileDescriptor) { writeFile(key, (ParcelFileDescriptor) value, null); } else if (value instanceof ParcelableResourceWithMimeType) { ParcelableResourceWithMimeType resourceWithMimeType = (ParcelableResourceWithMimeType) value; Parcelable resource = resourceWithMimeType.getResource(); String mimeType = resourceWithMimeType.getMimeType(); if (resource instanceof ParcelFileDescriptor) { writeFile(key, (ParcelFileDescriptor) resource, mimeType); } else if (resource instanceof Uri) { writeContentUri(key, (Uri) resource, mimeType); } else { throw getInvalidTypeError(); } } else { throw getInvalidTypeError(); } } private RuntimeException getInvalidTypeError() { return new IllegalArgumentException("value is not a supported type."); } public void writeRequestsAsJson( String key, JSONArray requestJsonArray, Collection<GraphRequest> 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 (GraphRequest 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(Locale.ROOT, "<Data: %d>", bytes.length)); } } public void writeContentUri(String key, Uri contentUri, 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 long contentSize = Utility.getContentSize(contentUri); ((ProgressNoopOutputStream) outputStream).addProgress(contentSize); } else { InputStream inputStream = FacebookSdk .getApplicationContext() .getContentResolver() .openInputStream(contentUri); totalBytes += Utility.copyAndCloseInputStream(inputStream, outputStream); } writeLine(""); writeRecordBoundary(); if (logger != null) { logger.appendKeyValue( " " + key, String.format(Locale.ROOT, "<Data: %d>", totalBytes)); } } 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 = new ParcelFileDescriptor.AutoCloseInputStream(descriptor); totalBytes += Utility.copyAndCloseInputStream(inputStream, outputStream); } writeLine(""); writeRecordBoundary(); if (logger != null) { logger.appendKeyValue( " " + key, String.format(Locale.ROOT, "<Data: %d>", totalBytes)); } } public void writeRecordBoundary() throws IOException { if (!useUrlEncode) { writeLine("--%s", MIME_BOUNDARY); } else { this.outputStream.write("&".getBytes()); } } public void writeContentDisposition( String name, String filename, String contentType ) throws IOException { if (!useUrlEncode) { 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 } else { this.outputStream.write(String.format("%s=", name).getBytes()); } } public void write(String format, Object... args) throws IOException { if (!useUrlEncode) { 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()); } else { this.outputStream.write( URLEncoder.encode( String.format(Locale.US, format, args), "UTF-8").getBytes()); } } public void writeLine(String format, Object... args) throws IOException { write(format, args); if (!useUrlEncode) { 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(GraphResponse 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 FacebookSdk#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); } /** * Callback for requests that result in an array of JSONObjects. */ public interface GraphJSONArrayCallback { /** * The method that will be called when the request completes. * * @param objects the list of GraphObjects representing the returned objects, or null * @param response the Response of this request, which may include error information if the * request was unsuccessful */ void onCompleted(JSONArray objects, GraphResponse response); } /** * Callback for requests that result in a JSONObject. */ public interface GraphJSONObjectCallback { /** * The method that will be called when the request completes. * * @param object the GraphObject representing the returned object, or null * @param response the Response of this request, which may include error information if the * request was unsuccessful */ void onCompleted(JSONObject object, GraphResponse response); } /** * Used during serialization for the graph request. * @param <RESOURCE> The Parcelable type parameter. */ public static class ParcelableResourceWithMimeType<RESOURCE extends Parcelable> implements Parcelable { private final String mimeType; private final RESOURCE resource; public String getMimeType() { return mimeType; } public RESOURCE getResource() { return resource; } public int describeContents() { return CONTENTS_FILE_DESCRIPTOR; } public void writeToParcel(Parcel out, int flags) { out.writeString(mimeType); out.writeParcelable(resource, flags); } @SuppressWarnings("unused") public static final Parcelable.Creator<ParcelableResourceWithMimeType> CREATOR = new Parcelable.Creator<ParcelableResourceWithMimeType>() { public ParcelableResourceWithMimeType createFromParcel(Parcel in) { return new ParcelableResourceWithMimeType(in); } public ParcelableResourceWithMimeType[] newArray(int size) { return new ParcelableResourceWithMimeType[size]; } }; /** * The constructor. * @param resource The resource to parcel. * @param mimeType The mime type. */ public ParcelableResourceWithMimeType( RESOURCE resource, String mimeType ) { this.mimeType = mimeType; this.resource = resource; } private ParcelableResourceWithMimeType(Parcel in) { mimeType = in.readString(); resource = in.readParcelable(FacebookSdk.getApplicationContext().getClassLoader()); } } }