facebook.FacebookModule.java Source code

Java tutorial

Introduction

Here is the source code for facebook.FacebookModule.java

Source

/**
 * Facebook Module
 * Copyright (c) 2009-2013 by Appcelerator, Inc. All Rights Reserved.
 * Please see the LICENSE included with this distribution for details.
 */

package facebook;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Date;

import org.appcelerator.kroll.KrollDict;
import org.appcelerator.kroll.KrollFunction;
import org.appcelerator.kroll.KrollModule;
import org.appcelerator.kroll.annotations.Kroll;
import org.appcelerator.kroll.common.CurrentActivityListener;
import org.appcelerator.kroll.common.Log;
import org.appcelerator.kroll.common.TiMessenger;
import org.appcelerator.titanium.TiApplication;
import org.appcelerator.titanium.TiBlob;
import org.appcelerator.titanium.TiContext;
import org.appcelerator.titanium.io.TiBaseFile;
import org.appcelerator.titanium.util.TiActivityResultHandler;
import org.appcelerator.titanium.util.TiActivitySupport;
import org.appcelerator.titanium.util.TiConvert;
import org.appcelerator.titanium.util.TiUIHelper;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

import com.facebook.android.AsyncFacebookRunner;
import com.facebook.android.AsyncFacebookRunner.RequestListener;
import com.facebook.android.DialogError;
import com.facebook.android.Facebook;
import com.facebook.android.Facebook.DialogListener;
import com.facebook.android.FacebookError;
import com.facebook.android.Util;
import com.facebook.internal.Utility;
import com.facebook.Settings;

@Kroll.module(name = "Facebook", id = "facebook")
public class FacebookModule extends KrollModule {
    protected static final String TAG = "FacebookModule";

    @Kroll.constant
    public static final int BUTTON_STYLE_NORMAL = 0;
    @Kroll.constant
    public static final int BUTTON_STYLE_WIDE = 1;

    public static final String EVENT_LOGIN = "login";
    public static final String EVENT_LOGOUT = "logout";
    public static final String PROPERTY_SUCCESS = "success";
    public static final String PROPERTY_CANCELLED = "cancelled";
    public static final String PROPERTY_ERROR = "error";
    public static final String PROPERTY_CODE = "code";
    public static final String PROPERTY_DATA = "data";
    public static final String PROPERTY_UID = "uid";
    public static final String PROPERTY_RESULT = "result";
    public static final String PROPERTY_PATH = "path";
    public static final String PROPERTY_METHOD = "method";

    protected Facebook facebook = null;
    protected String uid = null;
    protected WeakReference<Context> loginContext = null; // Facebook authorize and logout should use same context.

    private boolean loggedIn = false;
    private ArrayList<TiFacebookStateListener> stateListeners = new ArrayList<TiFacebookStateListener>();
    private SessionListener sessionListener = null;
    private AsyncFacebookRunner fbrunner;
    private String appid = null;
    private String[] permissions = new String[] {};
    private boolean forceDialogAuth = true;

    public FacebookModule() {
        super();
        Utility.setLogEnabled(Log.isDebugModeEnabled());
        Utility.loadResourceIds(TiApplication.getInstance());
        sessionListener = new SessionListener(this);
        SessionEvents.addAuthListener(sessionListener);
        SessionEvents.addLogoutListener(sessionListener);
        debug("FacebookModule()");
        appid = SessionStore.getSavedAppId(TiApplication.getInstance());
        if (appid != null) {
            debug("Attempting session restore for appid " + appid);
            facebook = new Facebook(appid);
            SessionStore.restore(this, TiApplication.getInstance());
            if (facebook.isSessionValid()) {
                debug("Session restore succeeded.  Now logged in.");
                loggedIn = true;
            } else {
                debug("Session restore failed.  Not logged in.");
                loggedIn = false;
            }
        }
    }

    public FacebookModule(TiContext tiContext) {
        this();
    }

    @Kroll.getProperty
    @Kroll.method
    public boolean getLoggedIn() {
        return isLoggedIn();
    }

    @Kroll.getProperty
    @Kroll.method
    public String getAccessToken() {
        if (facebook != null) {
            return facebook.getAccessToken();
        } else {
            return null;
        }
    }

    @Kroll.getProperty
    @Kroll.method
    public String getAppid() {
        return appid;
    }

