/** * 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.internal; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.support.annotation.Nullable; import android.util.Pair; import com.facebook.AccessToken; import com.facebook.CallbackManager; import com.facebook.FacebookCallback; import com.facebook.FacebookException; import com.facebook.FacebookGraphResponseException; import com.facebook.FacebookOperationCanceledException; import com.facebook.FacebookRequestError; import com.facebook.FacebookSdk; import com.facebook.GraphRequest; import com.facebook.GraphRequest.Callback; import com.facebook.GraphResponse; import com.facebook.HttpMethod; import com.facebook.appevents.AppEventsLogger; import com.facebook.internal.AnalyticsEvents; import com.facebook.internal.AppCall; import com.facebook.internal.CallbackManagerImpl; import com.facebook.internal.NativeAppCallAttachmentStore; import com.facebook.internal.NativeProtocol; import com.facebook.internal.Utility; import com.facebook.share.Sharer; import com.facebook.share.model.ShareMedia; import com.facebook.share.model.ShareMediaContent; import com.facebook.share.model.ShareOpenGraphAction; import com.facebook.share.model.ShareOpenGraphContent; import com.facebook.share.model.SharePhoto; import com.facebook.share.model.SharePhotoContent; import com.facebook.share.model.ShareVideo; import com.facebook.share.model.ShareVideoContent; import com.facebook.share.widget.LikeView; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; /** * com.facebook.share.internal is solely for the use of other packages within the * Facebook SDK for Android. Use of any of the classes in this package is * unsupported, and they may be modified or removed without warning at any time. */ public final class ShareInternalUtility { public static final String MY_PHOTOS = "me/photos"; private static final String MY_STAGING_RESOURCES = "me/staging_resources"; // Parameter names/values private static final String STAGING_PARAM = "file"; public static void invokeCallbackWithException( FacebookCallback<Sharer.Result> callback, final Exception exception) { if (exception instanceof FacebookException) { invokeOnErrorCallback(callback, (FacebookException) exception); return; } invokeCallbackWithError( callback, "Error preparing share content: " + exception.getLocalizedMessage()); } public static void invokeCallbackWithError( FacebookCallback<Sharer.Result> callback, String error) { invokeOnErrorCallback(callback, error); } public static void invokeCallbackWithResults( FacebookCallback<Sharer.Result> callback, final String postId, final GraphResponse graphResponse) { FacebookRequestError requestError = graphResponse.getError(); if (requestError != null) { String errorMessage = requestError.getErrorMessage(); if (Utility.isNullOrEmpty(errorMessage)) { errorMessage = "Unexpected error sharing."; } invokeOnErrorCallback(callback, graphResponse, errorMessage); } else { invokeOnSuccessCallback(callback, postId); } } /** * Returns the gesture with which the user completed the native dialog. This is only returned * if the user has previously authorized the calling app with basic permissions. * * @param result the bundle passed back to onActivityResult * @return "post" or "cancel" as the completion gesture */ public static String getNativeDialogCompletionGesture(Bundle result) { if (result.containsKey(NativeProtocol.RESULT_ARGS_DIALOG_COMPLETION_GESTURE_KEY)) { return result.getString(NativeProtocol.RESULT_ARGS_DIALOG_COMPLETION_GESTURE_KEY); } return result.getString(NativeProtocol.EXTRA_DIALOG_COMPLETION_GESTURE_KEY); } /** * Returns the id of the published post. This is only returned if the user has previously * given the app publish permissions. * * @param result the bundle passed back to onActivityResult * @return the id of the published post */ public static String getShareDialogPostId(Bundle result) { if (result.containsKey(ShareConstants.RESULT_POST_ID)) { return result.getString(ShareConstants.RESULT_POST_ID); } if (result.containsKey(ShareConstants.EXTRA_RESULT_POST_ID)) { return result.getString(ShareConstants.EXTRA_RESULT_POST_ID); } return result.getString(ShareConstants.WEB_DIALOG_RESULT_PARAM_POST_ID); } public static boolean handleActivityResult( int requestCode, int resultCode, Intent data, ResultProcessor resultProcessor) { AppCall appCall = getAppCallFromActivityResult(requestCode, resultCode, data); if (appCall == null) { return false; } NativeAppCallAttachmentStore.cleanupAttachmentsForCall(appCall.getCallId()); if (resultProcessor == null) { return true; } FacebookException exception = NativeProtocol.getExceptionFromErrorData( NativeProtocol.getErrorDataFromResultIntent(data)); if (exception != null) { if (exception instanceof FacebookOperationCanceledException) { resultProcessor.onCancel(appCall); } else { resultProcessor.onError(appCall, exception); } } else { // If here, we did not find an error in the result. Bundle results = NativeProtocol.getSuccessResultsFromIntent(data); resultProcessor.onSuccess(appCall, results); } return true; } // Custom handling for Share so that we can log results public static ResultProcessor getShareResultProcessor( final FacebookCallback<Sharer.Result> callback) { return new ResultProcessor(callback) { @Override public void onSuccess(AppCall appCall, Bundle results) { if (results != null) { final String gesture = getNativeDialogCompletionGesture(results); if (gesture == null || "post".equalsIgnoreCase(gesture)) { String postId = getShareDialogPostId(results); invokeOnSuccessCallback(callback, postId); } else if ("cancel".equalsIgnoreCase(gesture)) { invokeOnCancelCallback(callback); } else { invokeOnErrorCallback( callback, new FacebookException(NativeProtocol.ERROR_UNKNOWN_ERROR)); } } } @Override public void onCancel(AppCall appCall) { invokeOnCancelCallback(callback); } @Override public void onError(AppCall appCall, FacebookException error) { invokeOnErrorCallback(callback, error); } }; } private static AppCall getAppCallFromActivityResult(int requestCode, int resultCode, Intent data) { UUID callId = NativeProtocol.getCallIdFromIntent(data); if (callId == null) { return null; } return AppCall.finishPendingCall(callId, requestCode); } public static void registerStaticShareCallback( final int requestCode) { CallbackManagerImpl.registerStaticCallback( requestCode, new CallbackManagerImpl.Callback() { @Override public boolean onActivityResult(int resultCode, Intent data) { return handleActivityResult( requestCode, resultCode, data, getShareResultProcessor(null)); } } ); } public static void registerSharerCallback( final int requestCode, final CallbackManager callbackManager, final FacebookCallback<Sharer.Result> callback) { if (!(callbackManager instanceof CallbackManagerImpl)) { throw new FacebookException("Unexpected CallbackManager, " + "please use the provided Factory."); } ((CallbackManagerImpl) callbackManager).registerCallback( requestCode, new CallbackManagerImpl.Callback() { @Override public boolean onActivityResult(int resultCode, Intent data) { return handleActivityResult( requestCode, resultCode, data, getShareResultProcessor(callback)); } }); } public static List<String> getPhotoUrls( final SharePhotoContent photoContent, final UUID appCallId) { List<SharePhoto> photos; if (photoContent == null || (photos = photoContent.getPhotos()) == null) { return null; } List<NativeAppCallAttachmentStore.Attachment> attachments = Utility.map( photos, new Utility.Mapper<SharePhoto, NativeAppCallAttachmentStore.Attachment>() { @Override public NativeAppCallAttachmentStore.Attachment apply(SharePhoto item) { return getAttachment(appCallId, item); } }); List<String> attachmentUrls = Utility.map( attachments, new Utility.Mapper<NativeAppCallAttachmentStore.Attachment, String>() { @Override public String apply(NativeAppCallAttachmentStore.Attachment item) { return item.getAttachmentUrl(); } }); NativeAppCallAttachmentStore.addAttachments(attachments); return attachmentUrls; } public static String getVideoUrl(final ShareVideoContent videoContent, final UUID appCallId) { if (videoContent == null || videoContent.getVideo() == null) { return null; } NativeAppCallAttachmentStore.Attachment attachment = NativeAppCallAttachmentStore.createAttachment( appCallId, videoContent.getVideo().getLocalUrl()); ArrayList<NativeAppCallAttachmentStore.Attachment> attachments = new ArrayList<>(1); attachments.add(attachment); NativeAppCallAttachmentStore.addAttachments(attachments); return attachment.getAttachmentUrl(); } public static List<Bundle> getMediaInfos( final ShareMediaContent mediaContent, final UUID appCallId) { final List<ShareMedia> media; if (mediaContent == null || (media = mediaContent.getMedia()) == null) { return null; } final List<NativeAppCallAttachmentStore.Attachment> attachments = new ArrayList<>(); List<Bundle> mediaInfos = Utility.map( media, new Utility.Mapper<ShareMedia, Bundle>() { @Override public Bundle apply(ShareMedia item) { NativeAppCallAttachmentStore.Attachment attachment = getAttachment(appCallId, item); attachments.add(attachment); Bundle mediaInfo = new Bundle(); mediaInfo.putString( ShareConstants.MEDIA_TYPE, item.getMediaType().name()); mediaInfo.putString( ShareConstants.MEDIA_URI, attachment.getAttachmentUrl()); return mediaInfo; } }); NativeAppCallAttachmentStore.addAttachments(attachments); return mediaInfos; } public static JSONObject toJSONObjectForCall( final UUID callId, final ShareOpenGraphContent content) throws JSONException { final ShareOpenGraphAction action = content.getAction(); final ArrayList<NativeAppCallAttachmentStore.Attachment> attachments = new ArrayList<>(); JSONObject actionJSON = OpenGraphJSONUtility.toJSONObject( action, new OpenGraphJSONUtility.PhotoJSONProcessor() { @Override public JSONObject toJSONObject(SharePhoto photo) { NativeAppCallAttachmentStore.Attachment attachment = getAttachment( callId, photo); if (attachment == null) { return null; } attachments.add(attachment); JSONObject photoJSONObject = new JSONObject(); try { photoJSONObject.put( NativeProtocol.IMAGE_URL_KEY, attachment.getAttachmentUrl()); if (photo.getUserGenerated()) { photoJSONObject.put(NativeProtocol.IMAGE_USER_GENERATED_KEY, true); } } catch (JSONException e) { throw new FacebookException("Unable to attach images", e); } return photoJSONObject; } }); NativeAppCallAttachmentStore.addAttachments(attachments); // People and place tags must be moved from the share content to the open graph action if (content.getPlaceId() != null) { String placeTag = actionJSON.optString("place"); // Only if the place tag is already empty or null replace with the id from the // share content if (Utility.isNullOrEmpty(placeTag)) { actionJSON.put("place", content.getPlaceId()); } } if (content.getPeopleIds() != null) { JSONArray peopleTags = actionJSON.optJSONArray("tags"); Set<String> peopleIdSet = peopleTags == null ? new HashSet<String>() : Utility.jsonArrayToSet(peopleTags); for (String peopleId : content.getPeopleIds()) { peopleIdSet.add(peopleId); } actionJSON.put("tags", new JSONArray(peopleIdSet)); } return actionJSON; } public static JSONObject toJSONObjectForWeb( final ShareOpenGraphContent shareOpenGraphContent) throws JSONException { ShareOpenGraphAction action = shareOpenGraphContent.getAction(); return OpenGraphJSONUtility.toJSONObject( action, 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 (JSONException e) { throw new FacebookException("Unable to attach images", e); } return photoJSONObject; } }); } public static JSONArray removeNamespacesFromOGJsonArray( JSONArray jsonArray, boolean requireNamespace) throws JSONException { JSONArray newArray = new JSONArray(); for (int i = 0; i < jsonArray.length(); ++i) { Object value = jsonArray.get(i); if (value instanceof JSONArray) { value = removeNamespacesFromOGJsonArray((JSONArray) value, requireNamespace); } else if (value instanceof JSONObject) { value = removeNamespacesFromOGJsonObject((JSONObject) value, requireNamespace); } newArray.put(value); } return newArray; } public static JSONObject removeNamespacesFromOGJsonObject( JSONObject jsonObject, boolean requireNamespace) { if (jsonObject == null) { return null; } try { JSONObject newJsonObject = new JSONObject(); JSONObject data = new JSONObject(); JSONArray names = jsonObject.names(); for (int i = 0; i < names.length(); ++i) { String key = names.getString(i); Object value; value = jsonObject.get(key); if (value instanceof JSONObject) { value = removeNamespacesFromOGJsonObject((JSONObject) value, true); } else if (value instanceof JSONArray) { value = removeNamespacesFromOGJsonArray((JSONArray) value, true); } Pair<String, String> fieldNameAndNamespace = getFieldNameAndNamespaceFromFullName( key); String namespace = fieldNameAndNamespace.first; String fieldName = fieldNameAndNamespace.second; if (requireNamespace) { if (namespace != null && namespace.equals("fbsdk")) { newJsonObject.put(key, value); } else if (namespace == null || namespace.equals("og")) { newJsonObject.put(fieldName, value); } else { data.put(fieldName, value); } } else if (namespace != null && namespace.equals("fb")) { newJsonObject.put(key, value); } else { newJsonObject.put(fieldName, value); } } if (data.length() > 0) { newJsonObject.put("data", data); } return newJsonObject; } catch (JSONException e) { throw new FacebookException("Failed to create json object from share content"); } } public static Pair<String, String> getFieldNameAndNamespaceFromFullName(String fullName) { String namespace = null; String fieldName; int index = fullName.indexOf(':'); if (index != -1 && fullName.length() > index + 1) { namespace = fullName.substring(0, index); fieldName = fullName.substring(index + 1); } else { fieldName = fullName; } return new Pair<>(namespace, fieldName); } private static NativeAppCallAttachmentStore.Attachment getAttachment( UUID callId, ShareMedia medium) { Bitmap bitmap = null; Uri uri = null; if (medium instanceof SharePhoto) { SharePhoto photo = (SharePhoto)medium; bitmap = photo.getBitmap(); uri = photo.getImageUrl(); } else if (medium instanceof ShareVideo) { ShareVideo video = (ShareVideo)medium; uri = video.getLocalUrl(); } NativeAppCallAttachmentStore.Attachment attachment = null; if (bitmap != null) { attachment = NativeAppCallAttachmentStore.createAttachment( callId, bitmap); } else if (uri != null) { attachment = NativeAppCallAttachmentStore.createAttachment( callId, uri); } return attachment; } static void invokeOnCancelCallback(FacebookCallback<Sharer.Result> callback) { logShareResult(AnalyticsEvents.PARAMETER_SHARE_OUTCOME_CANCELLED, null); if (callback != null) { callback.onCancel(); } } static void invokeOnSuccessCallback( FacebookCallback<Sharer.Result> callback, String postId) { logShareResult(AnalyticsEvents.PARAMETER_SHARE_OUTCOME_SUCCEEDED, null); if (callback != null) { callback.onSuccess(new Sharer.Result(postId)); } } static void invokeOnErrorCallback( FacebookCallback<Sharer.Result> callback, GraphResponse response, String message) { logShareResult(AnalyticsEvents.PARAMETER_SHARE_OUTCOME_ERROR, message); if (callback != null) { callback.onError(new FacebookGraphResponseException(response, message)); } } static void invokeOnErrorCallback( FacebookCallback<Sharer.Result> callback, String message) { logShareResult(AnalyticsEvents.PARAMETER_SHARE_OUTCOME_ERROR, message); if (callback != null) { callback.onError(new FacebookException(message)); } } static void invokeOnErrorCallback( FacebookCallback<Sharer.Result> callback, FacebookException ex) { logShareResult(AnalyticsEvents.PARAMETER_SHARE_OUTCOME_ERROR, ex.getMessage()); if (callback != null) { callback.onError(ex); } } private static void logShareResult(String shareOutcome, String errorMessage) { Context context = FacebookSdk.getApplicationContext(); AppEventsLogger logger = AppEventsLogger.newLogger(context); Bundle parameters = new Bundle(); parameters.putString( AnalyticsEvents.PARAMETER_SHARE_OUTCOME, shareOutcome ); if (errorMessage != null) { parameters.putString(AnalyticsEvents.PARAMETER_SHARE_ERROR_MESSAGE, errorMessage); } logger.logSdkEvent(AnalyticsEvents.EVENT_SHARE_RESULT, null, parameters); } /** * Creates a new Request configured to upload an image to create a staging resource. Staging * resources allow you to post binary data such as images, in preparation for a post of an Open * Graph object or action which references the image. The URI returned when uploading a staging * resource may be passed as the image property for an Open Graph object or action. * * @param accessToken the access token to use, or null * @param image the image to upload * @param callback a callback that will be called when the request is completed to handle * success or error conditions * @return a Request that is ready to execute */ public static GraphRequest newUploadStagingResourceWithImageRequest( AccessToken accessToken, Bitmap image, Callback callback) { Bundle parameters = new Bundle(1); parameters.putParcelable(STAGING_PARAM, image); return new GraphRequest( accessToken, MY_STAGING_RESOURCES, parameters, HttpMethod.POST, callback); } /** * Creates a new Request configured to upload an image to create a staging resource. Staging * resources allow you to post binary data such as images, in preparation for a post of an Open * Graph object or action which references the image. The URI returned when uploading a staging * resource may be passed as the image property for an Open Graph object or action. * * @param accessToken the access token to use, or null * @param file the file containing the image to upload * @param callback a callback that will be called when the request is completed to handle * success or error conditions * @return a Request that is ready to execute * @throws FileNotFoundException */ public static GraphRequest newUploadStagingResourceWithImageRequest( AccessToken accessToken, File file, Callback callback ) throws FileNotFoundException { ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); GraphRequest.ParcelableResourceWithMimeType<ParcelFileDescriptor> resourceWithMimeType = new GraphRequest.ParcelableResourceWithMimeType<>(descriptor, "image/png"); Bundle parameters = new Bundle(1); parameters.putParcelable(STAGING_PARAM, resourceWithMimeType); return new GraphRequest( accessToken, MY_STAGING_RESOURCES, parameters, HttpMethod.POST, callback); } /** * Creates a new Request configured to upload an image to create a staging resource. Staging * resources allow you to post binary data such as images, in preparation for a post of an Open * Graph object or action which references the image. The URI returned when uploading a staging * resource may be passed as the image property for an Open Graph object or action. * * @param accessToken the access token to use, or null * @param imageUri the file:// or content:// Uri pointing to the image to upload * @param callback a callback that will be called when the request is completed to handle * success or error conditions * @return a Request that is ready to execute * @throws FileNotFoundException */ public static GraphRequest newUploadStagingResourceWithImageRequest( AccessToken accessToken, Uri imageUri, Callback callback ) throws FileNotFoundException { if (Utility.isFileUri(imageUri)) { return newUploadStagingResourceWithImageRequest( accessToken, new File(imageUri.getPath()), callback); } else if (!Utility.isContentUri(imageUri)) { throw new FacebookException("The image Uri must be either a file:// or content:// Uri"); } GraphRequest.ParcelableResourceWithMimeType<Uri> resourceWithMimeType = new GraphRequest.ParcelableResourceWithMimeType<>(imageUri, "image/png"); Bundle parameters = new Bundle(1); parameters.putParcelable(STAGING_PARAM, resourceWithMimeType); return new GraphRequest( accessToken, MY_STAGING_RESOURCES, parameters, HttpMethod.POST, callback); } @Nullable public static LikeView.ObjectType getMostSpecificObjectType( LikeView.ObjectType objectType1, LikeView.ObjectType objectType2) { if (objectType1 == objectType2) { return objectType1; } if (objectType1 == LikeView.ObjectType.UNKNOWN) { return objectType2; } else if (objectType2 == LikeView.ObjectType.UNKNOWN) { return objectType1; } else { // We can't have a PAGE and an OPEN_GRAPH type be compatible. return null; } } }