com.facebook.Session Maven / Gradle / Ivy
Show all versions of facebook-android-api Show documentation
/**
* 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.facebook;
import android.app.Activity;
import android.content.*;
import android.content.pm.ResolveInfo;
import android.os.*;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import com.facebook.internal.*;
import com.facebook.model.GraphMultiResult;
import com.facebook.model.GraphObject;
import com.facebook.model.GraphObjectList;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.*;
import java.lang.ref.WeakReference;
import java.util.*;
/**
*
* Session is used to authenticate a user and manage the user's session with
* Facebook.
*
*
* 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.
*
*
* Instances of Session provide state change notification via a callback
* interface, {@link Session.StatusCallback StatusCallback}.
*
*/
public class Session implements Serializable {
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;
private 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 OTHER_PUBLISH_PERMISSIONS = new HashSet() {{
add("ads_management");
add("create_event");
add("rsvp_event");
}};
private String applicationId;
private SessionState state;
private AccessToken tokenInfo;
private Date lastAttemptedTokenExtendDate = new Date(0);
private AuthorizationRequest pendingAuthorizationRequest;
private AuthorizationClient authorizationClient;
private Set requestedPermissions = new HashSet();
// The following are not serialized with the Session object
private volatile Bundle authorizationBundle;
private final List callbacks;
private Handler handler;
private AutoPublishAsyncTask autoPublishAsyncTask;
// This is the object that synchronizes access to state and tokenInfo
private final Object lock = new Object();
private TokenCachingStrategy tokenCachingStrategy;
private volatile TokenRefreshRequest currentTokenRefreshRequest;
private AppEventsLogger appEventsLogger;
/**
* 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(String applicationId, SessionState state,
AccessToken tokenInfo, Date lastAttemptedTokenExtendDate,
boolean shouldAutoPublish, 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(applicationId, state, tokenInfo,
lastAttemptedTokenExtendDate, shouldAutoPublish, 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 requestedPermissions;
SerializationProxyV2(String applicationId, SessionState state,
AccessToken tokenInfo, Date lastAttemptedTokenExtendDate,
boolean shouldAutoPublish, AuthorizationRequest pendingAuthorizationRequest,
Set 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(applicationId, state, tokenInfo,
lastAttemptedTokenExtendDate, shouldAutoPublish, pendingAuthorizationRequest, requestedPermissions);
}
}
/**
* Used by version 1 of the serialization proxy, do not modify.
*/
private Session(String applicationId, SessionState state,
AccessToken tokenInfo, Date lastAttemptedTokenExtendDate,
boolean shouldAutoPublish, AuthorizationRequest pendingAuthorizationRequest) {
this.applicationId = applicationId;
this.state = state;
this.tokenInfo = tokenInfo;
this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate;
this.pendingAuthorizationRequest = pendingAuthorizationRequest;
handler = new Handler(Looper.getMainLooper());
currentTokenRefreshRequest = null;
tokenCachingStrategy = null;
callbacks = new ArrayList();
}
/**
* Used by version 2 of the serialization proxy, do not modify.
*/
private Session(String applicationId, SessionState state,
AccessToken tokenInfo, Date lastAttemptedTokenExtendDate,
boolean shouldAutoPublish, AuthorizationRequest pendingAuthorizationRequest,
Set requestedPermissions) {
this.applicationId = applicationId;
this.state = state;
this.tokenInfo = tokenInfo;
this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate;
this.pendingAuthorizationRequest = pendingAuthorizationRequest;
this.requestedPermissions = requestedPermissions;
handler = new Handler(Looper.getMainLooper());
currentTokenRefreshRequest = null;
tokenCachingStrategy = null;
callbacks = new ArrayList();
}
/**
* Initializes a new Session with the specified context.
*
* @param currentContext The Activity or Service creating this Session.
*/
public Session(Context currentContext) {
this(currentContext, null, null, true);
}
Session(Context context, String applicationId, TokenCachingStrategy tokenCachingStrategy) {
this(context, applicationId, tokenCachingStrategy, true);
}
Session(Context context, String applicationId, TokenCachingStrategy tokenCachingStrategy,
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();
this.handler = new Handler(Looper.getMainLooper());
Bundle tokenState = loadTokenFromCache ? tokenCachingStrategy.load() : null;
if (TokenCachingStrategy.hasTokenInformation(tokenState)) {
Date cachedExpirationDate = TokenCachingStrategy
.getDate(tokenState, TokenCachingStrategy.EXPIRATION_DATE_KEY);
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(Collections.emptyList());
} 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(Collections.emptyList());
}
}
/**
* 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;
}
}
/**
* 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();
}
}
public final boolean isClosed() {
synchronized (this.lock) {
return this.state.isClosed();
}
}
/**
* 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;
}
}
/**
* Returns the application id associated with this Session.
*
* @return the application id associated with this Session.
*/
public final String getApplicationId() {
return this.applicationId;
}
/**
* 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 Date at which the current token will expire.
*
*
* Note that Session automatically attempts to extend the lifetime of Tokens
* as needed when Facebook requests are made.
*
*
* @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();
}
}
/**
*
* Returns the list of permissions associated with the session.
*
*
* 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}.
*
*
* @return the list of permissions associated with the session, or null if there is no access token
*/
public final List getPermissions() {
synchronized (this.lock) {
return (this.tokenInfo == null) ? null : this.tokenInfo.getPermissions();
}
}
/**
*
* Returns whether a particular permission has been granted
*
*
* @param permission The permission to check for
* @return true if the permission is granted, false otherwise
*/
public boolean isPermissionGranted(String permission) {
List grantedPermissions = getPermissions();
if (grantedPermissions != null) {
return grantedPermissions.contains(permission);
}
return false;
}
/**
*
* Returns the list of permissions that have been requested in this session but not granted
*
*
* @return the list of requested permissions that have been declined
*/
public final List getDeclinedPermissions() {
synchronized (this.lock) {
List grantedPermissions = getPermissions();
List declinedPermissions = new ArrayList(requestedPermissions);
if (grantedPermissions != null) {
boolean removedBasicInfo = false;
for (String permission : grantedPermissions) {
declinedPermissions.remove(permission);
// We can remove "basic_info" permission if we have any granted permissions
if (!removedBasicInfo && requestedPermissions.contains(BASIC_INFO_PERMISSION)) {
declinedPermissions.remove(BASIC_INFO_PERMISSION);
removedBasicInfo = true;
}
}
}
return declinedPermissions;
}
}
/**
*
* Logs a user in to Facebook.
*
*
* 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.
*
*
* 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.
*
*
* 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.
*
*
* @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(OpenRequest openRequest) {
open(openRequest, SessionAuthorizationType.READ);
}
/**
*
* Logs a user in to Facebook.
*
*
* 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.
*
*
* 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/.
*
*
* 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.
*
*
* @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(OpenRequest openRequest) {
open(openRequest, SessionAuthorizationType.PUBLISH);
}
/**
* 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.
*
* 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(AccessToken accessToken, StatusCallback callback) {
synchronized (this.lock) {
if (pendingAuthorizationRequest != null) {
throw new UnsupportedOperationException(
"Session: an attempt was made to open a session that has a pending request.");
}
if (state.isClosed()) {
throw new UnsupportedOperationException(
"Session: an attempt was made to open a previously-closed session.");
} else if (state != SessionState.CREATED && state != SessionState.CREATED_TOKEN_LOADED) {
throw new UnsupportedOperationException(
"Session: an attempt was made to open an already opened session.");
}
if (callback != null) {
addCallback(callback);
}
this.tokenInfo = accessToken;
if (this.tokenCachingStrategy != null) {
this.tokenCachingStrategy.save(accessToken.toCacheBundle());
}
final SessionState oldState = state;
state = SessionState.OPENED;
this.postStateChange(oldState, state, null);
}
autoPublishAsync();
}
/**
*
* Issues a request to add new read permissions to the Session.
*
*
* If successful, this will update the set of permissions on this session to
* match the newPermissions. If this fails, the Session remains unchanged.
*
*
* 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.
*
*
* @param newPermissionsRequest the new permissions request
*/
public final void requestNewReadPermissions(NewPermissionsRequest newPermissionsRequest) {
requestNewPermissions(newPermissionsRequest, SessionAuthorizationType.READ);
}
/**
*
* Issues a request to add new publish or manage permissions to the Session.
*
*
* If successful, this will update the set of permissions on this session to
* match the newPermissions. If this fails, the Session remains unchanged.
*
*
* 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.
*
*
* @param newPermissionsRequest the new permissions request
*/
public final void requestNewPublishPermissions(NewPermissionsRequest newPermissionsRequest) {
requestNewPermissions(newPermissionsRequest, SessionAuthorizationType.PUBLISH);
}
/**
*
* Issues a request to refresh the permissions on the session.
*
*
* 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.
*
*/
public final void refreshPermissions() {
Request request = new Request(this, "me/permissions");
request.setCallback(new Request.Callback() {
@Override
public void onCompleted(Response response) {
List grantedPermissions = handlePermissionResponse(Session.this, response);
if (grantedPermissions != null) {
// Update our token with the refreshed permissions
synchronized (lock) {
tokenInfo = AccessToken.createFromTokenWithRefreshedPermissions(tokenInfo, grantedPermissions);
postStateChange(state, SessionState.OPENED_TOKEN_UPDATED, null);
}
}
}
});
request.executeAsync();
}
/**
* 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 session An optional session to update the requested permission set
* @param response The server response
* @return A list of granted permissions or null if an error
*/
static List handlePermissionResponse(Session session, Response response) {
if (response.getError() != null) {
return null;
}
GraphMultiResult result = response.getGraphObjectAs(GraphMultiResult.class);
if (result == null) {
return null;
}
GraphObjectList data = result.getData();
if (data == null || data.size() == 0) {
return null;
}
List allPermissions = new ArrayList(data.size());
List grantedPermissions = new ArrayList(data.size());
// Check if we are dealing with v2.0 or v1.0 behavior until the server is updated
GraphObject firstObject = data.get(0);
if (firstObject.getProperty("permission") != null) { // v2.0
for (GraphObject graphObject : data) {
String permission = (String) graphObject.getProperty("permission");
String status = (String) graphObject.getProperty("status");
allPermissions.add(permission);
if(status.equals("granted")) {
grantedPermissions.add(permission);
}
}
} else { // v1.0
Map permissionsMap = firstObject.asMap();
for (Map.Entry entry : permissionsMap.entrySet()) {
if (entry.getKey().equals("installed")) {
continue;
}
allPermissions.add(entry.getKey());
if ((Integer)entry.getValue() == 1) {
grantedPermissions.add(entry.getKey());
}
}
}
// If we have a session track all the permissions that were requested
if (session != null) {
session.addRequestedPermissions(allPermissions);
}
return grantedPermissions;
}
/**
* 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(Activity currentActivity, int requestCode, int resultCode, Intent data) {
Validate.notNull(currentActivity, "currentActivity");
initializeStaticContext(currentActivity);
synchronized (lock) {
if (pendingAuthorizationRequest == null || (requestCode != pendingAuthorizationRequest.getRequestCode())) {
return false;
}
}
Exception exception = null;
AuthorizationClient.Result.Code code = AuthorizationClient.Result.Code.ERROR;
if (data != null) {
AuthorizationClient.Result result = (AuthorizationClient.Result) data.getSerializableExtra(
LoginActivity.RESULT_KEY);
if (result != null) {
// This came from LoginActivity.
handleAuthorizationResult(resultCode, result);
return true;
} else if (authorizationClient != null) {
// Delegate to the auth client.
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");
}
logAuthorizationComplete(code, null, exception);
finishAuthOrReauth(null, exception);
return true;
}
/**
* 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;
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;
postStateChange(oldState, this.state, null);
break;
case CLOSED:
case CLOSED_LOGIN_FAILED:
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);
close();
}
/**
* Adds a callback that will be called when the state of this Session changes.
*
* @param callback the callback
*/
public final void addCallback(StatusCallback callback) {
synchronized (callbacks) {
if (callback != null && !callbacks.contains(callback)) {
callbacks.add(callback);
}
}
}
/**
* Removes a StatusCallback from this Session.
*
* @param callback the callback
*/
public final void removeCallback(StatusCallback callback) {
synchronized (callbacks) {
callbacks.remove(callback);
}
}
@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();
}
void extendTokenCompleted(Bundle bundle) {
synchronized (this.lock) {
final SessionState oldState = this.state;
switch (this.state) {
case OPENED:
this.state = SessionState.OPENED_TOKEN_UPDATED;
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());
}
}
}
private Object writeReplace() {
return new SerializationProxyV2(applicationId, state, tokenInfo,
lastAttemptedTokenExtendDate, false, pendingAuthorizationRequest, requestedPermissions);
}
// have a readObject that throws to prevent spoofing
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("Cannot readObject, serialization proxy required");
}
/**
* 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(Session session, Bundle bundle) {
if (bundle != null && session != null && !bundle.containsKey(SESSION_BUNDLE_SAVE_KEY)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
new ObjectOutputStream(outputStream).writeObject(session);
} catch (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);
}
}
/**
* 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(
Context context, TokenCachingStrategy cachingStrategy, StatusCallback callback, Bundle bundle) {
if (bundle == null) {
return null;
}
byte[] data = bundle.getByteArray(SESSION_BUNDLE_SAVE_KEY);
if (data != null) {
ByteArrayInputStream is = new ByteArrayInputStream(data);
try {
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 (ClassNotFoundException e) {
Log.w(TAG, "Unable to restore session", e);
} catch (IOException e) {
Log.w(TAG, "Unable to restore session.", e);
}
}
return null;
}
/**
* 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;
}
}
/**
*
* Sets the current active Session.
*
*
* The active Session is used implicitly by predefined Request factory
* methods as well as optionally by UI controls in the sdk.
*
*
* It is legal to set this to null, or to a Session that is not yet open.
*
*
* @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(Session session) {
synchronized (Session.STATIC_LOCK) {
if (session != Session.activeSession) {
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);
}
}
}
}
}
/**
* 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(Context context) {
return openActiveSession(context, false, null);
}
/**
* 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.
*
* 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(Activity activity, boolean allowLoginUI,
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.
*
* 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(Activity activity, boolean allowLoginUI,
List permissions, 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.
*
* 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(Context context, Fragment fragment,
boolean allowLoginUI, StatusCallback callback) {
return openActiveSession(context, allowLoginUI, new OpenRequest(fragment).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.
*
* 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(Context context, Fragment fragment,
boolean allowLoginUI, List permissions, StatusCallback callback) {
return openActiveSession(
context,
allowLoginUI,
new OpenRequest(fragment).setCallback(callback).setPermissions(permissions));
}
/**
* 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.
*
* 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(Context context, AccessToken accessToken,
StatusCallback callback) {
Session session = new Session(context, null, null, false);
setActiveSession(session);
session.open(accessToken, callback);
return session;
}
private static Session openActiveSession(Context context, boolean allowLoginUI, OpenRequest openRequest) {
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;
}
static void initializeStaticContext(Context currentContext) {
if ((currentContext != null) && (staticContext == null)) {
Context applicationContext = currentContext.getApplicationContext();
staticContext = (applicationContext != null) ? applicationContext : currentContext;
}
}
void authorize(AuthorizationRequest request) {
boolean started = false;
request.setApplicationId(applicationId);
autoPublishAsync();
logAuthorizationStart();
started = tryLoginActivity(request);
addRequestedPermissions(request.getPermissions());
pendingAuthorizationRequest.loggingExtras.put(AuthorizationClient.EVENT_EXTRAS_TRY_LOGIN_ACTIVITY,
started ? AppEventsConstants.EVENT_PARAM_VALUE_YES : AppEventsConstants.EVENT_PARAM_VALUE_NO);
if (!started && request.isLegacy) {
pendingAuthorizationRequest.loggingExtras.put(AuthorizationClient.EVENT_EXTRAS_TRY_LEGACY,
AppEventsConstants.EVENT_PARAM_VALUE_YES);
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;
Exception exception = new FacebookException(
"Log in attempt failed: LoginActivity could not be started, and not legacy request");
logAuthorizationComplete(AuthorizationClient.Result.Code.ERROR, null, exception);
postStateChange(oldState, this.state, exception);
}
}
}
}
private void open(OpenRequest openRequest, SessionAuthorizationType authType) {
validatePermissions(openRequest, authType);
validateLoginBehavior(openRequest);
SessionState newState;
synchronized (this.lock) {
if (pendingAuthorizationRequest != null) {
postStateChange(state, 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");
}
pendingAuthorizationRequest = openRequest;
break;
case CREATED_TOKEN_LOADED:
if (openRequest != null && !Utility.isNullOrEmpty(openRequest.getPermissions())) {
if (!Utility.isSubset(openRequest.getPermissions(), getPermissions())) {
pendingAuthorizationRequest = openRequest;
}
}
if (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) {
addCallback(openRequest.getCallback());
}
this.postStateChange(oldState, newState, null);
}
if (newState == SessionState.OPENING) {
authorize(openRequest);
}
}
private void requestNewPermissions(NewPermissionsRequest newPermissionsRequest, SessionAuthorizationType authType) {
validatePermissions(newPermissionsRequest, authType);
validateLoginBehavior(newPermissionsRequest);
if (newPermissionsRequest != null) {
synchronized (this.lock) {
if (pendingAuthorizationRequest != null) {
throw new UnsupportedOperationException(
"Session: an attempt was made to request new permissions for a session that has a pending request.");
}
if (state.isOpened()) {
pendingAuthorizationRequest = newPermissionsRequest;
} else if (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(getAccessToken());
addCallback(newPermissionsRequest.getCallback());
authorize(newPermissionsRequest);
}
}
private void validateLoginBehavior(AuthorizationRequest request) {
if (request != null && !request.isLegacy) {
Intent intent = new Intent();
intent.setClass(getStaticContext(), LoginActivity.class);
if (!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(AuthorizationRequest request, 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 (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));
}
}
}
}
public static boolean isPublishPermission(String permission) {
return permission != null &&
(permission.startsWith(PUBLISH_PERMISSION_PREFIX) ||
permission.startsWith(MANAGE_PERMISSION_PREFIX) ||
OTHER_PUBLISH_PERMISSIONS.contains(permission));
}
private void handleAuthorizationResult(int resultCode, 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);
}
logAuthorizationComplete(result.code, result.loggingExtras, exception);
authorizationClient = null;
finishAuthOrReauth(newToken, exception);
}
private void logAuthorizationStart() {
Bundle bundle = AuthorizationClient.newAuthorizationLoggingBundle(pendingAuthorizationRequest.getAuthId());
bundle.putLong(AuthorizationClient.EVENT_PARAM_TIMESTAMP, System.currentTimeMillis());
// Log what we already know about the call in start event
try {
JSONObject extras = new JSONObject();
extras.put(AuthorizationClient.EVENT_EXTRAS_LOGIN_BEHAVIOR,
pendingAuthorizationRequest.loginBehavior.toString());
extras.put(AuthorizationClient.EVENT_EXTRAS_REQUEST_CODE, pendingAuthorizationRequest.requestCode);
extras.put(AuthorizationClient.EVENT_EXTRAS_IS_LEGACY, pendingAuthorizationRequest.isLegacy);
extras.put(AuthorizationClient.EVENT_EXTRAS_PERMISSIONS,
TextUtils.join(",", pendingAuthorizationRequest.permissions));
extras.put(AuthorizationClient.EVENT_EXTRAS_DEFAULT_AUDIENCE,
pendingAuthorizationRequest.defaultAudience.toString());
bundle.putString(AuthorizationClient.EVENT_PARAM_EXTRAS, extras.toString());
} catch (JSONException e) {
}
AppEventsLogger logger = getAppEventsLogger();
logger.logSdkEvent(AuthorizationClient.EVENT_NAME_LOGIN_START, null, bundle);
}
private void logAuthorizationComplete(AuthorizationClient.Result.Code result, Map resultExtras,
Exception exception) {
Bundle bundle = null;
if (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(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 (pendingAuthorizationRequest.loggingExtras.isEmpty() == false) {
jsonObject = new JSONObject(pendingAuthorizationRequest.loggingExtras);
}
if (resultExtras != null) {
if (jsonObject == null) {
jsonObject = new JSONObject();
}
try {
for (Map.Entry entry : resultExtras.entrySet()) {
jsonObject.put(entry.getKey(), entry.getValue());
}
} catch (JSONException e) {
}
}
if (jsonObject != null) {
bundle.putString(AuthorizationClient.EVENT_PARAM_EXTRAS, jsonObject.toString());
}
}
bundle.putLong(AuthorizationClient.EVENT_PARAM_TIMESTAMP, System.currentTimeMillis());
AppEventsLogger logger = getAppEventsLogger();
logger.logSdkEvent(AuthorizationClient.EVENT_NAME_LOGIN_COMPLETE, null, bundle);
}
private boolean tryLoginActivity(AuthorizationRequest request) {
Intent intent = getLoginActivityIntent(request);
if (!resolveIntent(intent)) {
return false;
}
try {
request.getStartActivityDelegate().startActivityForResult(intent, request.getRequestCode());
} catch (ActivityNotFoundException e) {
return false;
}
return true;
}
private boolean resolveIntent(Intent intent) {
ResolveInfo resolveInfo = getStaticContext().getPackageManager().resolveActivity(intent, 0);
if (resolveInfo == null) {
return false;
}
return true;
}
private Intent getLoginActivityIntent(AuthorizationRequest request) {
Intent intent = new Intent();
intent.setClass(getStaticContext(), LoginActivity.class);
intent.setAction(request.getLoginBehavior().toString());
// Let LoginActivity populate extras appropriately
AuthorizationClient.AuthorizationRequest authClientRequest = request.getAuthorizationClientRequest();
Bundle extras = LoginActivity.populateIntentExtras(authClientRequest);
intent.putExtras(extras);
return intent;
}
private void tryLegacyAuth(final AuthorizationRequest request) {
authorizationClient = new AuthorizationClient();
authorizationClient.setOnCompletedListener(new AuthorizationClient.OnCompletedListener() {
@Override
public void onCompleted(AuthorizationClient.Result result) {
int activityResult;
if (result.code == AuthorizationClient.Result.Code.CANCEL) {
activityResult = Activity.RESULT_CANCELED;
} else {
activityResult = Activity.RESULT_OK;
}
handleAuthorizationResult(activityResult, result);
}
});
authorizationClient.setContext(getStaticContext());
authorizationClient.startOrContinueAuth(request.getAuthorizationClientRequest());
}
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.
finishAuthorization(newToken, exception);
break;
case OPENED:
case OPENED_TOKEN_UPDATED:
// This means we are reauthorizing.
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;
}
}
}
private void finishAuthorization(AccessToken newToken, Exception exception) {
final SessionState oldState = state;
if (newToken != null) {
tokenInfo = newToken;
saveTokenToCache(newToken);
state = SessionState.OPENED;
} else if (exception != null) {
state = SessionState.CLOSED_LOGIN_FAILED;
}
pendingAuthorizationRequest = null;
postStateChange(oldState, state, exception);
}
private void finishReauthorization(final AccessToken newToken, Exception exception) {
final SessionState oldState = state;
if (newToken != null) {
tokenInfo = newToken;
saveTokenToCache(newToken);
state = SessionState.OPENED_TOKEN_UPDATED;
}
pendingAuthorizationRequest = null;
postStateChange(oldState, state, exception);
}
private void saveTokenToCache(AccessToken newToken) {
if (newToken != null && tokenCachingStrategy != null) {
tokenCachingStrategy.save(newToken.toCacheBundle());
}
}
private void addRequestedPermissions(List permissions) {
synchronized (this.lock) {
for(String permission : permissions) {
requestedPermissions.add(permission);
}
}
}
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(Collections.emptyList());
}
// 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.
Runnable runCallbacks = new Runnable() {
public void run() {
synchronized (callbacks) {
for (final StatusCallback callback : callbacks) {
Runnable closure = new Runnable() {
public void run() {
// This can be called inside a synchronized block.
callback.call(Session.this, newState, exception);
}
};
runWithHandlerOrExecutor(handler, closure);
}
}
}
};
runWithHandlerOrExecutor(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);
}
}
}
}
static void postActiveSessionAction(String action) {
final Intent intent = new Intent(action);
LocalBroadcastManager.getInstance(getStaticContext()).sendBroadcast(intent);
}
private static void runWithHandlerOrExecutor(Handler handler, Runnable runnable) {
if (handler != null) {
handler.post(runnable);
} else {
Settings.getExecutor().execute(runnable);
}
}
void extendAccessTokenIfNeeded() {
if (shouldExtendAccessToken()) {
extendAccessToken();
}
}
void extendAccessToken() {
TokenRefreshRequest newTokenRefreshRequest = null;
synchronized (this.lock) {
if (currentTokenRefreshRequest == null) {
newTokenRefreshRequest = new TokenRefreshRequest();
currentTokenRefreshRequest = newTokenRefreshRequest;
}
}
if (newTokenRefreshRequest != null) {
newTokenRefreshRequest.bind();
}
}
boolean shouldExtendAccessToken() {
if (currentTokenRefreshRequest != null) {
return false;
}
boolean result = false;
Date now = new Date();
if (state.isOpened() && tokenInfo.getSource().canExtendToken()
&& now.getTime() - lastAttemptedTokenExtendDate.getTime() > TOKEN_EXTEND_RETRY_SECONDS * 1000
&& now.getTime() - tokenInfo.getLastRefresh().getTime() > TOKEN_EXTEND_THRESHOLD_SECONDS * 1000) {
result = true;
}
return result;
}
private AppEventsLogger getAppEventsLogger() {
synchronized (lock) {
if (appEventsLogger == null) {
appEventsLogger = AppEventsLogger.newLogger(staticContext, applicationId);
}
return appEventsLogger;
}
}
AccessToken getTokenInfo() {
return tokenInfo;
}
void setTokenInfo(AccessToken tokenInfo) {
this.tokenInfo = tokenInfo;
}
Date getLastAttemptedTokenExtendDate() {
return lastAttemptedTokenExtendDate;
}
void setLastAttemptedTokenExtendDate(Date lastAttemptedTokenExtendDate) {
this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate;
}
void setCurrentTokenRefreshRequest(TokenRefreshRequest request) {
this.currentTokenRefreshRequest = request;
}
class TokenRefreshRequest implements ServiceConnection {
final Messenger messageReceiver = new Messenger(
new TokenRefreshRequestHandler(Session.this, this));
Messenger messageSender = null;
public void bind() {
Intent intent = NativeProtocol.createTokenRefreshIntent(getStaticContext());
if (intent != null
&& staticContext.bindService(intent, this, Context.BIND_AUTO_CREATE)) {
setLastAttemptedTokenExtendDate(new Date());
} else {
cleanup();
}
}
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
messageSender = new Messenger(service);
refreshToken();
}
@Override
public void onServiceDisconnected(ComponentName arg) {
cleanup();
// We returned an error so there's no point in
// keeping the binding open.
staticContext.unbindService(TokenRefreshRequest.this);
}
private void cleanup() {
if (currentTokenRefreshRequest == this) {
currentTokenRefreshRequest = null;
}
}
private void refreshToken() {
Bundle requestData = new Bundle();
requestData.putString(AccessToken.ACCESS_TOKEN_KEY, getTokenInfo().getToken());
Message request = Message.obtain();
request.setData(requestData);
request.replyTo = messageReceiver;
try {
messageSender.send(request);
} catch (RemoteException e) {
cleanup();
}
}
}
// 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 WeakReference sessionWeakReference;
private WeakReference refreshRequestWeakReference;
TokenRefreshRequestHandler(Session session, TokenRefreshRequest refreshRequest) {
super(Looper.getMainLooper());
sessionWeakReference = new WeakReference(session);
refreshRequestWeakReference = new WeakReference(refreshRequest);
}
@Override
public void handleMessage(Message msg) {
String token = msg.getData().getString(AccessToken.ACCESS_TOKEN_KEY);
Session session = sessionWeakReference.get();
if (session != null && token != null) {
session.extendTokenCompleted(msg.getData());
}
TokenRefreshRequest request = 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();
}
}
}
/**
* Provides asynchronous notification of Session state changes.
*
* @see Session#open open
*/
public interface StatusCallback {
public void call(Session session, SessionState state, Exception exception);
}
@Override
public int hashCode() {
return 0;
}
@Override
public boolean equals(Object otherObj) {
if (!(otherObj instanceof Session)) {
return false;
}
Session other = (Session) otherObj;
return areEqual(other.applicationId, applicationId) &&
areEqual(other.authorizationBundle, authorizationBundle) &&
areEqual(other.state, state) &&
areEqual(other.getExpirationDate(), getExpirationDate());
}
private static boolean areEqual(Object a, Object b) {
if (a == null) {
return b == null;
} else {
return a.equals(b);
}
}
/**
* 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(Context context) {
this.context = context;
}
/**
* 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;
}
/**
* Build the Session.
*
* @return a new Session
*/
public Session build() {
return new Session(context, applicationId, tokenCachingStrategy);
}
}
interface StartActivityDelegate {
public void startActivityForResult(Intent intent, int requestCode);
public Activity getActivityContext();
}
@SuppressWarnings("deprecation")
private void autoPublishAsync() {
AutoPublishAsyncTask asyncTask = null;
synchronized (this) {
if (autoPublishAsyncTask == null && Settings.getShouldAutoPublishInstall()) {
// copy the application id to guarantee thread safety against our container.
String applicationId = Session.this.applicationId;
// skip publish if we don't have an application id.
if (applicationId != null) {
asyncTask = autoPublishAsyncTask = new AutoPublishAsyncTask(applicationId, staticContext);
}
}
}
if (asyncTask != null) {
asyncTask.execute();
}
}
/**
* Async implementation to allow auto publishing to not block the ui thread.
*/
private class AutoPublishAsyncTask extends AsyncTask {
private final String mApplicationId;
private final Context mApplicationContext;
public AutoPublishAsyncTask(String applicationId, Context context) {
mApplicationId = applicationId;
mApplicationContext = context.getApplicationContext();
}
@Override
protected Void doInBackground(Void... voids) {
try {
Settings.publishInstallAndWaitForResponse(mApplicationContext, mApplicationId, true);
} catch (Exception e) {
Utility.logd("Facebook-publish", e);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
// always clear out the publisher to allow other invocations.
synchronized (Session.this) {
autoPublishAsyncTask = null;
}
}
}
/**
* Base class for authorization requests {@link OpenRequest} and {@link NewPermissionsRequest}.
*/
public static class AuthorizationRequest implements Serializable {
private static final long serialVersionUID = 1L;
private final StartActivityDelegate startActivityDelegate;
private SessionLoginBehavior loginBehavior = SessionLoginBehavior.SSO_WITH_FALLBACK;
private int requestCode = DEFAULT_AUTHORIZE_ACTIVITY_CODE;
private StatusCallback statusCallback;
private boolean isLegacy = false;
private List permissions = Collections.emptyList();
private SessionDefaultAudience defaultAudience = SessionDefaultAudience.FRIENDS;
private String applicationId;
private String validateSameFbidAsToken;
private final String authId = UUID.randomUUID().toString();
private final Map loggingExtras = new HashMap();
AuthorizationRequest(final Activity activity) {
startActivityDelegate = new StartActivityDelegate() {
@Override
public void startActivityForResult(Intent intent, int requestCode) {
activity.startActivityForResult(intent, requestCode);
}
@Override
public Activity getActivityContext() {
return activity;
}
};
}
AuthorizationRequest(final Fragment fragment) {
startActivityDelegate = new StartActivityDelegate() {
@Override
public void startActivityForResult(Intent intent, int requestCode) {
fragment.startActivityForResult(intent, requestCode);
}
@Override
public Activity getActivityContext() {
return fragment.getActivity();
}
};
}
/**
* Constructor to be used for V1 serialization only, DO NOT CHANGE.
*/
private AuthorizationRequest(SessionLoginBehavior loginBehavior, int requestCode,
List permissions, String defaultAudience, boolean isLegacy, String applicationId,
String validateSameFbidAsToken) {
startActivityDelegate = new StartActivityDelegate() {
@Override
public void startActivityForResult(Intent intent, int requestCode) {
throw new UnsupportedOperationException(
"Cannot create an AuthorizationRequest without a valid Activity or Fragment");
}
@Override
public Activity getActivityContext() {
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.isLegacy = isLegacy;
this.applicationId = applicationId;
this.validateSameFbidAsToken = validateSameFbidAsToken;
}
/**
* Used for backwards compatibility with Facebook.java only, DO NOT USE.
*
* @param isLegacy
*/
public void setIsLegacy(boolean isLegacy) {
this.isLegacy = isLegacy;
}
boolean isLegacy() {
return isLegacy;
}
AuthorizationRequest setCallback(StatusCallback statusCallback) {
this.statusCallback = statusCallback;
return this;
}
StatusCallback getCallback() {
return statusCallback;
}
AuthorizationRequest setLoginBehavior(SessionLoginBehavior loginBehavior) {
if (loginBehavior != null) {
this.loginBehavior = loginBehavior;
}
return this;
}
SessionLoginBehavior getLoginBehavior() {
return loginBehavior;
}
AuthorizationRequest setRequestCode(int requestCode) {
if (requestCode >= 0) {
this.requestCode = requestCode;
}
return this;
}
int getRequestCode() {
return requestCode;
}
AuthorizationRequest setPermissions(List permissions) {
if (permissions != null) {
this.permissions = permissions;
}
return this;
}
AuthorizationRequest setPermissions(String... permissions) {
return setPermissions(Arrays.asList(permissions));
}
List getPermissions() {
return permissions;
}
AuthorizationRequest setDefaultAudience(SessionDefaultAudience defaultAudience) {
if (defaultAudience != null) {
this.defaultAudience = defaultAudience;
}
return this;
}
SessionDefaultAudience getDefaultAudience() {
return defaultAudience;
}
StartActivityDelegate getStartActivityDelegate() {
return startActivityDelegate;
}
String getApplicationId() {
return applicationId;
}
void setApplicationId(String applicationId) {
this.applicationId = applicationId;
}
String getValidateSameFbidAsToken() {
return validateSameFbidAsToken;
}
void setValidateSameFbidAsToken(String validateSameFbidAsToken) {
this.validateSameFbidAsToken = validateSameFbidAsToken;
}
String getAuthId() {
return authId;
}
AuthorizationClient.AuthorizationRequest getAuthorizationClientRequest() {
AuthorizationClient.StartActivityDelegate delegate = new AuthorizationClient.StartActivityDelegate() {
@Override
public void startActivityForResult(Intent intent, int requestCode) {
startActivityDelegate.startActivityForResult(intent, requestCode);
}
@Override
public Activity getActivityContext() {
return startActivityDelegate.getActivityContext();
}
};
return new AuthorizationClient.AuthorizationRequest(loginBehavior, requestCode, isLegacy,
permissions, defaultAudience, applicationId, validateSameFbidAsToken, delegate, authId);
}
// package private so subclasses can use it
Object writeReplace() {
return new AuthRequestSerializationProxyV1(
loginBehavior, requestCode, permissions, defaultAudience.name(), isLegacy, applicationId, validateSameFbidAsToken);
}
// 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(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("Cannot readObject, serialization proxy required");
}
private static class AuthRequestSerializationProxyV1 implements Serializable {
private static final long serialVersionUID = -8748347685113614927L;
private final SessionLoginBehavior loginBehavior;
private final int requestCode;
private boolean isLegacy;
private final List permissions;
private final String defaultAudience;
private final String applicationId;
private final String validateSameFbidAsToken;
private AuthRequestSerializationProxyV1(SessionLoginBehavior loginBehavior,
int requestCode, List permissions, String defaultAudience, boolean isLegacy,
String applicationId, 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(loginBehavior, requestCode, permissions, defaultAudience, isLegacy,
applicationId, validateSameFbidAsToken);
}
}
}
/**
* 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(Activity activity) {
super(activity);
}
/**
* Constructs an OpenRequest.
*
* @param fragment the Fragment to use to open the Session
*/
public OpenRequest(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
*/
public final OpenRequest setCallback(StatusCallback statusCallback) {
super.setCallback(statusCallback);
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
*/
public final OpenRequest setLoginBehavior(SessionLoginBehavior loginBehavior) {
super.setLoginBehavior(loginBehavior);
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
*/
public final OpenRequest setRequestCode(int requestCode) {
super.setRequestCode(requestCode);
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
*/
public final OpenRequest setPermissions(List 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
*/
public final OpenRequest setPermissions(String... permissions) {
super.setPermissions(permissions);
return this;
}
/**
* Sets the defaultAudience for the OpenRequest.
*
* 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
*/
public final OpenRequest setDefaultAudience(SessionDefaultAudience defaultAudience) {
super.setDefaultAudience(defaultAudience);
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(Activity activity, List permissions) {
super(activity);
setPermissions(permissions);
}
/**
* Constructs a NewPermissionsRequest.
*
* @param fragment the Fragment used to issue the request
* @param permissions additional permissions to request
*/
public NewPermissionsRequest(Fragment fragment, List permissions) {
super(fragment);
setPermissions(permissions);
}
/**
* Constructs a NewPermissionsRequest.
*
* @param activity the Activity used to issue the request
* @param permissions additional permissions to request
*/
public NewPermissionsRequest(Activity activity, String... permissions) {
super(activity);
setPermissions(permissions);
}
/**
* Constructs a NewPermissionsRequest.
*
* @param fragment the Fragment used to issue the request
* @param permissions additional permissions to request
*/
public NewPermissionsRequest(Fragment fragment, String... permissions) {
super(fragment);
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
*/
public final NewPermissionsRequest setCallback(StatusCallback statusCallback) {
super.setCallback(statusCallback);
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
*/
public final NewPermissionsRequest setLoginBehavior(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
*/
public final NewPermissionsRequest setRequestCode(int requestCode) {
super.setRequestCode(requestCode);
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
*/
public final NewPermissionsRequest setDefaultAudience(SessionDefaultAudience defaultAudience) {
super.setDefaultAudience(defaultAudience);
return this;
}
@Override
AuthorizationClient.AuthorizationRequest getAuthorizationClientRequest() {
AuthorizationClient.AuthorizationRequest request = super.getAuthorizationClientRequest();
request.setRerequest(true);
return request;
}
}
}