    @Kroll.setProperty
    @Kroll.method
    public void setAppid(String appid) {
        if (this.appid != null && !this.appid.equals(appid)) {
            if (facebook != null && facebook.isSessionValid()) {
                // A facebook session existed, but the appid was changed.  Any session info
                // should be destroyed.
                Log.w(TAG, "Appid was changed while session active.  Removing session info.");
                destroyFacebookSession();
                facebook = null;
            }
        }
        this.appid = appid;
        if (facebook == null || !facebook.getAppId().equals(appid)) {
            facebook = new Facebook(appid);
        }
    }

    @Kroll.getProperty
    @Kroll.method
    public String getUid() {
        return uid;
    }

    @Kroll.getProperty
    @Kroll.method
    public String[] getPermissions() {
        return permissions;
    }

    @Kroll.setProperty
    @Kroll.method
    public void setPermissions(String[] permissions) {
        this.permissions = permissions;
    }

    @Kroll.getProperty
    @Kroll.method
    public Date getExpirationDate() {
        if (facebook != null) {
            return TiConvert.toDate(facebook.getAccessExpires());
        } else {
            return new Date(0);
        }
    }

    @Kroll.getProperty
    @Kroll.method
    public boolean getForceDialogAuth() {
        return forceDialogAuth;
    }

    @Kroll.setProperty
    @Kroll.method
    public void setForceDialogAuth(boolean value) {
        this.forceDialogAuth = value;
    }

    @Kroll.method
    public TiFacebookModuleLoginButtonProxy createLoginButton(@Kroll.argument(optional = true) KrollDict options) {
        TiFacebookModuleLoginButtonProxy login = new TiFacebookModuleLoginButtonProxy(this);
        if (options != null) {
            login.extend(options);
        }
        return login;
    }

    @Kroll.method
    public void authorize() {
        debug("authorize; permissions.length == " + permissions.length);
        if (this.isLoggedIn()) {
            // if already authorized, this should do nothing
            debug("Already logged in, ignoring authorize() request");
            return;
        }

        if (appid == null) {
            Log.w(TAG, "authorize() called without appid being set; throwing...");
            throw new IllegalStateException("missing appid");
        }

        // forget session in case this fails.
        SessionStore.clear(TiApplication.getInstance());

        if (facebook == null) {
            facebook = new Facebook(appid);
        }

        // Important to be done on the current activity since it will display dialog.
        TiUIHelper.waitForCurrentActivity(new CurrentActivityListener() {
            @Override
            public void onCurrentActivityReady(Activity activity) {
                executeAuthorize(activity);
            }
        });
    }

    @Kroll.method
    public void logout() {
        boolean wasLoggedIn = isLoggedIn();
        destroyFacebookSession();
        if (facebook != null && wasLoggedIn) {
            SessionEvents.onLogoutBegin();
            executeLogout();
        } else {
            loginContext = null;
        }
    }

    @Kroll.method
    public void requestWithGraphPath(String path, KrollDict params, String httpMethod, KrollFunction callback) {
        if (facebook == null) {
            Log.w(TAG, "requestWithGraphPath called without Facebook being instantiated.  Have you set appid?");
            return;
        }
        AsyncFacebookRunner runner = getFBRunner();
        Bundle paramBundle = Utils.mapToBundle(params);
        if (httpMethod == null || httpMethod.length() == 0) {
            httpMethod = "GET";
        }
        runner.request(path, paramBundle, httpMethod.toUpperCase(),
                new TiRequestListener(this, path, true, callback), null);
    }

    @Kroll.method
    public void request(String method, KrollDict params, KrollFunction callback) {
        if (facebook == null) {
            Log.w(TAG, "request called without Facebook being instantiated.  Have you set appid?");
            return;
        }

        String httpMethod = "GET";
        if (params != null) {
            for (Object v : params.values()) {
                if (v instanceof TiBlob || v instanceof TiBaseFile) {
                    httpMethod = "POST";
                    break;
                }
            }
        }

        Bundle bundle = Utils.mapToBundle(params);
        if (!bundle.containsKey("method")) {
            bundle.putString("method", method);
        }
        getFBRunner().request(null, bundle, httpMethod, new TiRequestListener(this, method, false, callback), null);
    }

