/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
* copy, modify, and distribute this software in source code or binary form for use
* in connection with the web services and APIs provided by Facebook.
*
* As with any software that integrates with the Facebook platform, your use of
* this software is subject to the Facebook Developer Principles and Policies
* [http://developers.facebook.com/policy/]. This copyright notice shall be
* included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.facebook;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.facebook.internal.Utility;
import com.facebook.internal.Validate;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.*;
/**
* This class represents an immutable access token for using Facebook APIs. It also includes
* associated metadata such as expiration date and permissions.
* <p/>
* For more information on access tokens, see
* <a href="https://developers.facebook.com/docs/facebook-login/access-tokens/">Access Tokens</a>.
*/
public final class AccessToken implements Parcelable {
public static final String ACCESS_TOKEN_KEY = "access_token";
public static final String EXPIRES_IN_KEY = "expires_in";
public static final String USER_ID_KEY = "user_id";
private static final Date MAX_DATE = new Date(Long.MAX_VALUE);
private static final Date DEFAULT_EXPIRATION_TIME = MAX_DATE;
private static final Date DEFAULT_LAST_REFRESH_TIME = new Date();
private static final AccessTokenSource DEFAULT_ACCESS_TOKEN_SOURCE =
AccessTokenSource.FACEBOOK_APPLICATION_WEB;
// Constants related to JSON serialization.
private static final int CURRENT_JSON_FORMAT = 1;
private static final String VERSION_KEY = "version";
private static final String EXPIRES_AT_KEY = "expires_at";
private static final String PERMISSIONS_KEY = "permissions";
private static final String DECLINED_PERMISSIONS_KEY = "declined_permissions";
private static final String TOKEN_KEY = "token";
private static final String SOURCE_KEY = "source";
private static final String LAST_REFRESH_KEY = "last_refresh";
private static final String APPLICATION_ID_KEY = "application_id";
private final Date expires;
private final Set<String> permissions;
private final Set<String> declinedPermissions;
private final String token;
private final AccessTokenSource source;
private final Date lastRefresh;
private final String applicationId;
private final String userId;
/**
* Creates a new AccessToken using the supplied information from a previously-obtained access
* token (for instance, from an already-cached access token obtained prior to integration with
* the Facebook SDK). Note that the caller is asserting that all parameters provided are correct
* with respect to the access token string; no validation is done to verify they are correct.
*
* @param accessToken the access token string obtained from Facebook
* @param applicationId the ID of the Facebook Application associated with this access
* token
* @param userId the id of the user
* @param permissions the permissions that were requested when the token was obtained
* (or when it was last reauthorized); may be null if permission set
* is unknown
* @param declinedPermissions the permissions that were declined when the token was obtained;
* may be null if permission set is unknown
* @param accessTokenSource an enum indicating how the token was originally obtained (in most
* cases, this will be either AccessTokenSource.FACEBOOK_APPLICATION
* or AccessTokenSource.WEB_VIEW); if null, FACEBOOK_APPLICATION is
* assumed.
* @param expirationTime the expiration date associated with the token; if null, an
* infinite expiration time is assumed (but will become correct when
* the token is refreshed)
* @param lastRefreshTime the last time the token was refreshed (or when it was first
* obtained); if null, the current time is used.
*/
public AccessToken(
final String accessToken,
final String applicationId,
final String userId,
@Nullable
final Collection<String> permissions,
@Nullable
final Collection<String> declinedPermissions,
@Nullable
final AccessTokenSource accessTokenSource,
@Nullable
final Date expirationTime,
@Nullable
final Date lastRefreshTime
) {
Validate.notNullOrEmpty(accessToken, "accessToken");
Validate.notNullOrEmpty(applicationId, "applicationId");
Validate.notNullOrEmpty(userId, "userId");
this.expires = expirationTime != null ? expirationTime : DEFAULT_EXPIRATION_TIME;
this.permissions = Collections.unmodifiableSet(
permissions != null ? new HashSet<String>(permissions) : new HashSet<String>());
this.declinedPermissions = Collections.unmodifiableSet(
declinedPermissions != null
? new HashSet<String>(declinedPermissions)
: new HashSet<String>());
this.token = accessToken;
this.source = accessTokenSource != null ? accessTokenSource : DEFAULT_ACCESS_TOKEN_SOURCE;
this.lastRefresh = lastRefreshTime != null ? lastRefreshTime : DEFAULT_LAST_REFRESH_TIME;
this.applicationId = applicationId;
this.userId = userId;
}
/**
* Getter for the access token that is current for the application.
* @return The access token that is current for the application.
*/
public static AccessToken getCurrentAccessToken() {
return AccessTokenManager.getInstance().getCurrentAccessToken();
}
/**
* Setter for the access token that is current for the application.
* @param accessToken The access token to set.
*/
public static void setCurrentAccessToken(AccessToken accessToken) {
AccessTokenManager.getInstance().setCurrentAccessToken(accessToken);
}
/**
* Gets the string representing the access token.
*
* @return the string representing the access token
*/
public String getToken() {
return this.token;
}
/**
* Gets the date at which the access token expires.
*
* @return the expiration date of the token
*/
public Date getExpires() {
return this.expires;
}
/**
* Gets the list of permissions associated with this access token. Note that the most up-to-date
* list of permissions is maintained by Facebook, so this list may be outdated if
* permissions have been added or removed since the time the AccessToken object was created. For
* more information on permissions, see
* https://developers.facebook.com/docs/reference/login/#permissions.
*
* @return a read-only list of strings representing the permissions granted via this access
* token
*/
public Set<String> getPermissions() {
return this.permissions;
}
/**
* Gets the list of permissions declined by the user with this access token. It represents the
* entire set of permissions that have been requested and declined. Note that the most
* up-to-date list of permissions is maintained by Facebook, so this list may be
* outdated if permissions have been granted or declined since the last time an AccessToken
* object was created.
*
* @return a read-only list of strings representing the permissions declined by the user
*/
public Set<String> getDeclinedPermissions() {
return this.declinedPermissions;
}
/**
* Gets the {@link AccessTokenSource} indicating how this access token was obtained.
*
* @return the enum indicating how the access token was obtained
*/
public AccessTokenSource getSource() {
return source;
}
/**
* Gets the date at which the token was last refreshed. Since tokens expire, the Facebook SDK
* will attempt to renew them periodically.
*
* @return the date at which this token was last refreshed
*/
public Date getLastRefresh() {
return this.lastRefresh;
}
/**
* Gets the ID of the Facebook Application associated with this access token.
* @return the application ID
*/
public String getApplicationId() {
return applicationId;
}
/**
* Returns the user id for this access token.
* @return The user id for this access token.
*/
public String getUserId() { return userId; }
/**
* A callback for creating an access token from a NativeLinkingIntent
*/
public interface AccessTokenCreationCallback {
/**
* The method called on a successful creation of an AccessToken.
*
* @param token the access token created from the native link intent.
*/
public void onSuccess(AccessToken token);
public void onError(FacebookException error);
}
/**
* Creates a new AccessToken using the information contained in an Intent populated by the
* Facebook application in order to launch a native link. For more information on native
* linking, please see https://developers.facebook.com/docs/mobile/android/deep_linking/.
*
* @param intent the Intent that was used to start an Activity; must not be null
* @param applicationId the ID of the Facebook Application associated with this access token
*/
public static void createFromNativeLinkingIntent(
Intent intent,
final String applicationId,
final AccessTokenCreationCallback accessTokenCallback) {
Validate.notNull(intent, "intent");
if (intent.getExtras() == null) {
accessTokenCallback.onError(
new FacebookException("No extras found on intent"));
return;
}
final Bundle extras = new Bundle(intent.getExtras());
String userId = extras.getString(USER_ID_KEY);
// Old versions of facebook for android don't provide the UserId. Obtain the id if missing
if (userId == null || userId.isEmpty()) {
Utility.getGraphMeRequestWithCacheAsync(extras.getString(ACCESS_TOKEN_KEY),
new Utility.GraphMeRequestWithCacheCallback() {
@Override
public void onSuccess(JSONObject userInfo) {
try {
String userId = userInfo.getString("id");
extras.putString(USER_ID_KEY, userId);
accessTokenCallback.onSuccess(createFromBundle(
null,
extras,
AccessTokenSource.FACEBOOK_APPLICATION_WEB,
new Date(),
applicationId));
} catch (JSONException ex) {
accessTokenCallback.onError(
new FacebookException(
"Unable to generate access token due to missing user id"));
}
}
@Override
public void onFailure(FacebookException error) {
accessTokenCallback.onError(error);
}
});
} else {
accessTokenCallback.onSuccess(createFromBundle(
null,
extras,
AccessTokenSource.FACEBOOK_APPLICATION_WEB,
new Date(),
applicationId));
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{AccessToken");
builder.append(" token:").append(tokenToString());
appendPermissions(builder);
builder.append("}");
return builder.toString();
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof AccessToken)) {
return false;
}
AccessToken o = (AccessToken) other;
return expires.equals(o.expires) &&
permissions.equals(o.permissions) &&
declinedPermissions.equals(o.declinedPermissions) &&
token.equals(o.token) &&
source == o.source &&
lastRefresh.equals(o.lastRefresh) &&
(applicationId == null ?
o.applicationId == null :
applicationId.equals(o.applicationId)) &&
userId.equals(o.userId);
}
@Override
public int hashCode() {
int result = 17;
result = result * 31 + expires.hashCode();
result = result * 31 + permissions.hashCode();
result = result * 31 + declinedPermissions.hashCode();
result = result * 31 + token.hashCode();
result = result * 31 + source.hashCode();
result = result * 31 + lastRefresh.hashCode();
result = result * 31 + (applicationId == null ? 0 : applicationId.hashCode());
result = result * 31 + userId.hashCode();
return result;
}
@SuppressLint("FieldGetter")
static AccessToken createFromRefresh(AccessToken current, Bundle bundle) {
// Only tokens obtained via SSO support refresh. Token refresh returns the expiration date
// in seconds from the epoch rather than seconds from now.
if (current.source != AccessTokenSource.FACEBOOK_APPLICATION_WEB &&
current.source != AccessTokenSource.FACEBOOK_APPLICATION_NATIVE &&
current.source != AccessTokenSource.FACEBOOK_APPLICATION_SERVICE) {
throw new FacebookException("Invalid token source: " + current.source);
}
Date expires = Utility.getBundleLongAsDate(bundle, EXPIRES_IN_KEY, new Date(0));
String token = bundle.getString(ACCESS_TOKEN_KEY);
if (Utility.isNullOrEmpty(token)) {
return null;
}
return new AccessToken(
token,
current.applicationId,
current.getUserId(),
current.getPermissions(),
current.getDeclinedPermissions(),
current.source,
expires,
new Date());
}
static AccessToken createFromLegacyCache(Bundle bundle) {
List<String> permissions = getPermissionsFromBundle(
bundle,
LegacyTokenHelper.PERMISSIONS_KEY);
List<String> declinedPermissions = getPermissionsFromBundle(
bundle,
LegacyTokenHelper.DECLINED_PERMISSIONS_KEY);
String applicationId = LegacyTokenHelper.getApplicationId(bundle);
if (Utility.isNullOrEmpty(applicationId)) {
applicationId = FacebookSdk.getApplicationId();
}
String tokenString = LegacyTokenHelper.getToken(bundle);
String userId;
JSONObject userInfo = Utility.awaitGetGraphMeRequestWithCache(tokenString);
try {
userId = userInfo.getString("id");
} catch (JSONException ex) {
// This code is only used by AccessTokenCache. If we for any reason fail to get the
// user id just return null.
return null;
}
return new AccessToken(
tokenString,
applicationId,
userId,
permissions,
declinedPermissions,
LegacyTokenHelper.getSource(bundle),
LegacyTokenHelper.getDate(
bundle,
LegacyTokenHelper.EXPIRATION_DATE_KEY),
LegacyTokenHelper.getDate(
bundle,
LegacyTokenHelper.LAST_REFRESH_DATE_KEY)
);
}
static List<String> getPermissionsFromBundle(Bundle bundle, String key) {
// Copy the list so we can guarantee immutable
List<String> originalPermissions = bundle.getStringArrayList(key);
List<String> permissions;
if (originalPermissions == null) {
permissions = Collections.emptyList();
} else {
permissions = Collections.unmodifiableList(new ArrayList<String>(originalPermissions));
}
return permissions;
}
/**
* Shows if the token is expired.
* @return true if the token is expired.
*/
public boolean isExpired() {
return new Date().after(this.expires);
}
JSONObject toJSONObject() throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put(VERSION_KEY, CURRENT_JSON_FORMAT);
jsonObject.put(TOKEN_KEY, token);
jsonObject.put(EXPIRES_AT_KEY, expires.getTime());
JSONArray permissionsArray = new JSONArray(permissions);
jsonObject.put(PERMISSIONS_KEY, permissionsArray);
JSONArray declinedPermissionsArray = new JSONArray(declinedPermissions);
jsonObject.put(DECLINED_PERMISSIONS_KEY, declinedPermissionsArray);
jsonObject.put(LAST_REFRESH_KEY, lastRefresh.getTime());
jsonObject.put(SOURCE_KEY, source.name());
jsonObject.put(APPLICATION_ID_KEY, applicationId);
jsonObject.put(USER_ID_KEY, userId);
return jsonObject;
}
static AccessToken createFromJSONObject(JSONObject jsonObject) throws JSONException {
int version = jsonObject.getInt(VERSION_KEY);
if (version > CURRENT_JSON_FORMAT) {
throw new FacebookException("Unknown AccessToken serialization format.");
}
String token = jsonObject.getString(TOKEN_KEY);
Date expiresAt = new Date(jsonObject.getLong(EXPIRES_AT_KEY));
JSONArray permissionsArray = jsonObject.getJSONArray(PERMISSIONS_KEY);
JSONArray declinedPermissionsArray = jsonObject.getJSONArray(DECLINED_PERMISSIONS_KEY);
Date lastRefresh = new Date(jsonObject.getLong(LAST_REFRESH_KEY));
AccessTokenSource source = AccessTokenSource.valueOf(jsonObject.getString(SOURCE_KEY));
String applicationId = jsonObject.getString(APPLICATION_ID_KEY);
String userId = jsonObject.getString(USER_ID_KEY);
return new AccessToken(
token,
applicationId,
userId,
Utility.jsonArrayToStringList(permissionsArray),
Utility.jsonArrayToStringList(declinedPermissionsArray),
source,
expiresAt,
lastRefresh);
}
private static AccessToken createFromBundle(
List<String> requestedPermissions,
Bundle bundle,
AccessTokenSource source,
Date expirationBase,
String applicationId) {
String token = bundle.getString(ACCESS_TOKEN_KEY);
Date expires = Utility.getBundleLongAsDate(bundle, EXPIRES_IN_KEY, expirationBase);
String userId = bundle.getString(USER_ID_KEY);
if (Utility.isNullOrEmpty(token) || (expires == null)) {
return null;
}
return new AccessToken(
token,
applicationId,
userId,
requestedPermissions,
null,
source,
expires,
new Date());
}
private String tokenToString() {
if (this.token == null) {
return "null";
} else if (FacebookSdk.isLoggingBehaviorEnabled(LoggingBehavior.INCLUDE_ACCESS_TOKENS)) {
return this.token;
} else {
return "ACCESS_TOKEN_REMOVED";
}
}
private void appendPermissions(StringBuilder builder) {
builder.append(" permissions:");
if (this.permissions == null) {
builder.append("null");
} else {
builder.append("[");
builder.append(TextUtils.join(", ", permissions));
builder.append("]");
}
}
AccessToken(Parcel parcel) {
this.expires = new Date(parcel.readLong());
ArrayList<String> permissionsList = new ArrayList<>();
parcel.readStringList(permissionsList);
this.permissions = Collections.unmodifiableSet(new HashSet<String>(permissionsList));
permissionsList.clear();
parcel.readStringList(permissionsList);
this.declinedPermissions = Collections.unmodifiableSet(
new HashSet<String>(permissionsList));
this.token = parcel.readString();
this.source = AccessTokenSource.valueOf(parcel.readString());
this.lastRefresh = new Date(parcel.readLong());
this.applicationId = parcel.readString();
this.userId = parcel.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(expires.getTime());
dest.writeStringList(new ArrayList<String>(permissions));
dest.writeStringList(new ArrayList<String>(declinedPermissions));
dest.writeString(token);
dest.writeString(source.name());
dest.writeLong(lastRefresh.getTime());
dest.writeString(applicationId);
dest.writeString(userId);
}
public static final Parcelable.Creator<AccessToken> CREATOR = new Parcelable.Creator() {
@Override
public AccessToken createFromParcel(Parcel source) {
return new AccessToken(source);
}
@Override
public AccessToken[] newArray(int size) {
return new AccessToken[size];
}
};
}