/** * 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.share; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import com.facebook.AccessToken; import com.facebook.FacebookCallback; import com.facebook.FacebookException; import com.facebook.FacebookGraphResponseException; import com.facebook.FacebookRequestError; import com.facebook.GraphRequest; import com.facebook.GraphResponse; import com.facebook.HttpMethod; import com.facebook.internal.CollectionMapper; import com.facebook.internal.Mutable; import com.facebook.internal.Utility; import com.facebook.share.internal.ShareContentValidation; import com.facebook.share.internal.ShareInternalUtility; import com.facebook.share.model.*; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.FileNotFoundException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.*; /** * Provides an interface for sharing through the graph API. Using this class requires an access * token in AccessToken.currentAccessToken that has been granted the "publish_actions" permission. */ public final class ShareApi { private final ShareContent shareContent; /** * Convenience method to share a piece of content. * * @param shareContent the content to share. * @param callback the callback to call once the share is complete. */ public static void share( final ShareContent shareContent, final FacebookCallback<Sharer.Result> callback) { new ShareApi(shareContent) .share(callback); } /** * Constructs a new instance. * * @param shareContent the content to share. */ public ShareApi(final ShareContent shareContent) { this.shareContent = shareContent; } /** * Returns the content to be shared. * * @return the content to be shared. */ public ShareContent getShareContent() { return this.shareContent; } /** * Returns true if the current access token has the publish_actions permission. * * @return true if the current access token has the publish_actions permission, false otherwise. */ public boolean canShare() { if (this.getShareContent() == null) { return false; } final AccessToken accessToken = AccessToken.getCurrentAccessToken(); if (accessToken == null) { return false; } final Set<String> permissions = accessToken.getPermissions(); if (permissions == null) { return false; } return (permissions.contains("publish_actions")); } /** * Share the content. * * @param callback the callback to call once the share is complete. */ public void share(FacebookCallback<Sharer.Result> callback) { if (!this.canShare()) { ShareInternalUtility.invokeCallbackWithError( callback, "Insufficient permissions for sharing content via Api."); return; } final ShareContent shareContent = this.getShareContent(); // Validate the share content try { ShareContentValidation.validateForApiShare(shareContent); } catch (FacebookException ex) { ShareInternalUtility.invokeCallbackWithException(callback, ex); return; } if (shareContent instanceof ShareLinkContent) { this.shareLinkContent((ShareLinkContent) shareContent, callback); } else if (shareContent instanceof SharePhotoContent) { this.sharePhotoContent((SharePhotoContent) shareContent, callback); } else if (shareContent instanceof ShareVideoContent) { this.shareVideoContent((ShareVideoContent) shareContent, callback); } else if (shareContent instanceof ShareOpenGraphContent) { this.shareOpenGraphContent((ShareOpenGraphContent) shareContent, callback); } } private void shareOpenGraphContent(final ShareOpenGraphContent openGraphContent, final FacebookCallback<Sharer.Result> callback) { // In order to create a new Open Graph action using a custom object that does not already // exist (objectID or URL), you must first send a request to post the object and then // another to post the action. If a local image is supplied with the object or action, that // must be staged first and then referenced by the staging URL that is returned by that // request. final GraphRequest.Callback requestCallback = new GraphRequest.Callback() { @Override public void onCompleted(GraphResponse response) { final JSONObject data = response.getJSONObject(); final String postId = (data == null ? null : data.optString("id")); ShareInternalUtility.invokeCallbackWithResults(callback, postId, response); } }; final ShareOpenGraphAction action = openGraphContent.getAction(); final Bundle parameters = action.getBundle(); final CollectionMapper.OnMapperCompleteListener stageCallback = new CollectionMapper .OnMapperCompleteListener() { @Override public void onComplete() { try { handleImagesOnAction(parameters); new GraphRequest( AccessToken.getCurrentAccessToken(), "/me/" + URLEncoder.encode(action.getActionType(), "UTF-8"), parameters, HttpMethod.POST, requestCallback).executeAsync(); } catch (final UnsupportedEncodingException ex) { ShareInternalUtility.invokeCallbackWithException(callback, ex); } } @Override public void onError(FacebookException exception) { ShareInternalUtility.invokeCallbackWithException(callback, exception); } }; this.stageOpenGraphAction(parameters, stageCallback); } private static void handleImagesOnAction(Bundle parameters) { // 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. String imageStr = parameters.getString("image"); if (imageStr != null) { try { // Check to see if this is an json array. Will throw if not JSONArray images = new JSONArray(imageStr); for (int i = 0; i < images.length(); ++i) { JSONObject jsonImage = images.optJSONObject(i); if(jsonImage != null) { putImageInBundleWithArrayFormat(parameters, i, jsonImage); } else { // If we don't have jsonImage we probably just have a url String url = images.getString(i); parameters.putString(String.format(Locale.ROOT, "image[%d][url]", i), url); } } parameters.remove("image"); return; } catch (JSONException ex) { // We couldn't parse the string as an array } // If the image is not in an array it might just be an single photo try { JSONObject image = new JSONObject(imageStr); putImageInBundleWithArrayFormat(parameters, 0, image); parameters.remove("image"); } catch (JSONException exception) { // The image was not in array format or a json object and can be safely passed // without modification } } } private static void putImageInBundleWithArrayFormat( Bundle parameters, int index, JSONObject image) throws JSONException{ Iterator<String> keys = image.keys(); while (keys.hasNext()) { String property = keys.next(); String key = String.format(Locale.ROOT, "image[%d][%s]", index, property); parameters.putString(key, image.get(property).toString()); } } private void sharePhotoContent(final SharePhotoContent photoContent, final FacebookCallback<Sharer.Result> callback) { final Mutable<Integer> requestCount = new Mutable<Integer>(0); final AccessToken accessToken = AccessToken.getCurrentAccessToken(); final ArrayList<GraphRequest> requests = new ArrayList<GraphRequest>(); final ArrayList<JSONObject> results = new ArrayList<JSONObject>(); final ArrayList<GraphResponse> errorResponses = new ArrayList<GraphResponse>(); final GraphRequest.Callback requestCallback = new GraphRequest.Callback() { @Override public void onCompleted(GraphResponse response) { final JSONObject result = response.getJSONObject(); if (result != null) { results.add(result); } if (response.getError() != null) { errorResponses.add(response); } requestCount.value -= 1; if (requestCount.value == 0) { if (!errorResponses.isEmpty()) { ShareInternalUtility.invokeCallbackWithResults( callback, null, errorResponses.get(0)); } else if (!results.isEmpty()) { final String postId = results.get(0).optString("id"); ShareInternalUtility.invokeCallbackWithResults( callback, postId, response); } } } }; try { for (SharePhoto photo : photoContent.getPhotos()) { final Bitmap bitmap = photo.getBitmap(); final Uri photoUri = photo.getImageUrl(); if (bitmap != null) { requests.add(ShareInternalUtility.newUploadPhotoRequest( accessToken, bitmap, requestCallback)); } else if (photoUri != null) { requests.add(ShareInternalUtility.newUploadPhotoRequest( accessToken, photoUri, requestCallback)); } } requestCount.value += requests.size(); for (GraphRequest request : requests) { request.executeAsync(); } } catch (final FileNotFoundException ex) { ShareInternalUtility.invokeCallbackWithException(callback, ex); } } private void shareLinkContent(final ShareLinkContent linkContent, final FacebookCallback<Sharer.Result> callback) { final GraphRequest.Callback requestCallback = new GraphRequest.Callback() { @Override public void onCompleted(GraphResponse response) { final JSONObject data = response.getJSONObject(); final String postId = (data == null ? null : data.optString("id")); ShareInternalUtility.invokeCallbackWithResults(callback, postId, response); } }; final Bundle parameters = new Bundle(); parameters.putString("link", Utility.getUriString(linkContent.getContentUrl())); parameters.putString("picture", Utility.getUriString(linkContent.getImageUrl())); parameters.putString("name", linkContent.getContentTitle()); parameters.putString("description", linkContent.getContentDescription()); parameters.putString("ref", linkContent.getRef()); new GraphRequest( AccessToken.getCurrentAccessToken(), "/me/feed", parameters, HttpMethod.POST, requestCallback).executeAsync(); } private void shareVideoContent(final ShareVideoContent videoContent, final FacebookCallback<Sharer.Result> callback) { final GraphRequest.Callback requestCallback = new GraphRequest.Callback() { @Override public void onCompleted(GraphResponse response) { String postId = null; if (response != null) { JSONObject responseJSON = response.getJSONObject(); if (responseJSON != null) { postId = responseJSON.optString("id"); } } ShareInternalUtility.invokeCallbackWithResults(callback, postId, response); } }; GraphRequest videoRequest; try { videoRequest = ShareInternalUtility.newUploadVideoRequest( AccessToken.getCurrentAccessToken(), videoContent.getVideo().getLocalUrl(), requestCallback); } catch (final FileNotFoundException ex) { ShareInternalUtility.invokeCallbackWithException(callback, ex); return; } final Bundle parameters = videoRequest.getParameters(); parameters.putString("title", videoContent.getContentTitle()); parameters.putString("description", videoContent.getContentDescription()); parameters.putString("ref", videoContent.getRef()); videoRequest.setParameters(parameters); videoRequest.executeAsync(); } private static void stageArrayList(final ArrayList arrayList, final CollectionMapper.OnMapValueCompleteListener onArrayListStagedListener) { final JSONArray stagedObject = new JSONArray(); final CollectionMapper.Collection<Integer> collection = new CollectionMapper .Collection<Integer>() { @Override public Iterator<Integer> keyIterator() { final int size = arrayList.size(); final Mutable<Integer> current = new Mutable<Integer>(0); return new Iterator<Integer>() { @Override public boolean hasNext() { return current.value < size; } @Override public Integer next() { return current.value++; } @Override public void remove() { } }; } @Override public Object get(Integer key) { return arrayList.get(key); } @Override public void set(Integer key, Object value, CollectionMapper.OnErrorListener onErrorListener) { try { stagedObject.put(key, value); } catch (final JSONException ex) { String message = ex.getLocalizedMessage(); if (message == null) { message = "Error staging object."; } onErrorListener.onError(new FacebookException(message)); } } }; final CollectionMapper.OnMapperCompleteListener onStagedArrayMapperCompleteListener = new CollectionMapper.OnMapperCompleteListener() { @Override public void onComplete() { onArrayListStagedListener.onComplete(stagedObject); } @Override public void onError(FacebookException exception) { onArrayListStagedListener.onError(exception); } }; stageCollectionValues(collection, onStagedArrayMapperCompleteListener); } private static <T> void stageCollectionValues(final CollectionMapper.Collection<T> collection, final CollectionMapper.OnMapperCompleteListener onCollectionValuesStagedListener) { final CollectionMapper.ValueMapper valueMapper = new CollectionMapper.ValueMapper() { @Override public void mapValue(Object value, CollectionMapper.OnMapValueCompleteListener onMapValueCompleteListener) { if (value instanceof ArrayList) { stageArrayList((ArrayList) value, onMapValueCompleteListener); } else if (value instanceof ShareOpenGraphObject) { stageOpenGraphObject( (ShareOpenGraphObject) value, onMapValueCompleteListener); } else if (value instanceof SharePhoto) { stagePhoto((SharePhoto) value, onMapValueCompleteListener); } else { onMapValueCompleteListener.onComplete(value); } } }; CollectionMapper.iterate(collection, valueMapper, onCollectionValuesStagedListener); } private static void stageOpenGraphAction(final Bundle parameters, final CollectionMapper.OnMapperCompleteListener onOpenGraphActionStagedListener) { final CollectionMapper.Collection<String> collection = new CollectionMapper .Collection<String>() { @Override public Iterator<String> keyIterator() { return parameters.keySet().iterator(); } @Override public Object get(String key) { return parameters.get(key); } @Override public void set(String key, Object value, CollectionMapper.OnErrorListener onErrorListener) { if (!Utility.putJSONValueInBundle(parameters, key, value)) { onErrorListener.onError( new FacebookException("Unexpected value: " + value.toString())); } } }; stageCollectionValues(collection, onOpenGraphActionStagedListener); } private static void stageOpenGraphObject(final ShareOpenGraphObject object, final CollectionMapper.OnMapValueCompleteListener onOpenGraphObjectStagedListener) { String type = object.getString("type"); if (type == null) { type = object.getString("og:type"); } if (type == null) { onOpenGraphObjectStagedListener.onError( new FacebookException("Open Graph objects must contain a type value.")); return; } final JSONObject stagedObject = new JSONObject(); final CollectionMapper.Collection<String> collection = new CollectionMapper .Collection<String>() { @Override public Iterator<String> keyIterator() { return object.keySet().iterator(); } @Override public Object get(String key) { return object.get(key); } @Override public void set(String key, Object value, CollectionMapper.OnErrorListener onErrorListener) { try { stagedObject.put(key, value); } catch (final JSONException ex) { String message = ex.getLocalizedMessage(); if (message == null) { message = "Error staging object."; } onErrorListener.onError(new FacebookException(message)); } } }; final GraphRequest.Callback requestCallback = new GraphRequest.Callback() { @Override public void onCompleted(GraphResponse response) { final FacebookRequestError error = response.getError(); if (error != null) { String message = error.getErrorMessage(); if (message == null) { message = "Error staging Open Graph object."; } onOpenGraphObjectStagedListener.onError( new FacebookGraphResponseException(response, message)); return; } final JSONObject data = response.getJSONObject(); if (data == null) { onOpenGraphObjectStagedListener.onError( new FacebookGraphResponseException(response, "Error staging Open Graph object.")); return; } final String stagedObjectId = data.optString("id"); if (stagedObjectId == null) { onOpenGraphObjectStagedListener.onError( new FacebookGraphResponseException(response, "Error staging Open Graph object.")); return; } onOpenGraphObjectStagedListener.onComplete(stagedObjectId); } }; final String ogType = type; final CollectionMapper.OnMapperCompleteListener onMapperCompleteListener = new CollectionMapper.OnMapperCompleteListener() { @Override public void onComplete() { final String objectString = stagedObject.toString(); final Bundle parameters = new Bundle(); parameters.putString("object", objectString); try { new GraphRequest( AccessToken.getCurrentAccessToken(), "/me/objects/" + URLEncoder.encode(ogType, "UTF-8"), parameters, HttpMethod.POST, requestCallback).executeAsync(); } catch (final UnsupportedEncodingException ex) { String message = ex.getLocalizedMessage(); if (message == null) { message = "Error staging Open Graph object."; } onOpenGraphObjectStagedListener.onError(new FacebookException(message)); } } @Override public void onError(FacebookException exception) { onOpenGraphObjectStagedListener.onError(exception); } }; stageCollectionValues(collection, onMapperCompleteListener); } private static void stagePhoto(final SharePhoto photo, final CollectionMapper.OnMapValueCompleteListener onPhotoStagedListener) { final Bitmap bitmap = photo.getBitmap(); final Uri imageUrl = photo.getImageUrl(); if ((bitmap != null) || (imageUrl != null)) { final GraphRequest.Callback requestCallback = new GraphRequest.Callback() { @Override public void onCompleted(GraphResponse response) { final FacebookRequestError error = response.getError(); if (error != null) { String message = error.getErrorMessage(); if (message == null) { message = "Error staging photo."; } onPhotoStagedListener.onError( new FacebookGraphResponseException(response, message)); return; } final JSONObject data = response.getJSONObject(); if (data == null) { onPhotoStagedListener.onError( new FacebookException("Error staging photo.")); return; } final String stagedImageUri = data.optString("uri"); if (stagedImageUri == null) { onPhotoStagedListener.onError( new FacebookException("Error staging photo.")); return; } final JSONObject stagedObject = new JSONObject(); try { stagedObject.put("url", stagedImageUri); stagedObject.put("user_generated", photo.getUserGenerated()); } catch (final JSONException ex) { String message = ex.getLocalizedMessage(); if (message == null) { message = "Error staging photo."; } onPhotoStagedListener.onError(new FacebookException(message)); return; } onPhotoStagedListener.onComplete(stagedObject); } }; if (bitmap != null) { ShareInternalUtility.newUploadStagingResourceWithImageRequest( AccessToken.getCurrentAccessToken(), bitmap, requestCallback).executeAsync(); } else { try { ShareInternalUtility.newUploadStagingResourceWithImageRequest( AccessToken.getCurrentAccessToken(), imageUrl, requestCallback).executeAsync(); } catch (final FileNotFoundException ex) { String message = ex.getLocalizedMessage(); if (message == null) { message = "Error staging photo."; } onPhotoStagedListener.onError(new FacebookException(message)); } } } else { onPhotoStagedListener.onError( new FacebookException("Photos must have an imageURL or bitmap.")); } } }