    @Kroll.method
    public void dialog(final String action, final KrollDict params, final KrollFunction callback) {
        if (facebook == null) {
            Log.w(TAG, "dialog called without Facebook being instantiated.  Have you set appid?");
            return;
        }

        TiUIHelper.waitForCurrentActivity(new CurrentActivityListener() {
            @Override
            public void onCurrentActivityReady(Activity activity) {
                final Activity fActivity = activity;
                fActivity.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        facebook.dialog(fActivity, action, Utils.mapToBundle(params),
                                new TiDialogListener(FacebookModule.this, callback, action));
                    }
                });
            }
        });
    }

    @Kroll.method
    public void publishInstall() {
        if (appid == null) {
            Log.w(TAG, "Trying publishInstall without appid.  Have you set appid?");
            return;
        }

        Context context = TiApplication.getInstance().getApplicationContext();
        Settings.publishInstallAsync(context, appid);
    }

    protected void completeLogin() {
        getFBRunner().request("me", new RequestListener() {
            @Override
            public void onMalformedURLException(MalformedURLException e, Object state) {
                loginError(e);
            }

            @Override
            public void onIOException(IOException e, Object state) {
                loginError(e);
            }

            @Override
            public void onFileNotFoundException(FileNotFoundException e, Object state) {
                loginError(e);
            }

            @Override
            public void onFacebookError(FacebookError e, Object state) {
                loginError(e);
            }

            @Override
            public void onComplete(String response, Object state) {
                try {
                    debug("onComplete (getting 'me'): " + response);
                    JSONObject json = Util.parseJson(response);
                    uid = json.getString("id");
                    loggedIn = true;
                    SessionStore.save(FacebookModule.this, TiApplication.getInstance());
                    KrollDict data = new KrollDict();
                    data.put(PROPERTY_CANCELLED, false);
                    data.put(PROPERTY_SUCCESS, true);
                    data.put(PROPERTY_UID, uid);
                    data.put(PROPERTY_DATA, response);
                    data.put(PROPERTY_CODE, 0);
                    fireLoginChange();
                    fireEvent(EVENT_LOGIN, data);
                } catch (JSONException e) {
                    Log.e(TAG, e.getMessage(), e);
                } catch (FacebookError e) {
                    Log.e(TAG, e.getMessage(), e);
                }
            }
        });
    }

    protected void completeLogout() {
        destroyFacebookSession();
        loginContext = null;
        fireLoginChange();
        fireEvent(EVENT_LOGOUT, new KrollDict());
    }

    protected void debug(String message) {
        Log.d(TAG, message, Log.DEBUG_MODE);
    }

    protected void addListener(TiFacebookStateListener listener) {
        if (!stateListeners.contains(listener)) {
            stateListeners.add(listener);
        }
    }

    protected void removeListener(TiFacebookStateListener listener) {
        stateListeners.remove(listener);
    }

    protected void executeAuthorize(final Activity activity) {
        loginContext = new WeakReference<Context>(activity);
        final TiActivitySupport activitySupport = (TiActivitySupport) activity;
        int activityCode;
        if (forceDialogAuth) {
            // Single sign-on support
            activityCode = Facebook.FORCE_DIALOG_AUTH;
        } else {
            activityCode = activitySupport.getUniqueResultCode();
        }
        final TiActivityResultHandler resultHandler = new TiActivityResultHandler() {
            @Override
            public void onResult(Activity activity, int requestCode, int resultCode, Intent data) {
                Log.d(TAG, "onResult from Facebook login attempt. resultCode: " + resultCode, Log.DEBUG_MODE);
                facebook.authorizeCallback(requestCode, resultCode, data);
            }

            @Override
            public void onError(Activity activity, int requestCode, Exception e) {
                Log.e(TAG, e.getLocalizedMessage(), e);
            }
        };

        if (TiApplication.isUIThread()) {
            facebook.authorize(activity, activitySupport, permissions, activityCode, new LoginDialogListener(),
                    resultHandler);
        } else {
            final int code = activityCode;
            TiMessenger.postOnMain(new Runnable() {
                @Override
                public void run() {
                    facebook.authorize(activity, activitySupport, permissions, code, new LoginDialogListener(),
                            resultHandler);
                }
            });
        }
    }

    protected void executeLogout() {
        Context logoutContext = null;

        // Try to use the same context as was used for login.
        if (loginContext != null) {
            logoutContext = loginContext.get();
        }

        if (logoutContext == null) {
            // Fallback by using the application context.  The reason facebook.authorize and facebook.logout
            // want a Context is because they use the CookieSyncManager, which needs a Context.
            // The CookieSyncManager anyway takes that Context and calls context.getApplicationContext()
            // for its use.
            logoutContext = TiApplication.getInstance().getApplicationContext();
        }

        getFBRunner().logout(logoutContext, new LogoutRequestListener());
    }

    private boolean isLoggedIn() {
        return loggedIn && facebook != null && facebook.isSessionValid();
    }

    private void loginError(Throwable t) {
        Log.e(TAG, t.getMessage(), t);
        loggedIn = false;
        KrollDict data = new KrollDict();
        data.put(PROPERTY_CANCELLED, false);
        data.put(PROPERTY_SUCCESS, false);
        data.put(PROPERTY_ERROR, t.getMessage());
        data.put(PROPERTY_CODE, -1);
        fireEvent(EVENT_LOGIN, data);
    }

    private void loginCancel() {
        debug("login canceled");
        loggedIn = false;
        KrollDict data = new KrollDict();
        data.put(PROPERTY_CANCELLED, true);
        data.put(PROPERTY_SUCCESS, false);
        data.put(PROPERTY_CODE, -1);
        fireEvent(EVENT_LOGIN, data);
    }

    private AsyncFacebookRunner getFBRunner() {
        if (fbrunner == null) {
            fbrunner = new AsyncFacebookRunner(facebook);
        }
        return fbrunner;
    }

    private void destroyFacebookSession() {
        SessionStore.clear(TiApplication.getInstance());
        uid = null;
        loggedIn = false;
    }

    private void fireLoginChange() {
        for (TiFacebookStateListener listener : stateListeners) {
            if (getLoggedIn()) {
                listener.login();
            } else {
                listener.logout();
            }
        }
    }

    @Override
    public void onDestroy(Activity activity) {
        super.onDestroy(activity);
        if (sessionListener != null) {
            SessionEvents.removeAuthListener(sessionListener);
            SessionEvents.removeLogoutListener(sessionListener);
            sessionListener = null;
        }
    }

    private final class LoginDialogListener implements DialogListener {
        public void onComplete(Bundle values) {
            debug("LoginDialogListener onComplete");
            SessionEvents.onLoginSuccess();
        }

        public void onFacebookError(FacebookError error) {
            String errorMessage = error.getMessage();
            // There is a bug in Facebook Android SDK 3.0. When the user cancels the login by pressing the
            // "X" button, the onFacebookError callback is called instead of onCancel.
            // http://stackoverflow.com/questions/14237157/facebookoperationcanceledexception-not-called
            if (errorMessage != null && errorMessage.indexOf("User canceled log in") > -1) {
                FacebookModule.this.loginCancel();
            } else {
                Log.e(TAG, "LoginDialogListener onFacebookError: " + error.getMessage(), error);
                SessionEvents.onLoginError(error.getMessage());
            }
            loginContext = null;
        }

        public void onError(DialogError error) {
            Log.e(TAG, "LoginDialogListener onError: " + error.getMessage(), error);
            loginContext = null;
            SessionEvents.onLoginError(error.getMessage());
        }

        public void onCancel() {
            loginContext = null;
            FacebookModule.this.loginCancel();
        }
    }

    private final class LogoutRequestListener implements RequestListener {
        @Override
        public void onComplete(String response, Object state) {
            debug("Logout request complete: " + response);
            SessionEvents.onLogoutFinish();
        }

        @Override
        public void onFacebookError(FacebookError e, Object state) {
            Log.e(TAG, "Logout failure: " + e.getMessage(), e);
        }

        @Override
        public void onFileNotFoundException(FileNotFoundException e, Object state) {
            Log.e(TAG, "Logout failure: " + e.getMessage(), e);
        }

        @Override
        public void onIOException(IOException e, Object state) {
            Log.e(TAG, "Logout failure: " + e.getMessage(), e);
        }

        @Override
        public void onMalformedURLException(MalformedURLException e, Object state) {
            Log.e(TAG, "Logout failure: " + e.getMessage(), e);
        }
    }

}