com.groundupworks.wings.facebook.FacebookEndpoint.java Source code

Java tutorial

Introduction

Here is the source code for com.groundupworks.wings.facebook.FacebookEndpoint.java

Source

/*
 * Copyright (C) 2012 Benedict Lau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.groundupworks.wings.facebook;

import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
import android.widget.Toast;

import com.facebook.FacebookException;
import com.facebook.FacebookRequestError;
import com.facebook.FacebookRequestError.Category;
import com.facebook.HttpMethod;
import com.facebook.Request;
import com.facebook.Request.Callback;
import com.facebook.Request.GraphUserCallback;
import com.facebook.RequestBatch;
import com.facebook.Response;
import com.facebook.Session;
import com.facebook.Session.NewPermissionsRequest;
import com.facebook.Session.OpenRequest;
import com.facebook.SessionDefaultAudience;
import com.facebook.SessionLoginBehavior;
import com.facebook.SessionState;
import com.facebook.model.GraphObject;
import com.groundupworks.wings.WingsEndpoint;
import com.groundupworks.wings.core.Destination;
import com.groundupworks.wings.core.ShareRequest;
import com.squareup.otto.Produce;

import org.json.JSONObject;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * The Wings endpoint for Facebook.
 *
 * @author Benedict Lau
 */
public class FacebookEndpoint extends WingsEndpoint {

    /**
     * Facebook endpoint id.
     */
    private static final int ENDPOINT_ID = 0;

    /**
     * Timeout value for http requests.
     */
    private static final int HTTP_REQUEST_TIMEOUT = 120000;

    /**
     * Facebook app package name.
     */
    private static final String FACEBOOK_APP_PACKAGE = "com.facebook.katana";

    //
    // Facebook permissions.
    //

    /**
     * Permission to public profile.
     */
    private static final String PERMISSION_PUBLIC_PROFILE = "public_profile";

    /**
     * Permission to user photos.
     */
    private static final String PERMISSION_USER_PHOTOS = "user_photos";

    /**
     * Permission to manage Pages.
     */
    private static final String PERMISSION_MANAGE_PAGES = "manage_pages";

    /**
     * Permission to publish content.
     */
    private static final String PERMISSION_PUBLISH_ACTIONS = "publish_actions";

    /**
     * Request code to use for {@link Activity#startActivityForResult(android.content.Intent, int)}.
     */
    private static final int SETTINGS_REQUEST_CODE = ENDPOINT_ID;

    //
    // Link request state machine.
    //

    private static final int STATE_NONE = -1;

    private static final int STATE_OPEN_SESSION_REQUEST = 0;

    private static final int STATE_PUBLISH_PERMISSIONS_REQUEST = 1;

    private static final int STATE_SETTINGS_REQUEST = 2;

    //
    // Account listing params.
    //

    /**
     * The graph path to list admin accounts for Pages.
     */
    private static final String ACCOUNTS_LISTING_GRAPH_PATH = "me/accounts";

    /**
     * The key for params to request.
     */
    private static final String ACCOUNTS_LISTING_FEILDS_KEY = "fields";

    /**
     * The value for params to request.
     */
    private static final String ACCOUNTS_LISTING_FIELDS_VALUE = "id,name,access_token,perms";

    //
    // Account listing results.
    //

    static final String ACCOUNTS_LISTING_RESULT_DATA_KEY = "data";

    static final String ACCOUNTS_LISTING_FIELD_ID = "id";

    static final String ACCOUNTS_LISTING_FIELD_NAME = "name";

    static final String ACCOUNTS_LISTING_FIELD_ACCESS_TOKEN = "access_token";

    static final String ACCOUNTS_LISTING_FIELD_PERMS = "perms";

    /**
     * The content creation account permission.
     */
    static final String ACCOUNT_PERM_CREATE_CONTENT = "CREATE_CONTENT";

