Java tutorial
/** * Copyright 2010-present Facebook. 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.TagFu.facebook; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import android.app.Activity; import android.app.Fragment; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ResolveInfo; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import android.util.Log; import org.json.JSONException; import org.json.JSONObject; import com.wTagFufacebook.internal.NativeProtocol; import com.woTagFuacebook.internal.SessionAuthorizationType; import com.wooTagFucebook.internal.Utility; import com.wootTagFuebook.internal.Validate; import com.wootaTagFubook.model.GraphMultiResult; import com.wootagTagFuook.model.GraphObject; import com.wootag.facebook.model.GraphObjectList; /** * <p> * Session is used to authenticate a user and manage the user's session with Facebook. * </p> * <p> * Sessions must be opened before they can be used to make a Request. When a Session is created, it attempts to * initialize itself from a TokenCachingStrategy. Closing the session can optionally clear this cache. The Session * lifecycle uses {@link SessionState SessionState} to indicate its state. Once a Session has been closed, it can't be * re-opened; a new Session must be created. * </p> * <p> * Instances of Session provide state change notification via a callback interface, {@link Session.StatusCallback * StatusCallback}. * </p> */ public class Session implements Serializable { private static final String RSVP_EVENT = "rsvp_event"; private static final String CREATE_EVENT = "create_event"; private static final String ADS_MANAGEMENT = "ads_management"; private static final long serialVersionUID = 1L; /** * The logging tag used by Session. */ public static final String TAG = Session.class.getCanonicalName(); /** * The default activity code used for authorization. * * @see #openForRead(OpenRequest) open */ public static final int DEFAULT_AUTHORIZE_ACTIVITY_CODE = 0xface; /** * If Session authorization fails and provides a web view error code, the web view error code is stored in the * Bundle returned from {@link #getAuthorizationBundle getAuthorizationBundle} under this key. */ public static final String WEB_VIEW_ERROR_CODE_KEY = "com.facebook.sdk.WebViewErrorCode"; /** * If Session authorization fails and provides a failing url, the failing url is stored in the Bundle returned from * {@link #getAuthorizationBundle getAuthorizationBundle} under this key. */ public static final String WEB_VIEW_FAILING_URL_KEY = "com.facebook.sdk.FailingUrl"; /** * The action used to indicate that the active session has been set. This should be used as an action in an * IntentFilter and BroadcastReceiver registered with the {@link android.support.v4.content.LocalBroadcastManager}. */ public static final String ACTION_ACTIVE_SESSION_SET = "com.facebook.sdk.ACTIVE_SESSION_SET"; /** * The action used to indicate that the active session has been set to null. This should be used as an action in an * IntentFilter and BroadcastReceiver registered with the {@link android.support.v4.content.LocalBroadcastManager}. */ public static final String ACTION_ACTIVE_SESSION_UNSET = "com.facebook.sdk.ACTIVE_SESSION_UNSET"; /** * The action used to indicate that the active session has been opened. This should be used as an action in an * IntentFilter and BroadcastReceiver registered with the {@link android.support.v4.content.LocalBroadcastManager}. */ public static final String ACTION_ACTIVE_SESSION_OPENED = "com.facebook.sdk.ACTIVE_SESSION_OPENED"; /** * The action used to indicate that the active session has been closed. This should be used as an action in an * IntentFilter and BroadcastReceiver registered with the {@link android.support.v4.content.LocalBroadcastManager}. */ public static final String ACTION_ACTIVE_SESSION_CLOSED = "com.facebook.sdk.ACTIVE_SESSION_CLOSED"; private static final Object STATIC_LOCK = new Object(); private static Session activeSession; protected static volatile Context staticContext; // Token extension constants private static final int TOKEN_EXTEND_THRESHOLD_SECONDS = 24 * 60 * 60; // 1 // day private static final int TOKEN_EXTEND_RETRY_SECONDS = 60 * 60; // 1 hour private static final String SESSION_BUNDLE_SAVE_KEY = "com.facebook.sdk.Session.saveSessionKey"; private static final String AUTH_BUNDLE_SAVE_KEY = "com.facebook.sdk.Session.authBundleKey"; private static final String PUBLISH_PERMISSION_PREFIX = "publish"; private static final String MANAGE_PERMISSION_PREFIX = "manage"; private static final String BASIC_INFO_PERMISSION = "basic_info"; @SuppressWarnings("serial") private static final Set<String> OTHER_PUBLISH_PERMISSIONS = new HashSet<String>() { { this.add(ADS_MANAGEMENT); this.add(CREATE_EVENT); this.add(RSVP_EVENT); } }; private final String applicationId; protected SessionState state; AccessToken tokenInfo; private Date lastAttemptedTokenExtendDate = new Date(0); private AuthorizationRequest pendingAuthorizationRequest; private AuthorizationClient authorizationClient; // The following are not serialized with the Session object private volatile Bundle authorizationBundle; protected final List<StatusCallback> callbacks; protected final Handler handler; protected AutoPublishAsyncTask autoPublishAsyncTask; // This is the object that synchronizes access to state and tokenInfo protected final Object lock = new Object(); private TokenCachingStrategy tokenCachingStrategy; protected volatile TokenRefreshRequest currentTokenRefreshRequest; private AppEventsLogger appEventsLogger; /** * Initializes a new Session with the specified context. * * @param currentContext The Activity or Service creating this Session. */ public Session(final Context currentContext) { this(currentContext, null, null, true); } /** * Used by version 2 of the serialization proxy, do not modify. */ private Session(final String applicationId, final SessionState state, final AccessToken tokenInfo, final Date lastAttemptedTokenExtendDate, final boolean shouldAutoPublish, final AuthorizationRequest pendingAuthorizationRequest, final Set<String> requestedPermissions) { this.applicationId = applicationId; this.state = state; this.tokenInfo = tokenInfo; this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate; this.pendingAuthorizationRequest = pendingAuthorizationRequest; this.handler = new Handler(Looper.getMainLooper()); this.currentTokenRefreshRequest = null; this.tokenCachingStrategy = null; this.callbacks = new ArrayList<StatusCallback>(); } Session(final Context context, final String applicationId, final TokenCachingStrategy tokenCachingStrategy) { this(context, applicationId, tokenCachingStrategy, true); } Session(final Context context, String applicationId, TokenCachingStrategy tokenCachingStrategy, final boolean loadTokenFromCache) { // if the application ID passed in is null, try to get it from the // meta-data in the manifest. if ((context != null) && (applicationId == null)) { applicationId = Utility.getMetadataApplicationId(context); } Validate.notNull(applicationId, "applicationId"); initializeStaticContext(context); if (tokenCachingStrategy == null) { tokenCachingStrategy = new SharedPreferencesTokenCachingStrategy(staticContext); } this.applicationId = applicationId; this.tokenCachingStrategy = tokenCachingStrategy; this.state = SessionState.CREATED; this.pendingAuthorizationRequest = null; this.callbacks = new ArrayList<StatusCallback>(); this.handler = new Handler(Looper.getMainLooper()); final Bundle tokenState = loadTokenFromCache ? tokenCachingStrategy.load() : null; if (TokenCachingStrategy.hasTokenInformation(tokenState)) { final Date cachedExpirationDate = TokenCachingStrategy.getDate(tokenState, TokenCachingStrategy.EXPIRATION_DATE_KEY); final Date now = new Date(); if ((cachedExpirationDate == null) || cachedExpirationDate.before(now)) { // If expired or we require new permissions, clear out the // current token cache. tokenCachingStrategy.clear(); this.tokenInfo = AccessToken.createEmptyToken(); } else { // Otherwise we have a valid token, so use it. this.tokenInfo = AccessToken.createFromCache(tokenState); this.state = SessionState.CREATED_TOKEN_LOADED; } } else { this.tokenInfo = AccessToken.createEmptyToken(); } } /** * Used by version 1 of the serialization proxy, do not modify. */ Session(final String applicationId, final SessionState state, final AccessToken tokenInfo, final Date lastAttemptedTokenExtendDate, final boolean shouldAutoPublish, final AuthorizationRequest pendingAuthorizationRequest) { this.applicationId = applicationId; this.state = state; this.tokenInfo = tokenInfo; this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate; this.pendingAuthorizationRequest = pendingAuthorizationRequest; this.handler = new Handler(Looper.getMainLooper()); this.currentTokenRefreshRequest = null; this.tokenCachingStrategy = null; this.callbacks = new ArrayList<StatusCallback>(); } /** * Returns the current active Session, or null if there is none. * * @return the current active Session, or null if there is none. */ public static final Session getActiveSession() { synchronized (Session.STATIC_LOCK) { return Session.activeSession; } } public static boolean isPublishPermission(final String permission) { return (permission != null) && (permission.startsWith(PUBLISH_PERMISSION_PREFIX) || permission.startsWith(MANAGE_PERMISSION_PREFIX) || OTHER_PUBLISH_PERMISSIONS.contains(permission)); } /** * If allowLoginUI is true, this will create a new Session, make it active, and open it. If the default token cache * is not available, then this will request the permissions provided (and basic permissions of no permissions are * provided). If the default token cache is available and cached tokens are loaded, this will use the cached token * and associated permissions. * <p/> * If allowedLoginUI is false, this will only create the active session and open it if it requires no user * interaction (i.e. the token cache is available and there are cached tokens). * * @param activity The Activity that is opening the new Session. * @param allowLoginUI if false, only sets the active session and opens it if it does not require user interaction * @param permissions The permissions to request for this Session * @param callback The {@link StatusCallback SessionStatusCallback} to notify regarding Session state changes. May * be null. * @return The new Session or null if one could not be created */ public static Session openActiveSession(final Activity activity, final boolean allowLoginUI, final List<String> permissions, final StatusCallback callback) { return openActiveSession(activity, allowLoginUI, new OpenRequest(activity).setCallback(callback).setPermissions(permissions)); } /** * If allowLoginUI is true, this will create a new Session, make it active, and open it. If the default token cache * is not available, then this will request basic permissions. If the default token cache is available and cached * tokens are loaded, this will use the cached token and associated permissions. * <p/> * If allowedLoginUI is false, this will only create the active session and open it if it requires no user * interaction (i.e. the token cache is available and there are cached tokens). * * @param activity The Activity that is opening the new Session. * @param allowLoginUI if false, only sets the active session and opens it if it does not require user interaction * @param callback The {@link StatusCallback SessionStatusCallback} to notify regarding Session state changes. May * be null. * @return The new Session or null if one could not be created */ public static Session openActiveSession(final Activity activity, final boolean allowLoginUI, final StatusCallback callback) { return openActiveSession(activity, allowLoginUI, new OpenRequest(activity).setCallback(callback)); } /** * If allowLoginUI is true, this will create a new Session, make it active, and open it. If the default token cache * is not available, then this will request the permissions provided (and basic permissions of no permissions are * provided). If the default token cache is available and cached tokens are loaded, this will use the cached token * and associated permissions. * <p/> * If allowedLoginUI is false, this will only create the active session and open it if it requires no user * interaction (i.e. the token cache is available and there are cached tokens). * * @param context The Activity or Service creating this Session * @param fragment The Fragment that is opening the new Session. * @param allowLoginUI if false, only sets the active session and opens it if it does not require user interaction * @param permissions The permissions to request for this Session * @param callback The {@link StatusCallback SessionStatusCallback} to notify regarding Session state changes. * @return The new Session or null if one could not be created */ public static Session openActiveSession(final Context context, final Fragment fragment, final boolean allowLoginUI, final List<String> permissions, final StatusCallback callback) { return openActiveSession(context, allowLoginUI, new OpenRequest(fragment).setCallback(callback).setPermissions(permissions)); } /** * If allowLoginUI is true, this will create a new Session, make it active, and open it. If the default token cache * is not available, then this will request basic permissions. If the default token cache is available and cached * tokens are loaded, this will use the cached token and associated permissions. * <p/> * If allowedLoginUI is false, this will only create the active session and open it if it requires no user * interaction (i.e. the token cache is available and there are cached tokens). * * @param context The Activity or Service creating this Session * @param fragment The Fragment that is opening the new Session. * @param allowLoginUI if false, only sets the active session and opens it if it does not require user interaction * @param callback The {@link StatusCallback SessionStatusCallback} to notify regarding Session state changes. * @return The new Session or null if one could not be created */ public static Session openActiveSession(final Context context, final Fragment fragment, final boolean allowLoginUI, final StatusCallback callback) { return openActiveSession(context, allowLoginUI, new OpenRequest(fragment).setCallback(callback)); } /** * If a cached token is available, creates and opens the session and makes it active without any user interaction, * otherwise this does nothing. * * @param context The Context creating this session * @return The new session or null if one could not be created */ public static Session openActiveSessionFromCache(final Context context) { return openActiveSession(context, false, null); } /** * Opens a session based on an existing Facebook access token, and also makes this session the currently active * session. This method should be used only in instances where an application has previously obtained an access * token and wishes to import it into the Session/TokenCachingStrategy-based session-management system. A primary * example would be an application which previously did not use the Facebook SDK for Android and implemented its own * session-management scheme, but wishes to implement an upgrade path for existing users so they do not need to log * in again when upgrading to a version of the app that uses the SDK. In general, this method will be called only * once, when the app detects that it has been upgraded -- after that, the usual Session lifecycle methods should be * used to manage the session and its associated token. * <p/> * No validation is done that the token, token source, or permissions are actually valid. It is the caller's * responsibility to ensure that these accurately reflect the state of the token that has been passed in, or calls * to the Facebook API may fail. * * @param context the Context to use for creation the session * @param accessToken the access token obtained from Facebook * @param callback a callback that will be called when the session status changes; may be null * @return The new Session or null if one could not be created */ public static Session openActiveSessionWithAccessToken(final Context context, final AccessToken accessToken, final StatusCallback callback) { final Session session = new Session(context, null, null, false); setActiveSession(session); session.open(accessToken, callback); return session; } /** * Restores the saved session from a Bundle, if any. Returns the restored Session or null if it could not be * restored. This method is intended to be called from an Activity or Fragment's onCreate method when a Session has * previously been saved into a Bundle via saveState to preserve a Session across Activity lifecycle events. * * @param context the Activity or Service creating the Session, must not be null * @param cachingStrategy the TokenCachingStrategy to use to load and store the token. If this is null, a default * token cachingStrategy that stores data in SharedPreferences will be used * @param callback the callback to notify for Session state changes, can be null * @param bundle the bundle to restore the Session from * @return the restored Session, or null */ public static final Session restoreSession(final Context context, final TokenCachingStrategy cachingStrategy, final StatusCallback callback, final Bundle bundle) { if (bundle == null) { return null; } final byte[] data = bundle.getByteArray(SESSION_BUNDLE_SAVE_KEY); if (data != null) { final ByteArrayInputStream is = new ByteArrayInputStream(data); try { final Session session = (Session) (new ObjectInputStream(is)).readObject(); initializeStaticContext(context); if (cachingStrategy != null) { session.tokenCachingStrategy = cachingStrategy; } else { session.tokenCachingStrategy = new SharedPreferencesTokenCachingStrategy(context); } if (callback != null) { session.addCallback(callback); } session.authorizationBundle = bundle.getBundle(AUTH_BUNDLE_SAVE_KEY); return session; } catch (final ClassNotFoundException e) { Log.w(TAG, "Unable to restore session", e); } catch (final IOException e) { Log.w(TAG, "Unable to restore session.", e); } } return null; } /** * Save the Session object into the supplied Bundle. This method is intended to be called from an Activity or * Fragment's onSaveInstanceState method in order to preserve Sessions across Activity lifecycle events. * * @param session the Session to save * @param bundle the Bundle to save the Session to */ public static final void saveSession(final Session session, final Bundle bundle) { if ((bundle != null) && (session != null) && !bundle.containsKey(SESSION_BUNDLE_SAVE_KEY)) { final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { new ObjectOutputStream(outputStream).writeObject(session); } catch (final IOException e) { throw new FacebookException("Unable to save session.", e); } bundle.putByteArray(SESSION_BUNDLE_SAVE_KEY, outputStream.toByteArray()); bundle.putBundle(AUTH_BUNDLE_SAVE_KEY, session.authorizationBundle); } } /** * <p> * Sets the current active Session. * </p> * <p> * The active Session is used implicitly by predefined Request factory methods as well as optionally by UI controls * in the sdk. * </p> * <p> * It is legal to set this to null, or to a Session that is not yet open. * </p> * * @param session A Session to use as the active Session, or null to indicate that there is no active Session. */ public static final void setActiveSession(final Session session) { synchronized (Session.STATIC_LOCK) { if (session != Session.activeSession) { final Session oldSession = Session.activeSession; if (oldSession != null) { oldSession.close(); } Session.activeSession = session; if (oldSession != null) { postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_UNSET); } if (session != null) { postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_SET); if (session.isOpened()) { postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_OPENED); } } } } } private static boolean areEqual(final Object a, final Object b) { if (a == null) { return b == null; } return a.equals(b); } private static Session openActiveSession(final Context context, final boolean allowLoginUI, final OpenRequest openRequest) { final Session session = new Builder(context).build(); if (SessionState.CREATED_TOKEN_LOADED.equals(session.getState()) || allowLoginUI) { setActiveSession(session); session.openForRead(openRequest); return session; } return null; } static Context getStaticContext() { return staticContext; } /** * This parses a server response to a call to me/permissions. It will return the list of granted permissions. It * will optionally update a session with the requested permissions. It also handles the distinction between 1.0 and * 2.0 calls to the endpoint. * * @param response The server response * @return A list of granted permissions or null if an error */ static PermissionsPair handlePermissionResponse(final Response response) { if (response.getError() != null) { return null; } final GraphMultiResult result = response.getGraphObjectAs(GraphMultiResult.class); if (result == null) { return null; } final GraphObjectList<GraphObject> data = result.getData(); if ((data == null) || (data.size() == 0)) { return null; } final List<String> grantedPermissions = new ArrayList<String>(data.size()); final List<String> declinedPermissions = new ArrayList<String>(data.size()); // Check if we are dealing with v2.0 or v1.0 behavior until the server is updated final GraphObject firstObject = data.get(0); if (firstObject.getProperty("permission") != null) { // v2.0 for (final GraphObject graphObject : data) { final String permission = (String) graphObject.getProperty("permission"); if (permission.equals("installed")) { continue; } final String status = (String) graphObject.getProperty("status"); if (status.equals("granted")) { grantedPermissions.add(permission); } else if (status.equals("declined")) { declinedPermissions.add(permission); } } } else { // v1.0 final Map<String, Object> permissionsMap = firstObject.asMap(); for (final Map.Entry<String, Object> entry : permissionsMap.entrySet()) { if (entry.getKey().equals("installed")) { continue; } if ((Integer) entry.getValue() == 1) { grantedPermissions.add(entry.getKey()); } } } return new PermissionsPair(grantedPermissions, declinedPermissions); } static void initializeStaticContext(final Context currentContext) { if ((currentContext != null) && (staticContext == null)) { final Context applicationContext = currentContext.getApplicationContext(); staticContext = (applicationContext != null) ? applicationContext : currentContext; } } static void postActiveSessionAction(final String action) { final Intent intent = new Intent(action); LocalBroadcastManager.getInstance(getStaticContext()).sendBroadcast(intent); } static void runWithHandlerOrExecutor(final Handler handler, final Runnable runnable) { if (handler != null) { handler.post(runnable); } else { Settings.getExecutor().execute(runnable); } } /** * Adds a callback that will be called when the state of this Session changes. * * @param callback the callback */ public final void addCallback(final StatusCallback callback) { synchronized (this.callbacks) { if ((callback != null) && !this.callbacks.contains(callback)) { this.callbacks.add(callback); } } } /** * Closes the local in-memory Session object, but does not clear the persisted token cache. */ public final void close() { synchronized (this.lock) { final SessionState oldState = this.state; switch (this.state) { case CREATED: case OPENING: this.state = SessionState.CLOSED_LOGIN_FAILED; this.postStateChange(oldState, this.state, new FacebookException("Log in attempt aborted.")); break; case CREATED_TOKEN_LOADED: case OPENED: case OPENED_TOKEN_UPDATED: this.state = SessionState.CLOSED; this.postStateChange(oldState, this.state, null); break; case CLOSED: case CLOSED_LOGIN_FAILED: break; default: break; } } } /** * Closes the local in-memory Session object and clears any persisted token cache related to the Session. */ public final void closeAndClearTokenInformation() { if (this.tokenCachingStrategy != null) { this.tokenCachingStrategy.clear(); } Utility.clearFacebookCookies(staticContext); Utility.clearCaches(staticContext); this.close(); } @Override public boolean equals(final Object otherObj) { if (!(otherObj instanceof Session)) { return false; } final Session other = (Session) otherObj; return areEqual(other.applicationId, this.applicationId) && areEqual(other.authorizationBundle, this.authorizationBundle) && areEqual(other.state, this.state) && areEqual(other.getExpirationDate(), this.getExpirationDate()); } /** * Returns the access token String. * * @return the access token String, or null if there is no access token */ public final String getAccessToken() { synchronized (this.lock) { return (this.tokenInfo == null) ? null : this.tokenInfo.getToken(); } } /** * Returns the application id associated with this Session. * * @return the application id associated with this Session. */ public final String getApplicationId() { return this.applicationId; } /** * Returns a Bundle containing data that was returned from Facebook during authorization. * * @return a Bundle containing data that was returned from Facebook during authorization. */ public final Bundle getAuthorizationBundle() { synchronized (this.lock) { return this.authorizationBundle; } } /** * <p> * Returns the list of permissions that have been requested in this session but not granted * </p> * * @return the list of requested permissions that have been declined */ public final List<String> getDeclinedPermissions() { synchronized (this.lock) { return (this.tokenInfo == null) ? null : this.tokenInfo.getDeclinedPermissions(); } } /** * <p> * Returns the Date at which the current token will expire. * </p> * <p> * Note that Session automatically attempts to extend the lifetime of Tokens as needed when Facebook requests are * made. * </p> * * @return the Date at which the current token will expire, or null if there is no access token */ public final Date getExpirationDate() { synchronized (this.lock) { return (this.tokenInfo == null) ? null : this.tokenInfo.getExpires(); } } /** * <p> * Returns the list of permissions associated with the session. * </p> * <p> * If there is a valid token, this represents the permissions granted by that token. This can change during calls to * {@link #requestNewReadPermissions} or {@link #requestNewPublishPermissions}. * </p> * * @return the list of permissions associated with the session, or null if there is no access token */ public final List<String> getPermissions() { synchronized (this.lock) { return (this.tokenInfo == null) ? null : this.tokenInfo.getPermissions(); } } /** * Returns the current state of the Session. See {@link SessionState} for details. * * @return the current state of the Session. */ public final SessionState getState() { synchronized (this.lock) { return this.state; } } @Override public int hashCode() { return 0; } public final boolean isClosed() { synchronized (this.lock) { return this.state.isClosed(); } } /** * Returns a boolean indicating whether the session is opened. * * @return a boolean indicating whether the session is opened. */ public final boolean isOpened() { synchronized (this.lock) { return this.state.isOpened(); } } /** * <p> * Returns whether a particular permission has been granted * </p> * * @param permission The permission to check for * @return true if the permission is granted, false otherwise */ public boolean isPermissionGranted(final String permission) { final List<String> grantedPermissions = this.getPermissions(); if (grantedPermissions != null) { return grantedPermissions.contains(permission); } return false; } /** * Provides an implementation for {@link Activity#onActivityResult onActivityResult} that updates the Session based * on information returned during the authorization flow. The Activity that calls open or requestNewPermissions * should forward the resulting onActivityResult call here to update the Session state based on the contents of the * resultCode and data. * * @param currentActivity The Activity that is forwarding the onActivityResult call. * @param requestCode The requestCode parameter from the forwarded call. When this onActivityResult occurs as part * of Facebook authorization flow, this value is the activityCode passed to open or authorize. * @param resultCode An int containing the resultCode parameter from the forwarded call. * @param data The Intent passed as the data parameter from the forwarded call. * @return A boolean indicating whether the requestCode matched a pending authorization request for this Session. */ public final boolean onActivityResult(final Activity currentActivity, final int requestCode, final int resultCode, final Intent data) { Validate.notNull(currentActivity, "currentActivity"); initializeStaticContext(currentActivity); synchronized (this.lock) { if ((this.pendingAuthorizationRequest == null) || (requestCode != this.pendingAuthorizationRequest.getRequestCode())) { return false; } } Exception exception = null; AuthorizationClient.Result.Code code = AuthorizationClient.Result.Code.ERROR; if (data != null) { final AuthorizationClient.Result result = (AuthorizationClient.Result) data .getSerializableExtra(LoginActivity.RESULT_KEY); if (result != null) { // This came from LoginActivity. this.handleAuthorizationResult(resultCode, result); return true; } else if (this.authorizationClient != null) { // Delegate to the auth client. this.authorizationClient.onActivityResult(requestCode, resultCode, data); return true; } } else if (resultCode == Activity.RESULT_CANCELED) { exception = new FacebookOperationCanceledException("User canceled operation."); code = AuthorizationClient.Result.Code.CANCEL; } if (exception == null) { exception = new FacebookException("Unexpected call to Session.onActivityResult"); } this.logAuthorizationComplete(code, null, exception); this.finishAuthOrReauth(null, exception); return true; } /** * Opens a session based on an existing Facebook access token. This method should be used only in instances where an * application has previously obtained an access token and wishes to import it into the * Session/TokenCachingStrategy-based session-management system. An example would be an application which previously * did not use the Facebook SDK for Android and implemented its own session-management scheme, but wishes to * implement an upgrade path for existing users so they do not need to log in again when upgrading to a version of * the app that uses the SDK. * <p/> * No validation is done that the token, token source, or permissions are actually valid. It is the caller's * responsibility to ensure that these accurately reflect the state of the token that has been passed in, or calls * to the Facebook API may fail. * * @param accessToken the access token obtained from Facebook * @param callback a callback that will be called when the session status changes; may be null */ public final void open(final AccessToken accessToken, final StatusCallback callback) { synchronized (this.lock) { if (this.pendingAuthorizationRequest != null) { throw new UnsupportedOperationException( "Session: an attempt was made to open a session that has a pending request."); } if (this.state.isClosed()) { throw new UnsupportedOperationException( "Session: an attempt was made to open a previously-closed session."); } else if ((this.state != SessionState.CREATED) && (this.state != SessionState.CREATED_TOKEN_LOADED)) { throw new UnsupportedOperationException( "Session: an attempt was made to open an already opened session."); } if (callback != null) { this.addCallback(callback); } this.tokenInfo = accessToken; if (this.tokenCachingStrategy != null) { this.tokenCachingStrategy.save(accessToken.toCacheBundle()); } final SessionState oldState = this.state; this.state = SessionState.OPENED; this.postStateChange(oldState, this.state, null); } this.autoPublishAsync(); } /** * <p> * Logs a user in to Facebook. * </p> * <p> * A session may not be used with {@link Request Request} and other classes in the SDK until it is open. If, prior * to calling open, the session is in the {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} state, and * the requested permissions are a subset of the previously authorized permissions, then the Session becomes usable * immediately with no user interaction. * </p> * <p> * The permissions associated with the openRequest passed to this method must be publish or manage permissions only * and must be non-empty. Any read permissions will result in a warning, and may fail during server-side * authorization. Also, an application must have at least basic read permissions prior to requesting publish * permissions, so this method should only be used if the application knows that the user has already granted read * permissions to the application; otherwise, openForRead should be used, followed by a call to * requestNewPublishPermissions. For more information on this flow, see * https://developers.facebook.com/docs/facebook-login/permissions/. * </p> * <p> * Any open method must be called at most once, and cannot be called after the Session is closed. Calling the method * at an invalid time will result in UnsuportedOperationException. * </p> * * @param openRequest the open request, can be null only if the Session is in the * {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} state * @throws FacebookException if the passed in request is null or has no permissions set. */ public final void openForPublish(final OpenRequest openRequest) { this.open(openRequest, SessionAuthorizationType.PUBLISH); } /** * <p> * Logs a user in to Facebook. * </p> * <p> * A session may not be used with {@link Request Request} and other classes in the SDK until it is open. If, prior * to calling open, the session is in the {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} state, and * the requested permissions are a subset of the previously authorized permissions, then the Session becomes usable * immediately with no user interaction. * </p> * <p> * The permissions associated with the openRequest passed to this method must be read permissions only (or * null/empty). It is not allowed to pass publish permissions to this method and will result in an exception being * thrown. * </p> * <p> * Any open method must be called at most once, and cannot be called after the Session is closed. Calling the method * at an invalid time will result in UnsuportedOperationException. * </p> * * @param openRequest the open request, can be null only if the Session is in the * {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} state * @throws FacebookException if any publish or manage permissions are requested */ public final void openForRead(final OpenRequest openRequest) { this.open(openRequest, SessionAuthorizationType.READ); } /** * <p> * Issues a request to refresh the permissions on the session. * </p> * <p> * If successful, this will update the permissions and call the app back with * {@link SessionState#OPENED_TOKEN_UPDATED}. The session can then be queried to see the granted and declined * permissions. If this fails because the user has removed the app, the session will close. * </p> */ public final void refreshPermissions() { final Request request = new Request(this, "me/permissions"); request.setCallback(new Request.Callback() { @Override public void onCompleted(final Response response) { final PermissionsPair permissionsPair = handlePermissionResponse(response); if (permissionsPair != null) { // Update our token with the refreshed permissions synchronized (Session.this.lock) { Session.this.tokenInfo = AccessToken.createFromTokenWithRefreshedPermissions( Session.this.tokenInfo, permissionsPair.getGrantedPermissions(), permissionsPair.getDeclinedPermissions()); Session.this.postStateChange(Session.this.state, SessionState.OPENED_TOKEN_UPDATED, null); } } } }); request.executeAsync(); } /** * Removes a StatusCallback from this Session. * * @param callback the callback */ public final void removeCallback(final StatusCallback callback) { synchronized (this.callbacks) { this.callbacks.remove(callback); } } /** * <p> * Issues a request to add new publish or manage permissions to the Session. * </p> * <p> * If successful, this will update the set of permissions on this session to match the newPermissions. If this * fails, the Session remains unchanged. * </p> * <p> * The permissions associated with the newPermissionsRequest passed to this method must be publish or manage * permissions only and must be non-empty. Any read permissions will result in a warning, and may fail during * server-side authorization. * </p> * * @param newPermissionsRequest the new permissions request */ public final void requestNewPublishPermissions(final NewPermissionsRequest newPermissionsRequest) { this.requestNewPermissions(newPermissionsRequest, SessionAuthorizationType.PUBLISH); } /** * <p> * Issues a request to add new read permissions to the Session. * </p> * <p> * If successful, this will update the set of permissions on this session to match the newPermissions. If this * fails, the Session remains unchanged. * </p> * <p> * The permissions associated with the newPermissionsRequest passed to this method must be read permissions only (or * null/empty). It is not allowed to pass publish permissions to this method and will result in an exception being * thrown. * </p> * * @param newPermissionsRequest the new permissions request */ public final void requestNewReadPermissions(final NewPermissionsRequest newPermissionsRequest) { this.requestNewPermissions(newPermissionsRequest, SessionAuthorizationType.READ); } @Override public String toString() { return new StringBuilder().append("{Session").append(" state:").append(this.state).append(", token:") .append((this.tokenInfo == null) ? "null" : this.tokenInfo).append(", appId:") .append((this.applicationId == null) ? "null" : this.applicationId).append("}").toString(); } @SuppressWarnings("deprecation") private void autoPublishAsync() { AutoPublishAsyncTask asyncTask = null; synchronized (this) { if ((this.autoPublishAsyncTask == null) && Settings.getShouldAutoPublishInstall()) { // copy the application id to guarantee thread safety against our container. final String applicationId = Session.this.applicationId; // skip publish if we don't have an application id. if (applicationId != null) { asyncTask = this.autoPublishAsyncTask = new AutoPublishAsyncTask(applicationId, staticContext); } } } if (asyncTask != null) { asyncTask.execute(); } } private void finishAuthorization(final AccessToken newToken, final Exception exception) { final SessionState oldState = this.state; if (newToken != null) { this.tokenInfo = newToken; this.saveTokenToCache(newToken); this.state = SessionState.OPENED; } else if (exception != null) { this.state = SessionState.CLOSED_LOGIN_FAILED; } this.pendingAuthorizationRequest = null; this.postStateChange(oldState, this.state, exception); } private void finishReauthorization(final AccessToken newToken, final Exception exception) { final SessionState oldState = this.state; if (newToken != null) { this.tokenInfo = newToken; this.saveTokenToCache(newToken); this.state = SessionState.OPENED_TOKEN_UPDATED; } this.pendingAuthorizationRequest = null; this.postStateChange(oldState, this.state, exception); } private AppEventsLogger getAppEventsLogger() { synchronized (this.lock) { if (this.appEventsLogger == null) { this.appEventsLogger = AppEventsLogger.newLogger(staticContext, this.applicationId); } return this.appEventsLogger; } } private Intent getLoginActivityIntent(final AuthorizationRequest request) { final Intent intent = new Intent(); intent.setClass(getStaticContext(), LoginActivity.class); intent.setAction(request.getLoginBehavior().toString()); // Let LoginActivity populate extras appropriately final AuthorizationClient.AuthorizationRequest authClientRequest = request.getAuthorizationClientRequest(); final Bundle extras = LoginActivity.populateIntentExtras(authClientRequest); intent.putExtras(extras); return intent; } private void logAuthorizationComplete(final AuthorizationClient.Result.Code result, final Map<String, String> resultExtras, final Exception exception) { Bundle bundle = null; if (this.pendingAuthorizationRequest == null) { // We don't expect this to happen, but if it does, log an event for diagnostic purposes. bundle = AuthorizationClient.newAuthorizationLoggingBundle(""); bundle.putString(AuthorizationClient.EVENT_PARAM_LOGIN_RESULT, AuthorizationClient.Result.Code.ERROR.getLoggingValue()); bundle.putString(AuthorizationClient.EVENT_PARAM_ERROR_MESSAGE, "Unexpected call to logAuthorizationComplete with null pendingAuthorizationRequest."); } else { bundle = AuthorizationClient .newAuthorizationLoggingBundle(this.pendingAuthorizationRequest.getAuthId()); if (result != null) { bundle.putString(AuthorizationClient.EVENT_PARAM_LOGIN_RESULT, result.getLoggingValue()); } if ((exception != null) && (exception.getMessage() != null)) { bundle.putString(AuthorizationClient.EVENT_PARAM_ERROR_MESSAGE, exception.getMessage()); } // Combine extras from the request and from the result. JSONObject jsonObject = null; if (this.pendingAuthorizationRequest.loggingExtras.isEmpty() == false) { jsonObject = new JSONObject(this.pendingAuthorizationRequest.loggingExtras); } if (resultExtras != null) { if (jsonObject == null) { jsonObject = new JSONObject(); } try { for (final Map.Entry<String, String> entry : resultExtras.entrySet()) { jsonObject.put(entry.getKey(), entry.getValue()); } } catch (final JSONException e) { } } if (jsonObject != null) { bundle.putString(AuthorizationClient.EVENT_PARAM_EXTRAS, jsonObject.toString()); } } bundle.putLong(AuthorizationClient.EVENT_PARAM_TIMESTAMP, System.currentTimeMillis()); final AppEventsLogger logger = this.getAppEventsLogger(); logger.logSdkEvent(AuthorizationClient.EVENT_NAME_LOGIN_COMPLETE, null, bundle); } private void logAuthorizationStart() { final Bundle bundle = AuthorizationClient .newAuthorizationLoggingBundle(this.pendingAuthorizationRequest.getAuthId()); bundle.putLong(AuthorizationClient.EVENT_PARAM_TIMESTAMP, System.currentTimeMillis()); // Log what we already know about the call in start event try { final JSONObject extras = new JSONObject(); extras.put(AuthorizationClient.EVENT_EXTRAS_LOGIN_BEHAVIOR, this.pendingAuthorizationRequest.loginBehavior.toString()); extras.put(AuthorizationClient.EVENT_EXTRAS_REQUEST_CODE, this.pendingAuthorizationRequest.requestCode); extras.put(AuthorizationClient.EVENT_EXTRAS_IS_LEGACY, this.pendingAuthorizationRequest.legacy); extras.put(AuthorizationClient.EVENT_EXTRAS_PERMISSIONS, TextUtils.join(",", this.pendingAuthorizationRequest.permissions)); extras.put(AuthorizationClient.EVENT_EXTRAS_DEFAULT_AUDIENCE, this.pendingAuthorizationRequest.defaultAudience.toString()); bundle.putString(AuthorizationClient.EVENT_PARAM_EXTRAS, extras.toString()); } catch (final JSONException e) { } final AppEventsLogger logger = this.getAppEventsLogger(); logger.logSdkEvent(AuthorizationClient.EVENT_NAME_LOGIN_START, null, bundle); } private void open(final OpenRequest openRequest, final SessionAuthorizationType authType) { this.validatePermissions(openRequest, authType); this.validateLoginBehavior(openRequest); SessionState newState; synchronized (this.lock) { if (this.pendingAuthorizationRequest != null) { this.postStateChange(this.state, this.state, new UnsupportedOperationException( "Session: an attempt was made to open a session that has a pending request.")); return; } final SessionState oldState = this.state; switch (this.state) { case CREATED: this.state = newState = SessionState.OPENING; if (openRequest == null) { throw new IllegalArgumentException("openRequest cannot be null when opening a new Session"); } this.pendingAuthorizationRequest = openRequest; break; case CREATED_TOKEN_LOADED: if ((openRequest != null) && !Utility.isNullOrEmpty(openRequest.getPermissions())) { if (!Utility.isSubset(openRequest.getPermissions(), this.getPermissions())) { this.pendingAuthorizationRequest = openRequest; } } if (this.pendingAuthorizationRequest == null) { this.state = newState = SessionState.OPENED; } else { this.state = newState = SessionState.OPENING; } break; default: throw new UnsupportedOperationException( "Session: an attempt was made to open an already opened session."); } if (openRequest != null) { this.addCallback(openRequest.getCallback()); } this.postStateChange(oldState, newState, null); } if (newState == SessionState.OPENING) { this.authorize(openRequest); } } // have a readObject that throws to prevent spoofing private void readObject(final ObjectInputStream stream) throws InvalidObjectException { throw new InvalidObjectException("Cannot readObject, serialization proxy required"); } private void requestNewPermissions(final NewPermissionsRequest newPermissionsRequest, final SessionAuthorizationType authType) { this.validatePermissions(newPermissionsRequest, authType); this.validateLoginBehavior(newPermissionsRequest); if (newPermissionsRequest != null) { synchronized (this.lock) { if (this.pendingAuthorizationRequest != null) { throw new UnsupportedOperationException( "Session: an attempt was made to request new permissions for a session that has a pending request."); } if (this.state.isOpened()) { this.pendingAuthorizationRequest = newPermissionsRequest; } else if (this.state.isClosed()) { throw new UnsupportedOperationException( "Session: an attempt was made to request new permissions for a session that has been closed."); } else { throw new UnsupportedOperationException( "Session: an attempt was made to request new permissions for a session that is not currently open."); } } newPermissionsRequest.setValidateSameFbidAsToken(this.getAccessToken()); this.addCallback(newPermissionsRequest.getCallback()); this.authorize(newPermissionsRequest); } } private boolean resolveIntent(final Intent intent) { final ResolveInfo resolveInfo = getStaticContext().getPackageManager().resolveActivity(intent, 0); if (resolveInfo == null) { return false; } return true; } private void saveTokenToCache(final AccessToken newToken) { if ((newToken != null) && (this.tokenCachingStrategy != null)) { this.tokenCachingStrategy.save(newToken.toCacheBundle()); } } private void tryLegacyAuth(final AuthorizationRequest request) { this.authorizationClient = new AuthorizationClient(); this.authorizationClient.setOnCompletedListener(new AuthorizationClient.OnCompletedListener() { @Override public void onCompleted(final AuthorizationClient.Result result) { int activityResult; if (result.code == AuthorizationClient.Result.Code.CANCEL) { activityResult = Activity.RESULT_CANCELED; } else { activityResult = Activity.RESULT_OK; } Session.this.handleAuthorizationResult(activityResult, result); } }); this.authorizationClient.setContext(getStaticContext()); this.authorizationClient.startOrContinueAuth(request.getAuthorizationClientRequest()); } private boolean tryLoginActivity(final AuthorizationRequest request) { final Intent intent = this.getLoginActivityIntent(request); if (!this.resolveIntent(intent)) { return false; } try { request.getStartActivityDelegate().startActivityForResult(intent, request.getRequestCode()); } catch (final ActivityNotFoundException e) { return false; } return true; } private void validateLoginBehavior(final AuthorizationRequest request) { if ((request != null) && !request.legacy) { final Intent intent = new Intent(); intent.setClass(getStaticContext(), LoginActivity.class); if (!this.resolveIntent(intent)) { throw new FacebookException(String.format( "Cannot use SessionLoginBehavior %s when %s is not declared as an activity in AndroidManifest.xml", request.getLoginBehavior(), LoginActivity.class.getName())); } } } private void validatePermissions(final AuthorizationRequest request, final SessionAuthorizationType authType) { if ((request == null) || Utility.isNullOrEmpty(request.getPermissions())) { if (SessionAuthorizationType.PUBLISH.equals(authType)) { throw new FacebookException("Cannot request publish or manage authorization with no permissions."); } return; // nothing to check } for (final String permission : request.getPermissions()) { if (isPublishPermission(permission)) { if (SessionAuthorizationType.READ.equals(authType)) { throw new FacebookException(String.format( "Cannot pass a publish or manage permission (%s) to a request for read authorization", permission)); } } else { if (SessionAuthorizationType.PUBLISH.equals(authType)) { Log.w(TAG, String.format( "Should not pass a read permission (%s) to a request for publish or manage authorization", permission)); } } } } private Object writeReplace() { return new SerializationProxyV1(this.applicationId, this.state, this.tokenInfo, this.lastAttemptedTokenExtendDate, false, this.pendingAuthorizationRequest); } void authorize(final AuthorizationRequest request) { boolean started = false; request.setApplicationId(this.applicationId); this.autoPublishAsync(); this.logAuthorizationStart(); started = this.tryLoginActivity(request); this.pendingAuthorizationRequest.loggingExtras.put(AuthorizationClient.EVENT_EXTRAS_TRY_LOGIN_ACTIVITY, started ? AppEventsConstants.EVENT_PARAM_VALUE_YES : AppEventsConstants.EVENT_PARAM_VALUE_NO); if (!started && request.legacy) { this.pendingAuthorizationRequest.loggingExtras.put(AuthorizationClient.EVENT_EXTRAS_TRY_LEGACY, AppEventsConstants.EVENT_PARAM_VALUE_YES); this.tryLegacyAuth(request); started = true; } if (!started) { synchronized (this.lock) { final SessionState oldState = this.state; switch (this.state) { case CLOSED: case CLOSED_LOGIN_FAILED: return; default: this.state = SessionState.CLOSED_LOGIN_FAILED; final Exception exception = new FacebookException( "Log in attempt failed: LoginActivity could not be started, and not legacy request"); this.logAuthorizationComplete(AuthorizationClient.Result.Code.ERROR, null, exception); this.postStateChange(oldState, this.state, exception); } } } } void extendAccessToken() { TokenRefreshRequest newTokenRefreshRequest = null; synchronized (this.lock) { if (this.currentTokenRefreshRequest == null) { newTokenRefreshRequest = new TokenRefreshRequest(); this.currentTokenRefreshRequest = newTokenRefreshRequest; } } if (newTokenRefreshRequest != null) { newTokenRefreshRequest.bind(); } } void extendAccessTokenIfNeeded() { if (this.shouldExtendAccessToken()) { this.extendAccessToken(); } } void extendTokenCompleted(final Bundle bundle) { synchronized (this.lock) { final SessionState oldState = this.state; switch (this.state) { case OPENED: this.state = SessionState.OPENED_TOKEN_UPDATED; this.postStateChange(oldState, this.state, null); break; case OPENED_TOKEN_UPDATED: break; default: // Silently ignore attempts to refresh token if we are not open Log.d(TAG, "refreshToken ignored in state " + this.state); return; } this.tokenInfo = AccessToken.createFromRefresh(this.tokenInfo, bundle); if (this.tokenCachingStrategy != null) { this.tokenCachingStrategy.save(this.tokenInfo.toCacheBundle()); } } } void finishAuthOrReauth(AccessToken newToken, Exception exception) { // If the token we came up with is expired/invalid, then auth failed. if ((newToken != null) && newToken.isInvalid()) { newToken = null; exception = new FacebookException("Invalid access token."); } synchronized (this.lock) { switch (this.state) { case OPENING: // This means we are authorizing for the first time in this Session. this.finishAuthorization(newToken, exception); break; case OPENED: case OPENED_TOKEN_UPDATED: // This means we are reauthorizing. this.finishReauthorization(newToken, exception); break; case CREATED: case CREATED_TOKEN_LOADED: case CLOSED: case CLOSED_LOGIN_FAILED: Log.d(TAG, "Unexpected call to finishAuthOrReauth in state " + this.state); break; default: break; } } } Date getLastAttemptedTokenExtendDate() { return this.lastAttemptedTokenExtendDate; } AccessToken getTokenInfo() { return this.tokenInfo; } void handleAuthorizationResult(final int resultCode, final AuthorizationClient.Result result) { AccessToken newToken = null; Exception exception = null; if (resultCode == Activity.RESULT_OK) { if (result.code == AuthorizationClient.Result.Code.SUCCESS) { newToken = result.token; } else { exception = new FacebookAuthorizationException(result.errorMessage); } } else if (resultCode == Activity.RESULT_CANCELED) { exception = new FacebookOperationCanceledException(result.errorMessage); } this.logAuthorizationComplete(result.code, result.loggingExtras, exception); this.authorizationClient = null; this.finishAuthOrReauth(newToken, exception); } void postStateChange(final SessionState oldState, final SessionState newState, final Exception exception) { // When we request new permissions, we stay in SessionState.OPENED_TOKEN_UPDATED, // but we still want notifications of the state change since permissions are // different now. if ((oldState == newState) && (oldState != SessionState.OPENED_TOKEN_UPDATED) && (exception == null)) { return; } if (newState.isClosed()) { this.tokenInfo = AccessToken.createEmptyToken(); } // Need to schedule the callbacks inside the same queue to preserve ordering. // Otherwise these callbacks could have been added to the queue before the SessionTracker // gets the ACTIVE_SESSION_SET action. final Runnable runCallbacks = new Runnable() { @Override public void run() { synchronized (Session.this.callbacks) { for (final StatusCallback callback : Session.this.callbacks) { final Runnable closure = new Runnable() { @Override public void run() { // This can be called inside a synchronized block. callback.call(Session.this, newState, exception); } }; runWithHandlerOrExecutor(Session.this.handler, closure); } } } }; runWithHandlerOrExecutor(this.handler, runCallbacks); if (this == Session.activeSession) { if (oldState.isOpened() != newState.isOpened()) { if (newState.isOpened()) { postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_OPENED); } else { postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_CLOSED); } } } } void setCurrentTokenRefreshRequest(final TokenRefreshRequest request) { this.currentTokenRefreshRequest = request; } void setLastAttemptedTokenExtendDate(final Date lastAttemptedTokenExtendDate) { this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate; } void setTokenInfo(final AccessToken tokenInfo) { this.tokenInfo = tokenInfo; } boolean shouldExtendAccessToken() { if (this.currentTokenRefreshRequest != null) { return false; } boolean result = false; final Date now = new Date(); if (this.state.isOpened() && this.tokenInfo.getSource().canExtendToken() && ((now.getTime() - this.lastAttemptedTokenExtendDate.getTime()) > (TOKEN_EXTEND_RETRY_SECONDS * 1000)) && ((now.getTime() - this.tokenInfo.getLastRefresh().getTime()) > (TOKEN_EXTEND_THRESHOLD_SECONDS * 1000))) { result = true; } return result; } /** * Base class for authorization requests {@link OpenRequest} and {@link NewPermissionsRequest}. */ public static class AuthorizationRequest implements Serializable { private static final long serialVersionUID = 1L; protected final StartActivityDelegate startActivityDelegate; protected SessionLoginBehavior loginBehavior = SessionLoginBehavior.SSO_WITH_FALLBACK; protected int requestCode = DEFAULT_AUTHORIZE_ACTIVITY_CODE; private StatusCallback statusCallback; protected boolean legacy; protected List<String> permissions = Collections.emptyList(); protected SessionDefaultAudience defaultAudience = SessionDefaultAudience.FRIENDS; private String applicationId; private String validateSameFbidAsToken; private final String authId = UUID.randomUUID().toString(); protected final Map<String, String> loggingExtras = new HashMap<String, String>(); /** * Constructor to be used for V1 serialization only, DO NOT CHANGE. */ private AuthorizationRequest(final SessionLoginBehavior loginBehavior, final int requestCode, final List<String> permissions, final String defaultAudience, final boolean isLegacy, final String applicationId, final String validateSameFbidAsToken) { this.startActivityDelegate = new StartActivityDelegate() { @Override public Activity getActivityContext() { throw new UnsupportedOperationException( "Cannot create an AuthorizationRequest without a valid Activity or Fragment"); } @Override public void startActivityForResult(final Intent intent, final int requestCode) { throw new UnsupportedOperationException( "Cannot create an AuthorizationRequest without a valid Activity or Fragment"); } }; this.loginBehavior = loginBehavior; this.requestCode = requestCode; this.permissions = permissions; this.defaultAudience = SessionDefaultAudience.valueOf(defaultAudience); this.legacy = isLegacy; this.applicationId = applicationId; this.validateSameFbidAsToken = validateSameFbidAsToken; } AuthorizationRequest(final Activity activity) { this.startActivityDelegate = new StartActivityDelegate() { @Override public Activity getActivityContext() { return activity; } @Override public void startActivityForResult(final Intent intent, final int requestCode) { activity.startActivityForResult(intent, requestCode); } }; } AuthorizationRequest(final Fragment fragment) { this.startActivityDelegate = new StartActivityDelegate() { @Override public Activity getActivityContext() { return fragment.getActivity(); } @Override public void startActivityForResult(final Intent intent, final int requestCode) { fragment.startActivityForResult(intent, requestCode); } }; } /** * Used for backwards compatibility with Facebook.java only, DO NOT USE. * * @param isLegacy */ public void setIsLegacy(final boolean isLegacy) { this.legacy = isLegacy; } // have a readObject that throws to prevent spoofing; must be private so serializer will call it (will be // called automatically prior to any base class) private void readObject(final ObjectInputStream stream) throws InvalidObjectException { throw new InvalidObjectException("Cannot readObject, serialization proxy required"); } String getApplicationId() { return this.applicationId; } String getAuthId() { return this.authId; } AuthorizationClient.AuthorizationRequest getAuthorizationClientRequest() { final AuthorizationClient.StartActivityDelegate delegate = new AuthorizationClient.StartActivityDelegate() { @Override public Activity getActivityContext() { return AuthorizationRequest.this.startActivityDelegate.getActivityContext(); } @Override public void startActivityForResult(final Intent intent, final int requestCode) { AuthorizationRequest.this.startActivityDelegate.startActivityForResult(intent, requestCode); } }; return new AuthorizationClient.AuthorizationRequest(this.loginBehavior, this.requestCode, this.legacy, this.permissions, this.defaultAudience, this.applicationId, this.validateSameFbidAsToken, delegate, this.authId); } StatusCallback getCallback() { return this.statusCallback; } SessionDefaultAudience getDefaultAudience() { return this.defaultAudience; } SessionLoginBehavior getLoginBehavior() { return this.loginBehavior; } List<String> getPermissions() { return this.permissions; } int getRequestCode() { return this.requestCode; } StartActivityDelegate getStartActivityDelegate() { return this.startActivityDelegate; } String getValidateSameFbidAsToken() { return this.validateSameFbidAsToken; } boolean isLegacy() { return this.legacy; } void setApplicationId(final String applicationId) { this.applicationId = applicationId; } AuthorizationRequest setCallback(final StatusCallback statusCallback) { this.statusCallback = statusCallback; return this; } AuthorizationRequest setDefaultAudience(final SessionDefaultAudience defaultAudience) { if (defaultAudience != null) { this.defaultAudience = defaultAudience; } return this; } AuthorizationRequest setLoginBehavior(final SessionLoginBehavior loginBehavior) { if (loginBehavior != null) { this.loginBehavior = loginBehavior; } return this; } AuthorizationRequest setPermissions(final List<String> permissions) { if (permissions != null) { this.permissions = permissions; } return this; } AuthorizationRequest setPermissions(final String... permissions) { return this.setPermissions(Arrays.asList(permissions)); } AuthorizationRequest setRequestCode(final int requestCode) { if (requestCode >= 0) { this.requestCode = requestCode; } return this; } void setValidateSameFbidAsToken(final String validateSameFbidAsToken) { this.validateSameFbidAsToken = validateSameFbidAsToken; } // package private so subclasses can use it Object writeReplace() { return new AuthRequestSerializationProxyV1(this.loginBehavior, this.requestCode, this.permissions, this.defaultAudience.name(), this.legacy, this.applicationId, this.validateSameFbidAsToken); } private static class AuthRequestSerializationProxyV1 implements Serializable { private static final long serialVersionUID = -8748347685113614927L; private final SessionLoginBehavior loginBehavior; private final int requestCode; private final boolean isLegacy; private final List<String> permissions; private final String defaultAudience; private final String applicationId; private final String validateSameFbidAsToken; private AuthRequestSerializationProxyV1(final SessionLoginBehavior loginBehavior, final int requestCode, final List<String> permissions, final String defaultAudience, final boolean isLegacy, final String applicationId, final String validateSameFbidAsToken) { this.loginBehavior = loginBehavior; this.requestCode = requestCode; this.permissions = permissions; this.defaultAudience = defaultAudience; this.isLegacy = isLegacy; this.applicationId = applicationId; this.validateSameFbidAsToken = validateSameFbidAsToken; } private Object readResolve() { return new AuthorizationRequest(this.loginBehavior, this.requestCode, this.permissions, this.defaultAudience, this.isLegacy, this.applicationId, this.validateSameFbidAsToken); } } } /** * Builder class used to create a Session. */ public static final class Builder { private final Context context; private String applicationId; private TokenCachingStrategy tokenCachingStrategy; /** * Constructs a new Builder associated with the context. * * @param context the Activity or Service starting the Session */ public Builder(final Context context) { this.context = context; } /** * Build the Session. * * @return a new Session */ public Session build() { return new Session(this.context, this.applicationId, this.tokenCachingStrategy); } /** * Sets the application id for the Session. * * @param applicationId the application id * @return the Builder instance */ public Builder setApplicationId(final String applicationId) { this.applicationId = applicationId; return this; } /** * Sets the TokenCachingStrategy for the Session. * * @param tokenCachingStrategy the token cache to use * @return the Builder instance */ public Builder setTokenCachingStrategy(final TokenCachingStrategy tokenCachingStrategy) { this.tokenCachingStrategy = tokenCachingStrategy; return this; } } /** * A request to be used to request new permissions for a Session. */ public static final class NewPermissionsRequest extends AuthorizationRequest { private static final long serialVersionUID = 1L; /** * Constructs a NewPermissionsRequest. * * @param activity the Activity used to issue the request * @param permissions additional permissions to request */ public NewPermissionsRequest(final Activity activity, final List<String> permissions) { super(activity); this.setPermissions(permissions); } /** * Constructs a NewPermissionsRequest. * * @param activity the Activity used to issue the request * @param permissions additional permissions to request */ public NewPermissionsRequest(final Activity activity, final String... permissions) { super(activity); this.setPermissions(permissions); } /** * Constructs a NewPermissionsRequest. * * @param fragment the Fragment used to issue the request * @param permissions additional permissions to request */ public NewPermissionsRequest(final Fragment fragment, final List<String> permissions) { super(fragment); this.setPermissions(permissions); } /** * Constructs a NewPermissionsRequest. * * @param fragment the Fragment used to issue the request * @param permissions additional permissions to request */ public NewPermissionsRequest(final Fragment fragment, final String... permissions) { super(fragment); this.setPermissions(permissions); } /** * Sets the StatusCallback for the NewPermissionsRequest. Note that once the request is made, this callback will * be added to the session, and will receive all future state changes on the session. * * @param statusCallback The {@link StatusCallback SessionStatusCallback} to notify regarding Session state * changes. * @return the NewPermissionsRequest object to allow for chaining */ @Override public final NewPermissionsRequest setCallback(final StatusCallback statusCallback) { super.setCallback(statusCallback); return this; } /** * Sets the defaultAudience for the OpenRequest. * * @param defaultAudience A SessionDefaultAudience representing the default audience setting to request. * @return the NewPermissionsRequest object to allow for chaining */ @Override public final NewPermissionsRequest setDefaultAudience(final SessionDefaultAudience defaultAudience) { super.setDefaultAudience(defaultAudience); return this; } /** * Sets the login behavior for the NewPermissionsRequest. * * @param loginBehavior The {@link SessionLoginBehavior SessionLoginBehavior} that specifies what behaviors * should be attempted during authorization. * @return the NewPermissionsRequest object to allow for chaining */ @Override public final NewPermissionsRequest setLoginBehavior(final SessionLoginBehavior loginBehavior) { super.setLoginBehavior(loginBehavior); return this; } /** * Sets the request code for the NewPermissionsRequest. * * @param requestCode An integer that identifies this request. This integer will be used as the request code in * {@link Activity#onActivityResult onActivityResult}. This integer should be >= 0. If a value < 0 is * passed in, then a default value will be used. * @return the NewPermissionsRequest object to allow for chaining */ @Override public final NewPermissionsRequest setRequestCode(final int requestCode) { super.setRequestCode(requestCode); return this; } @Override AuthorizationClient.AuthorizationRequest getAuthorizationClientRequest() { final AuthorizationClient.AuthorizationRequest request = super.getAuthorizationClientRequest(); request.setRerequest(true); return request; } } /** * A request used to open a Session. */ public static final class OpenRequest extends AuthorizationRequest { private static final long serialVersionUID = 1L; /** * Constructs an OpenRequest. * * @param activity the Activity to use to open the Session */ public OpenRequest(final Activity activity) { super(activity); } /** * Constructs an OpenRequest. * * @param fragment the Fragment to use to open the Session */ public OpenRequest(final Fragment fragment) { super(fragment); } /** * Sets the StatusCallback for the OpenRequest. * * @param statusCallback The {@link StatusCallback SessionStatusCallback} to notify regarding Session state * changes. * @return the OpenRequest object to allow for chaining */ @Override public final OpenRequest setCallback(final StatusCallback statusCallback) { super.setCallback(statusCallback); return this; } /** * Sets the defaultAudience for the OpenRequest. * <p/> * This is only used during Native login using a sufficiently recent facebook app. * * @param defaultAudience A SessionDefaultAudience representing the default audience setting to request. * @return the OpenRequest object to allow for chaining */ @Override public final OpenRequest setDefaultAudience(final SessionDefaultAudience defaultAudience) { super.setDefaultAudience(defaultAudience); return this; } /** * Sets the login behavior for the OpenRequest. * * @param loginBehavior The {@link SessionLoginBehavior SessionLoginBehavior} that specifies what behaviors * should be attempted during authorization. * @return the OpenRequest object to allow for chaining */ @Override public final OpenRequest setLoginBehavior(final SessionLoginBehavior loginBehavior) { super.setLoginBehavior(loginBehavior); return this; } /** * Sets the permissions for the OpenRequest. * * @param permissions A List<String> representing the permissions to request during the authentication * flow. A null or empty List represents basic permissions. * @return the OpenRequest object to allow for chaining */ @Override public final OpenRequest setPermissions(final List<String> permissions) { super.setPermissions(permissions); return this; } /** * Sets the permissions for the OpenRequest. * * @param permissions the permissions to request during the authentication flow. * @return the OpenRequest object to allow for chaining */ @Override public final OpenRequest setPermissions(final String... permissions) { super.setPermissions(permissions); return this; } /** * Sets the request code for the OpenRequest. * * @param requestCode An integer that identifies this request. This integer will be used as the request code in * {@link Activity#onActivityResult onActivityResult}. This integer should be >= 0. If a value < 0 is * passed in, then a default value will be used. * @return the OpenRequest object to allow for chaining */ @Override public final OpenRequest setRequestCode(final int requestCode) { super.setRequestCode(requestCode); return this; } } /** * Provides asynchronous notification of Session state changes. * * @see Session#open open */ public interface StatusCallback { void call(Session session, SessionState state, Exception exception); } /** * Async implementation to allow auto publishing to not block the ui thread. */ private class AutoPublishAsyncTask extends AsyncTask<Void, Void, Void> { private final String mApplicationId; private final Context mApplicationContext; public AutoPublishAsyncTask(final String applicationId, final Context context) { this.mApplicationId = applicationId; this.mApplicationContext = context.getApplicationContext(); } @Override protected Void doInBackground(final Void... voids) { try { Settings.publishInstallAndWaitForResponse(this.mApplicationContext, this.mApplicationId, true); } catch (final Exception e) { // Utility.logd("Facebook-publish", e); } return null; } @Override protected void onPostExecute(final Void result) { // always clear out the publisher to allow other invocations. synchronized (Session.this) { Session.this.autoPublishAsyncTask = null; } } } /** * Serialization proxy for the Session class. This is version 1 of serialization. Future serializations may differ * in format. This class should not be modified. If serializations formats change, create a new class * SerializationProxyVx. */ private static class SerializationProxyV1 implements Serializable { private static final long serialVersionUID = 7663436173185080063L; private final String applicationId; private final SessionState state; private final AccessToken tokenInfo; private final Date lastAttemptedTokenExtendDate; private final boolean shouldAutoPublish; private final AuthorizationRequest pendingAuthorizationRequest; SerializationProxyV1(final String applicationId, final SessionState state, final AccessToken tokenInfo, final Date lastAttemptedTokenExtendDate, final boolean shouldAutoPublish, final AuthorizationRequest pendingAuthorizationRequest) { this.applicationId = applicationId; this.state = state; this.tokenInfo = tokenInfo; this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate; this.shouldAutoPublish = shouldAutoPublish; this.pendingAuthorizationRequest = pendingAuthorizationRequest; } private Object readResolve() { return new Session(this.applicationId, this.state, this.tokenInfo, this.lastAttemptedTokenExtendDate, this.shouldAutoPublish, this.pendingAuthorizationRequest); } } /** * Serialization proxy for the Session class. This is version 2 of serialization. Future serializations may differ * in format. This class should not be modified. If serializations formats change, create a new class * SerializationProxyVx. */ private static class SerializationProxyV2 implements Serializable { private static final long serialVersionUID = 7663436173185080064L; private final String applicationId; private final SessionState state; private final AccessToken tokenInfo; private final Date lastAttemptedTokenExtendDate; private final boolean shouldAutoPublish; private final AuthorizationRequest pendingAuthorizationRequest; private final Set<String> requestedPermissions; SerializationProxyV2(final String applicationId, final SessionState state, final AccessToken tokenInfo, final Date lastAttemptedTokenExtendDate, final boolean shouldAutoPublish, final AuthorizationRequest pendingAuthorizationRequest, final Set<String> requestedPermissions) { this.applicationId = applicationId; this.state = state; this.tokenInfo = tokenInfo; this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate; this.shouldAutoPublish = shouldAutoPublish; this.pendingAuthorizationRequest = pendingAuthorizationRequest; this.requestedPermissions = requestedPermissions; } private Object readResolve() { return new Session(this.applicationId, this.state, this.tokenInfo, this.lastAttemptedTokenExtendDate, this.shouldAutoPublish, this.pendingAuthorizationRequest, this.requestedPermissions); } } /** * Internal helper class that is used to hold two different permission lists (granted and declined) */ static class PermissionsPair { List<String> grantedPermissions; List<String> declinedPermissions; public PermissionsPair(final List<String> grantedPermissions, final List<String> declinedPermissions) { this.grantedPermissions = grantedPermissions; this.declinedPermissions = declinedPermissions; } public List<String> getDeclinedPermissions() { return this.declinedPermissions; } public List<String> getGrantedPermissions() { return this.grantedPermissions; } } interface StartActivityDelegate { Activity getActivityContext(); void startActivityForResult(Intent intent, int requestCode); } class TokenRefreshRequest implements ServiceConnection { final Messenger messageReceiver = new Messenger(new TokenRefreshRequestHandler(Session.this, this)); Messenger messageSender; public void bind() { final Intent intent = NativeProtocol.createTokenRefreshIntent(getStaticContext()); if ((intent != null) && staticContext.bindService(intent, this, Context.BIND_AUTO_CREATE)) { Session.this.setLastAttemptedTokenExtendDate(new Date()); } else { this.cleanup(); } } @Override public void onServiceConnected(final ComponentName className, final IBinder service) { this.messageSender = new Messenger(service); this.refreshToken(); } @Override public void onServiceDisconnected(final ComponentName arg) { this.cleanup(); // We returned an error so there's no point in // keeping the binding open. staticContext.unbindService(TokenRefreshRequest.this); } private void refreshToken() { final Bundle requestData = new Bundle(); requestData.putString(AccessToken.ACCESS_TOKEN_KEY, Session.this.getTokenInfo().getToken()); final Message request = Message.obtain(); request.setData(requestData); request.replyTo = this.messageReceiver; try { this.messageSender.send(request); } catch (final RemoteException e) { this.cleanup(); } } protected void cleanup() { if (Session.this.currentTokenRefreshRequest == this) { Session.this.currentTokenRefreshRequest = null; } } } // Creating a static Handler class to reduce the possibility of a memory leak. // Handler objects for the same thread all share a common Looper object, which they post messages // to and read from. As messages contain target Handler, as long as there are messages with target // handler in the message queue, the handler cannot be garbage collected. If handler is not static, // the instance of the containing class also cannot be garbage collected even if it is destroyed. static class TokenRefreshRequestHandler extends Handler { private final WeakReference<Session> sessionWeakReference; private final WeakReference<TokenRefreshRequest> refreshRequestWeakReference; TokenRefreshRequestHandler(final Session session, final TokenRefreshRequest refreshRequest) { super(Looper.getMainLooper()); this.sessionWeakReference = new WeakReference<Session>(session); this.refreshRequestWeakReference = new WeakReference<TokenRefreshRequest>(refreshRequest); } @Override public void handleMessage(final Message msg) { final String token = msg.getData().getString(AccessToken.ACCESS_TOKEN_KEY); final Session session = this.sessionWeakReference.get(); if ((session != null) && (token != null)) { session.extendTokenCompleted(msg.getData()); } final TokenRefreshRequest request = this.refreshRequestWeakReference.get(); if (request != null) { // The refreshToken function should be called rarely, // so there is no point in keeping the binding open. staticContext.unbindService(request); request.cleanup(); } } } }