    /**
     * The {@link String} to append to Page id to create graph path.
     */
    static final String PAGE_ID_TO_GRAPH_PATH = "/photos";

    //
    // Albums listing params.
    //

    /**
     * The graph path to list photo albums.
     */
    private static final String ALBUMS_LISTING_GRAPH_PATH = "me/albums";

    /**
     * The key for max number of albums to request.
     */
    private static final String ALBUMS_LISTING_LIMIT_KEY = "limit";

    /**
     * The value for max number of albums to request.
     */
    private static final String ALBUMS_LISTING_LIMIT_VALUE = "200";

    /**
     * The key for params to request.
     */
    private static final String ALBUMS_LISTING_FEILDS_KEY = "fields";

    /**
     * The value for params to request.
     */
    private static final String ALBUMS_LISTING_FIELDS_VALUE = "id,name,type,privacy,can_upload";

    //
    // Album listing results.
    //

    static final String ALBUMS_LISTING_RESULT_DATA_KEY = "data";

    static final String ALBUMS_LISTING_FIELD_ID = "id";

    static final String ALBUMS_LISTING_FIELD_NAME = "name";

    static final String ALBUMS_LISTING_FIELD_TYPE = "type";

    static final String ALBUMS_LISTING_FIELD_PRIVACY = "privacy";

    static final String ALBUMS_LISTING_FIELD_CAN_UPLOAD = "can_upload";

    /**
     * The {@link String} to append to album id to create graph path.
     */
    static final String ALBUM_ID_TO_GRAPH_PATH = "/photos";

    //
    // Default album params.
    //

    /**
     * The graph path of the app album.
     */
    static final String APP_ALBUM_GRAPH_PATH = "me/photos";

    /**
     * The privacy level of the Page album.
     */
    static final String PAGE_PRIVACY = "page";

    /**
     * The configurable privacy level of the app album.
     */
    static final String APP_ALBUM_PRIVACY = "select privacy level";

    /**
     * The type of the default album to share to.
     */
    static final String DEFAULT_ALBUM_TYPE = "app";

    //
    // Privacy levels for albums with 'custom' privacy level.
    //

    /**
     * Shared photos are visible to 'Only Me'.
     */
    static final String PHOTO_PRIVACY_SELF = "{'value':'SELF'}";

    /**
     * Shared photos are visible to 'Friends'.
     */
    static final String PHOTO_PRIVACY_FRIENDS = "{'value':'ALL_FRIENDS'}";

    /**
     * Shared photos are visible to 'Friends of Friends'.
     */
    static final String PHOTO_PRIVACY_FRIENDS_OF_FRIENDS = "{'value':'FRIENDS_OF_FRIENDS'}";

    /**
     * Shared photos are visible to 'Public'.
     */
    static final String PHOTO_PRIVACY_EVERYONE = "{'value':'EVERYONE'}";

    //
    // Share params.
    //

    private static final String SHARE_KEY_PICTURE = "picture";

    private static final String SHARE_KEY_PAGE_ACCESS_TOKEN = "access_token";

    private static final String SHARE_KEY_PHOTO_PRIVACY = "privacy";

    private static final String SHARE_KEY_PHOTO_ID = "id";

    private static final String SHARE_NOTIFICATION_INTENT_BASE_URI = "fb://photo/";

    /**
     * Flag to track if a link request is started.
     */
    private volatile int mLinkRequestState = STATE_NONE;

    //
    // Private methods.
    //

    /**
     * Opens a new session with read permissions.
     *
     * @param activity       the {@link Activity}.
     * @param fragment       the {@link Fragment}. May be null.
     * @param statusCallback callback when the {@link Session} state changes.
     */
    private void startOpenSessionRequest(Activity activity, Fragment fragment,
            Session.StatusCallback statusCallback) {
        // State transition.
        mLinkRequestState = STATE_OPEN_SESSION_REQUEST;

        // Construct new session.
        Session session = new Session(activity);
        Session.setActiveSession(session);

        // Construct read permissions to request for.
        List<String> readPermissions = new LinkedList<String>();
        readPermissions.add(PERMISSION_PUBLIC_PROFILE);
        readPermissions.add(PERMISSION_USER_PHOTOS);

        // Construct open request.
        OpenRequest openRequest;
        if (fragment == null) {
            openRequest = new OpenRequest(activity);
        } else {
            openRequest = new OpenRequest(fragment);
        }

        // Allow SSO login only because the web login does not allow PERMISSION_USER_PHOTOS to be bundled with the
        // first openForRead() call.
        openRequest.setLoginBehavior(SessionLoginBehavior.SSO_ONLY);
        openRequest.setPermissions(readPermissions);
        openRequest.setDefaultAudience(SessionDefaultAudience.EVERYONE);
        openRequest.setCallback(statusCallback);

        // Execute open request.
        session.openForRead(openRequest);
    }

    /**
     * Finishes a {@link com.groundupworks.wings.facebook.FacebookEndpoint#startOpenSessionRequest(android.app.Activity, android.support.v4.app.Fragment, com.facebook.Session.StatusCallback)}.
     *
     * @param activity    the {@link Activity}.
     * @param requestCode the integer request code originally supplied to startActivityForResult(), allowing you to identify who
     *                    this result came from.
     * @param resultCode  the integer result code returned by the child activity through its setResult().
     * @param data        an Intent, which can return result data to the caller (various data can be attached to Intent
     *                    "extras").
     * @return true if open session request is successful; false otherwise.
     */
    private boolean finishOpenSessionRequest(final Activity activity, int requestCode, int resultCode,
            Intent data) {
        boolean isSuccessful = false;

        Session session = Session.getActiveSession();

        // isOpened() must be called after onActivityResult().
        if (session != null && session.onActivityResult(activity, requestCode, resultCode, data)
                && session.isOpened() && session.getPermissions().contains(PERMISSION_USER_PHOTOS)) {
            isSuccessful = true;
        }

        return isSuccessful;
    }

    /**
     * Requests for permissions to publish publicly. Requires an opened active {@link Session}.
     *
     * @param activity the {@link Activity}.
     * @param fragment the {@link Fragment}. May be null.
     * @return true if the request is made; false if no opened {@link Session} is active.
     */
    private boolean startPublishPermissionsRequest(Activity activity, Fragment fragment) {
        boolean isSuccessful = false;

        // State transition.
        mLinkRequestState = STATE_PUBLISH_PERMISSIONS_REQUEST;

        Session session = Session.getActiveSession();
        if (session != null && session.isOpened()) {
            // Construct publish permissions.
            List<String> publishPermissions = new LinkedList<String>();
            publishPermissions.add(PERMISSION_PUBLISH_ACTIONS);
            publishPermissions.add(PERMISSION_MANAGE_PAGES);

            // Construct permissions request to publish publicly.
            NewPermissionsRequest permissionsRequest;
            if (fragment == null) {
                permissionsRequest = new NewPermissionsRequest(activity, publishPermissions);
            } else {
                if (fragment.getActivity() == null) {
                    return false;
                }
                permissionsRequest = new NewPermissionsRequest(fragment, publishPermissions);
            }
            permissionsRequest.setDefaultAudience(SessionDefaultAudience.EVERYONE);

            // Execute publish permissions request.
            session.requestNewPublishPermissions(permissionsRequest);
            isSuccessful = true;
        }
        return isSuccessful;
    }

    /**
     * Finishes a {@link com.groundupworks.wings.facebook.FacebookEndpoint#startPublishPermissionsRequest(android.app.Activity, android.support.v4.app.Fragment)}.
     *
     * @param activity    the {@link Activity}.
     * @param requestCode the integer request code originally supplied to startActivityForResult(), allowing you to identify who
     *                    this result came from.
     * @param resultCode  the integer result code returned by the child activity through its setResult().
     * @param data        an Intent, which can return result data to the caller (various data can be attached to Intent
     *                    "extras").
     * @return true if publish permissions request is successful; false otherwise.
     */
    private boolean finishPublishPermissionsRequest(Activity activity, int requestCode, int resultCode,
            Intent data) {
        boolean isSuccessful = false;

        Session session = Session.getActiveSession();

        // isOpened() must be called after onActivityResult().
        if (session != null && session.onActivityResult(activity, requestCode, resultCode, data)
                && session.isOpened() && session.getPermissions().contains(PERMISSION_PUBLISH_ACTIONS)) {
            isSuccessful = true;
        }

        return isSuccessful;
    }

    /**
     * Requests for permissions to publish publicly. Requires an opened active {@link Session}.
     *
     * @param activity the {@link Activity}.
     * @param fragment the {@link Fragment}. May be null.
     * @return true if the request is made; false if no opened {@link Session} is active.
     */
    private boolean startSettingsRequest(Activity activity, Fragment fragment) {
        boolean isSuccessful = false;

        // State transition.
        mLinkRequestState = STATE_SETTINGS_REQUEST;

        Session session = Session.getActiveSession();
        if (session != null && session.isOpened()) {
            // Start activity for result.
            Intent intent = new Intent(activity, FacebookSettingsActivity.class);
            if (fragment == null) {
                activity.startActivityForResult(intent, SETTINGS_REQUEST_CODE);
            } else {
                fragment.startActivityForResult(intent, SETTINGS_REQUEST_CODE);
            }

            isSuccessful = true;
        }
        return isSuccessful;
    }

    /**
     * Finishes a {@link com.groundupworks.wings.facebook.FacebookEndpoint#startSettingsRequest(android.app.Activity, android.support.v4.app.Fragment)}.
     *
     * @param requestCode the integer request code originally supplied to startActivityForResult(), allowing you to identify who
     *                    this result came from.
     * @param resultCode  the integer result code returned by the child activity through its setResult().
     * @param data        an Intent, which can return result data to the caller (various data can be attached to Intent
     *                    "extras").
     * @return the settings; or null if failed.
     */
    private FacebookSettings finishSettingsRequest(int requestCode, int resultCode, Intent data) {
        FacebookSettings settings = null;

        if (requestCode == SETTINGS_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) {
            // Construct settings from the extras bundle.
            settings = FacebookSettings.newInstance(data.getExtras());
        }
        return settings;
    }

    /**
     * Checks if the Facebook native app is installed on the device.
     *
     * @return true if installed; false otherwise.
     */
    private boolean isFacebookAppInstalled() {
        boolean isInstalled = false;
        try {
            // An exception will be thrown if the package is not found.
            mContext.getPackageManager().getApplicationInfo(FACEBOOK_APP_PACKAGE, 0);
            isInstalled = true;
        } catch (PackageManager.NameNotFoundException e) {
            // Do nothing.
        }
        return isInstalled;
    }

    /**
     * Links an account.
     *
     * @param settings the {@link com.groundupworks.wings.facebook.FacebookSettings}.
     * @return true if successful; false otherwise.
     */
    private boolean link(FacebookSettings settings) {
        boolean isSuccessful = false;

        // Validate account params and store.
        if (settings != null) {
            storeSettings(settings);

            // Emit link state change event.
            notifyLinkStateChanged(new LinkEvent(true));

            isSuccessful = true;
        }
        return isSuccessful;
    }

    /**
     * Handles an error case during the linking process.
     */
    private void handleLinkError() {
        // Reset link request state.
        mLinkRequestState = STATE_NONE;

        // Show toast to indicate error during linking.
        if (isFacebookAppInstalled()) {
            showLinkError();
        } else {
            showFacebookAppError();
        }

        // Unlink account to ensure proper reset.
        unlink();
    }

    /**
     * Displays the link error message.
     */
    private void showLinkError() {
        Toast.makeText(mContext, mContext.getString(R.string.wings_facebook__error_link), Toast.LENGTH_SHORT)
                .show();
    }

    /**
     * Displays the Facebook app error message.
     */
    private void showFacebookAppError() {
        Toast.makeText(mContext, mContext.getString(R.string.wings_facebook__error_facebook_app),
                Toast.LENGTH_SHORT).show();
    }

    /**
     * Stores the link settings in persisted storage.
     *
     * @param settings the {@link com.groundupworks.wings.facebook.FacebookSettings}.
     */
    private void storeSettings(FacebookSettings settings) {
        int destinationId = settings.getDestinationId();
        String accountName = settings.getAccountName();
        String albumName = settings.getAlbumName();
        String albumGraphPath = settings.getAlbumGraphPath();
        String pageAccessToken = settings.optPageAccessToken();
        String photoPrivacy = settings.optPhotoPrivacy();

        Editor editor = PreferenceManager.getDefaultSharedPreferences(mContext).edit();
        editor.putInt(mContext.getString(R.string.wings_facebook__destination_id_key), destinationId);
        editor.putString(mContext.getString(R.string.wings_facebook__account_name_key), accountName);
        editor.putString(mContext.getString(R.string.wings_facebook__album_name_key), albumName);
        editor.putString(mContext.getString(R.string.wings_facebook__album_graph_path_key), albumGraphPath);
        if (!TextUtils.isEmpty(pageAccessToken)) {
            editor.putString(mContext.getString(R.string.wings_facebook__page_access_token_key), pageAccessToken);
        }
        if (!TextUtils.isEmpty(photoPrivacy)) {
            editor.putString(mContext.getString(R.string.wings_facebook__photo_privacy_key), photoPrivacy);
        }

        // Set preference to linked.
        editor.putBoolean(mContext.getString(R.string.wings_facebook__link_key), true);
        editor.apply();
    }

    /**
     * Fetches the link settings from persisted storage.
     *
     * @return the {@link com.groundupworks.wings.facebook.FacebookSettings}; or null if unlinked.
     */
    private FacebookSettings fetchSettings() {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
        if (!preferences.getBoolean(mContext.getString(R.string.wings_facebook__link_key), false)) {
            return null;
        }

        int destinationId = preferences.getInt(mContext.getString(R.string.wings_facebook__destination_id_key),
                DestinationId.UNLINKED);
        String accountName = preferences.getString(mContext.getString(R.string.wings_facebook__account_name_key),
                null);
        String albumName = preferences.getString(mContext.getString(R.string.wings_facebook__album_name_key), null);
        String albumGraphPath = preferences
                .getString(mContext.getString(R.string.wings_facebook__album_graph_path_key), null);
        String pageAccessToken = preferences
                .getString(mContext.getString(R.string.wings_facebook__page_access_token_key), null);
        String photoPrivacy = preferences.getString(mContext.getString(R.string.wings_facebook__photo_privacy_key),
                null);

        return FacebookSettings.newInstance(destinationId, accountName, albumName, albumGraphPath, pageAccessToken,
                photoPrivacy);
    }

    /**
     * Removes the link settings from persisted storage.
     */
    private void removeSettings() {
        Editor editor = PreferenceManager.getDefaultSharedPreferences(mContext).edit();
        editor.remove(mContext.getString(R.string.wings_facebook__destination_id_key));
        editor.remove(mContext.getString(R.string.wings_facebook__account_name_key));
        editor.remove(mContext.getString(R.string.wings_facebook__album_name_key));
        editor.remove(mContext.getString(R.string.wings_facebook__album_graph_path_key));
        editor.remove(mContext.getString(R.string.wings_facebook__page_access_token_key));
        editor.remove(mContext.getString(R.string.wings_facebook__photo_privacy_key));

        // Set preference to unlinked.
        editor.putBoolean(mContext.getString(R.string.wings_facebook__link_key), false);
        editor.apply();
    }

    /**
     * Parses the photo id from a {@link GraphObject}.
     *
     * @param graphObject the {@link GraphObject} to parse.
     * @return the photo id; or null if not found.
     */
    private String parsePhotoId(GraphObject graphObject) {
        String photoId = null;

        if (graphObject != null) {
            JSONObject jsonObject = graphObject.getInnerJSONObject();
            if (jsonObject != null) {
                photoId = jsonObject.optString(SHARE_KEY_PHOTO_ID, null);
            }
        }
        return photoId;
    }

    //
    // Package private methods.
    //

    /**
     * Asynchronously requests the user name associated with the linked account. Requires an opened active
     * {@link Session}.
     *
     * @param graphUserCallback a {@link GraphUserCallback} when the request completes.
     * @return true if the request is made; false if no opened {@link Session} is active.
     */
    boolean requestAccountName(GraphUserCallback graphUserCallback) {
        boolean isSuccessful = false;

        Session session = Session.getActiveSession();
        if (session != null && session.isOpened()) {
            Request.newMeRequest(session, graphUserCallback).executeAsync();
            isSuccessful = true;
        }
        return isSuccessful;
    }

    /**
     * Asynchronously requests the Page accounts associated with the linked account. Requires an opened active {@link Session}.
     *
     * @param callback a {@link Callback} when the request completes.
     * @return true if the request is made; false if no opened {@link Session} is active.
     */
    boolean requestAccounts(Callback callback) {
        boolean isSuccessful = false;

        Session session = Session.getActiveSession();
        if (session != null && session.isOpened()) {
            // Construct fields to request.
            Bundle params = new Bundle();
            params.putString(ACCOUNTS_LISTING_FEILDS_KEY, ACCOUNTS_LISTING_FIELDS_VALUE);

            // Construct and execute albums listing request.
            Request request = new Request(session, ACCOUNTS_LISTING_GRAPH_PATH, params, HttpMethod.GET, callback);
            request.executeAsync();

            isSuccessful = true;
        }
        return isSuccessful;
    }

    /**
     * Asynchronously requests the albums associated with the linked account. Requires an opened active {@link Session}.
     *
     * @param callback a {@link Callback} when the request completes.
     * @return true if the request is made; false if no opened {@link Session} is active.
     */
    boolean requestAlbums(Callback callback) {
        boolean isSuccessful = false;

        Session session = Session.getActiveSession();
        if (session != null && session.isOpened()) {
            // Construct fields to request.
            Bundle params = new Bundle();
            params.putString(ALBUMS_LISTING_LIMIT_KEY, ALBUMS_LISTING_LIMIT_VALUE);
            params.putString(ALBUMS_LISTING_FEILDS_KEY, ALBUMS_LISTING_FIELDS_VALUE);

            // Construct and execute albums listing request.
            Request request = new Request(session, ALBUMS_LISTING_GRAPH_PATH, params, HttpMethod.GET, callback);
            request.executeAsync();

            isSuccessful = true;
        }
        return isSuccessful;
    }

    //
    // Public methods.
    //

    @Override
    public int getEndpointId() {
        return ENDPOINT_ID;
    }

    @Override
    public void startLinkRequest(final Activity activity, final Fragment fragment) {
        // Construct status callback.
        Session.StatusCallback statusCallback = new Session.StatusCallback() {
            @Override
            public void call(Session session, SessionState state, Exception exception) {
                if (mLinkRequestState == STATE_OPEN_SESSION_REQUEST && state.isOpened()) {
                    // Request publish permissions.
                    if (!startPublishPermissionsRequest(activity, fragment)) {
                        handleLinkError();
                    }
                }
            }
        };

        // Open session.
        startOpenSessionRequest(activity, fragment, statusCallback);
    }

    @Override
    public void unlink() {
        // Unlink in persisted storage.
        removeSettings();

        // Unlink any current session.
        Session session = Session.getActiveSession();
        if (session != null && !session.isClosed()) {
            session.closeAndClearTokenInformation();
        }

        // Emit link state change event.
        notifyLinkStateChanged(new LinkEvent(false));

        // Remove existing share requests in a background thread.
        mHandler.post(new Runnable() {

            @Override
            public void run() {
                mDatabase.deleteShareRequests(new Destination(DestinationId.PROFILE, ENDPOINT_ID));
                mDatabase.deleteShareRequests(new Destination(DestinationId.PAGE, ENDPOINT_ID));
            }
        });
    }

    @Override
    public boolean isLinked() {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
        return preferences.getBoolean(mContext.getString(R.string.wings_facebook__link_key), false);
    }

    @Override
    public void onResumeImpl() {
        // Do nothing.
    }

    @Override
    public void onActivityResultImpl(Activity activity, Fragment fragment, int requestCode, int resultCode,
            Intent data) {
        // State machine to handle the linking process.
        switch (mLinkRequestState) {
        case STATE_OPEN_SESSION_REQUEST: {
            // Only handle the error case. If successful, the publish permissions request will be started by a
            // session callback.
            if (!finishOpenSessionRequest(activity, requestCode, resultCode, data)) {
                handleLinkError();
            }
            break;
        }
        case STATE_PUBLISH_PERMISSIONS_REQUEST: {
            if (finishPublishPermissionsRequest(activity, requestCode, resultCode, data)) {
                // Start request for settings.
                if (!startSettingsRequest(activity, fragment)) {
                    handleLinkError();
                }
            } else {
                handleLinkError();
            }
            break;
        }
        case STATE_SETTINGS_REQUEST: {
            // Link account.
            FacebookSettings settings = finishSettingsRequest(requestCode, resultCode, data);
            if (link(settings)) {
                // End link request, but persist link tokens.
                Session session = Session.getActiveSession();
                if (session != null && !session.isClosed()) {
                    session.close();
                }
                mLinkRequestState = STATE_NONE;
            } else {
                handleLinkError();
            }
            break;
        }
        default: {
        }
        }
    }

    @Override
    public LinkInfo getLinkInfo() {
        FacebookSettings settings = fetchSettings();
        if (settings != null) {
            int destinationId = settings.getDestinationId();
            String accountName = settings.getAccountName();

            int resId = R.string.wings_facebook__destination_profile_description;
            if (DestinationId.PAGE == destinationId) {
                resId = R.string.wings_facebook__destination_page_description;
            }
            String destinationDescription = mContext.getString(resId, accountName, settings.getAlbumName());

            return new LinkInfo(accountName, destinationId, destinationDescription);
        }
        return null;
    }

    @Override
    public Set<ShareNotification> processShareRequests() {
        Set<ShareNotification> notifications = new HashSet<ShareNotification>();

        // Get params associated with the linked account.
        FacebookSettings settings = fetchSettings();
        if (settings != null) {
            // Get share requests for Facebook.
            int destinationId = settings.getDestinationId();
            Destination destination = new Destination(destinationId, ENDPOINT_ID);
            List<ShareRequest> shareRequests = mDatabase.checkoutShareRequests(destination);
            int shared = 0;
            String intentUri = null;

            if (!shareRequests.isEmpty()) {
                // Try open session with cached access token.
                Session session = Session.openActiveSessionFromCache(mContext);
                if (session != null && session.isOpened()) {
                    // Process share requests.
                    for (ShareRequest shareRequest : shareRequests) {
                        File file = new File(shareRequest.getFilePath());
                        ParcelFileDescriptor fileDescriptor = null;
                        try {
                            // Construct graph params.
                            fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
                            Bundle params = new Bundle();
                            params.putParcelable(SHARE_KEY_PICTURE, fileDescriptor);

                            String pageAccessToken = settings.optPageAccessToken();
                            if (DestinationId.PAGE == destinationId && !TextUtils.isEmpty(pageAccessToken)) {
                                params.putString(SHARE_KEY_PAGE_ACCESS_TOKEN, pageAccessToken);
                            }

                            String photoPrivacy = settings.optPhotoPrivacy();
                            if (!TextUtils.isEmpty(photoPrivacy)) {
                                params.putString(SHARE_KEY_PHOTO_PRIVACY, photoPrivacy);
                            }

                            // Execute upload request synchronously. Need to use RequestBatch to set connection timeout.
                            Request request = new Request(session, settings.getAlbumGraphPath(), params,
                                    HttpMethod.POST, null);
                            RequestBatch requestBatch = new RequestBatch(request);
                            requestBatch.setTimeout(HTTP_REQUEST_TIMEOUT);
                            List<Response> responses = requestBatch.executeAndWait();
                            if (responses != null && !responses.isEmpty()) {
                                // Process response.
                                Response response = responses.get(0);
                                if (response != null) {
                                    FacebookRequestError error = response.getError();
                                    if (error == null) {
                                        // Mark as successfully processed.
                                        mDatabase.markSuccessful(shareRequest.getId());

                                        // Parse photo id to construct notification intent uri.
                                        if (intentUri == null) {
                                            String photoId = parsePhotoId(response.getGraphObject());
                                            if (photoId != null && photoId.length() > 0) {
                                                intentUri = SHARE_NOTIFICATION_INTENT_BASE_URI + photoId;
                                            }
                                        }

                                        shared++;
                                    } else {
                                        mDatabase.markFailed(shareRequest.getId());

                                        Category category = error.getCategory();
                                        if (Category.AUTHENTICATION_RETRY.equals(category)
                                                || Category.PERMISSION.equals(category)) {
                                            // Update account linking state to unlinked.
                                            unlink();
                                        }
                                    }
                                } else {
                                    mDatabase.markFailed(shareRequest.getId());
                                }
                            } else {
                                mDatabase.markFailed(shareRequest.getId());
                            }
                        } catch (FacebookException e) {
                            mDatabase.markFailed(shareRequest.getId());
                        } catch (IllegalArgumentException e) {
                            mDatabase.markFailed(shareRequest.getId());
                        } catch (FileNotFoundException e) {
                            mDatabase.markFailed(shareRequest.getId());
                        } catch (Exception e) {
                            // Safety.
                            mDatabase.markFailed(shareRequest.getId());
                        } finally {
                            if (fileDescriptor != null) {
                                try {
                                    fileDescriptor.close();
                                } catch (IOException e) {
                                    // Do nothing.
                                }
                            }
                        }
                    }
                } else {
                    // Mark all share requests as failed to process since we failed to open an active session.
                    for (ShareRequest shareRequest : shareRequests) {
                        mDatabase.markFailed(shareRequest.getId());
                    }
                }
            }

            // Construct and add notification representing share results.
            if (shared > 0) {
                notifications.add(new FacebookShareNotification(mContext, destination.getHash(),
                        settings.getAlbumName(), shared, intentUri));
            }
        }

        return notifications;
    }

    @Override
    @Produce
    public LinkEvent produceLinkEvent() {
        return new LinkEvent(isLinked());
    }

    //
    // Public interfaces and classes.
    //

    /**
     * The list of destination ids.
     */
    public interface DestinationId extends WingsEndpoint.DestinationId {

        /**
         * The personal Facebook profile.
         */
        public static final int PROFILE = 0;

        /**
         * A Facebook Page.
         */
        public static final int PAGE = 1;
    }

    /**
     * The link event implementation associated with this endpoint.
     */
    public class LinkEvent extends WingsEndpoint.LinkEvent {

        /**
         * Private constructor.
         *
         * @param isLinked true if current link state for this endpoint is linked; false otherwise.
         */
        private LinkEvent(boolean isLinked) {
            super(FacebookEndpoint.class, isLinked);
        }
    }
}