/**
* 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.login;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.content.Context;
import com.facebook.AccessToken;
import com.facebook.CallbackManager;
import com.facebook.FacebookActivity;
import com.facebook.FacebookAuthorizationException;
import com.facebook.FacebookCallback;
import com.facebook.FacebookException;
import com.facebook.FacebookSdk;
import com.facebook.GraphResponse;
import com.facebook.Profile;
import com.facebook.internal.CallbackManagerImpl;
import com.facebook.internal.Validate;
import com.facebook.appevents.AppEventsConstants;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* This class manages login and permissions for Facebook.
*/
public class LoginManager {
private static final String PUBLISH_PERMISSION_PREFIX = "publish";
private static final String MANAGE_PERMISSION_PREFIX = "manage";
private static final Set<String> OTHER_PUBLISH_PERMISSIONS = getOtherPublishPermissions();
private static volatile LoginManager instance;
private LoginBehavior loginBehavior = LoginBehavior.SSO_WITH_FALLBACK;
private DefaultAudience defaultAudience = DefaultAudience.FRIENDS;
private LoginClient.Request pendingLoginRequest;
private HashMap<String, String> pendingLoggingExtras;
private Context context;
private LoginLogger loginLogger;
LoginManager() {
Validate.sdkInitialized();
}
/**
* Getter for the login manager.
* @return The login manager.
*/
public static LoginManager getInstance() {
if (instance == null) {
synchronized (LoginManager.class) {
if (instance == null) {
instance = new LoginManager();
}
}
}
return instance;
}
/**
* Starts the login process to resolve the error defined in the response. The registered login
* callbacks will be called on completion.
*
* @param activity The activity which is starting the login process.
* @param response The response that has the error.
*/
public void resolveError(final Activity activity, final GraphResponse response) {
startLogin(
new ActivityStartActivityDelegate(activity),
createLoginRequestFromResponse(response)
);
}
/**
* Starts the login process to resolve the error defined in the response. The registered login
* callbacks will be called on completion.
*
* @param fragment The fragment which is starting the login process.
* @param response The response that has the error.
*/
public void resolveError(final Fragment fragment, final GraphResponse response) {
startLogin(
new FragmentStartActivityDelegate(fragment),
createLoginRequestFromResponse(response)
);
}
private LoginClient.Request createLoginRequestFromResponse(final GraphResponse response) {
Validate.notNull(response, "response");
AccessToken failedToken = response.getRequest().getAccessToken();
return createLoginRequest(failedToken != null ? failedToken.getPermissions() : null);
}
/**
* Registers a login callback to the given callback manager.
* @param callbackManager The callback manager that will encapsulate the callback.
* @param callback The login callback that will be called on login completion.
*/
public void registerCallback(
final CallbackManager callbackManager,
final FacebookCallback<LoginResult> callback) {
if (!(callbackManager instanceof CallbackManagerImpl)) {
throw new FacebookException("Unexpected CallbackManager, " +
"please use the provided Factory.");
}
((CallbackManagerImpl) callbackManager).registerCallback(
CallbackManagerImpl.RequestCodeOffset.Login.toRequestCode(),
new CallbackManagerImpl.Callback() {
@Override
public boolean onActivityResult(int resultCode, Intent data) {
return LoginManager.this.onActivityResult(
resultCode,
data,
callback);
}
}
);
}
boolean onActivityResult(int resultCode, Intent data) {
return onActivityResult(resultCode, data, null);
}
boolean onActivityResult(int resultCode, Intent data, FacebookCallback<LoginResult> callback) {
if (pendingLoginRequest == null) {
return false;
}
FacebookException exception = null;
AccessToken newToken = null;
LoginClient.Result.Code code = LoginClient.Result.Code.ERROR;
Map<String, String> loggingExtras = null;
boolean isCanceled = false;
if (data != null) {
LoginClient.Result result = (LoginClient.Result)
data.getParcelableExtra(LoginFragment.RESULT_KEY);
if (result != null) {
code = result.code;
if (resultCode == Activity.RESULT_OK) {
if (result.code == LoginClient.Result.Code.SUCCESS) {
newToken = result.token;
} else {
exception = new FacebookAuthorizationException(result.errorMessage);
}
} else if (resultCode == Activity.RESULT_CANCELED) {
isCanceled = true;
}
loggingExtras = result.loggingExtras;
}
} else if (resultCode == Activity.RESULT_CANCELED) {
isCanceled = true;
code = LoginClient.Result.Code.CANCEL;
}
if (exception == null && newToken == null && !isCanceled) {
exception = new FacebookException("Unexpected call to LoginManager.onActivityResult");
}
logCompleteLogin(code, loggingExtras, exception);
finishLogin(newToken, exception, isCanceled, callback);
return true;
}
/**
* Getter for the login behavior.
* @return the login behavior.
*/
public LoginBehavior getLoginBehavior() {
return loginBehavior;
}
/**
* Setter for the login behavior.
* @param loginBehavior The login behavior.
* @return The login manager.
*/
public LoginManager setLoginBehavior(LoginBehavior loginBehavior) {
this.loginBehavior = loginBehavior;
return this;
}
/**
* Getter for the default audience.
* @return The default audience.
*/
public DefaultAudience getDefaultAudience() {
return defaultAudience;
}
/**
* Setter for the default audience.
* @param defaultAudience The default audience.
* @return The login manager.
*/
public LoginManager setDefaultAudience(DefaultAudience defaultAudience) {
this.defaultAudience = defaultAudience;
return this;
}
/**
* Logs out the user.
*/
public void logOut() {
AccessToken.setCurrentAccessToken(null);
Profile.setCurrentProfile(null);
}
/**
* Logs the user in with the requested read permissions.
* @param fragment The fragment which is starting the login process.
* @param permissions The requested permissions.
*/
public void logInWithReadPermissions(Fragment fragment, Collection<String> permissions) {
validateReadPermissions(permissions);
LoginClient.Request loginRequest = createLoginRequest(permissions);
startLogin(new FragmentStartActivityDelegate(fragment), loginRequest);
}
/**
* Logs the user in with the requested read permissions.
* @param activity The activity which is starting the login process.
* @param permissions The requested permissions.
*/
public void logInWithReadPermissions(Activity activity, Collection<String> permissions) {
validateReadPermissions(permissions);
LoginClient.Request loginRequest = createLoginRequest(permissions);
startLogin(new ActivityStartActivityDelegate(activity), loginRequest);
}
/**
* Logs the user in with the requested publish permissions.
* @param fragment The fragment which is starting the login process.
* @param permissions The requested permissions.
*/
public void logInWithPublishPermissions(Fragment fragment, Collection<String> permissions) {
validatePublishPermissions(permissions);
LoginClient.Request loginRequest = createLoginRequest(permissions);
startLogin(new FragmentStartActivityDelegate(fragment), loginRequest);
}
/**
* Logs the user in with the requested publish permissions.
* @param activity The activity which is starting the login process.
* @param permissions The requested permissions.
*/
public void logInWithPublishPermissions(Activity activity, Collection<String> permissions) {
validatePublishPermissions(permissions);
LoginClient.Request loginRequest = createLoginRequest(permissions);
startLogin(new ActivityStartActivityDelegate(activity), loginRequest);
}
LoginClient.Request getPendingLoginRequest() {
return pendingLoginRequest;
}
private void validateReadPermissions(Collection<String> permissions) {
if (permissions == null) {
return;
}
for (String permission : permissions) {
if (isPublishPermission(permission)) {
throw new FacebookException(
String.format(
"Cannot pass a publish or manage permission (%s) to a request for read " +
"authorization",
permission));
}
}
}
private void validatePublishPermissions(Collection<String> permissions) {
if (permissions == null) {
return;
}
for (String permission : permissions) {
if (!isPublishPermission(permission)) {
throw new FacebookException(
String.format(
"Cannot pass a read permission (%s) to a request for publish authorization",
permission));
}
}
}
private static boolean isPublishPermission(String permission) {
return permission != null &&
(permission.startsWith(PUBLISH_PERMISSION_PREFIX) ||
permission.startsWith(MANAGE_PERMISSION_PREFIX) ||
OTHER_PUBLISH_PERMISSIONS.contains(permission));
}
private static Set<String> getOtherPublishPermissions() {
HashSet<String> set = new HashSet<String>() {{
add("ads_management");
add("create_event");
add("rsvp_event");
}};
return Collections.unmodifiableSet(set);
}
private LoginClient.Request createLoginRequest(Collection<String> permissions) {
LoginClient.Request request = new LoginClient.Request(
loginBehavior,
Collections.unmodifiableSet(
permissions != null ? new HashSet(permissions) : new HashSet<String>()),
defaultAudience,
FacebookSdk.getApplicationId(),
UUID.randomUUID().toString()
);
request.setRerequest(AccessToken.getCurrentAccessToken() != null);
return request;
}
private void startLogin(
StartActivityDelegate startActivityDelegate,
LoginClient.Request request
) throws FacebookException {
this.pendingLoginRequest = request;
this.pendingLoggingExtras = new HashMap<>();
this.context = startActivityDelegate.getActivityContext();
logStartLogin();
// Make sure the static handler for login is registered if there isn't an explicit callback
CallbackManagerImpl.registerStaticCallback(
CallbackManagerImpl.RequestCodeOffset.Login.toRequestCode(),
new CallbackManagerImpl.Callback() {
@Override
public boolean onActivityResult(int resultCode, Intent data) {
return LoginManager.this.onActivityResult(resultCode, data);
}
}
);
boolean started = tryLoginActivity(startActivityDelegate, request);
pendingLoggingExtras.put(
LoginLogger.EVENT_EXTRAS_TRY_LOGIN_ACTIVITY,
started ?
AppEventsConstants.EVENT_PARAM_VALUE_YES : AppEventsConstants.EVENT_PARAM_VALUE_NO
);
if (!started) {
FacebookException exception = new FacebookException(
"Log in attempt failed: LoginActivity could not be started");
logCompleteLogin(LoginClient.Result.Code.ERROR, null, exception);
this.pendingLoginRequest = null;
throw exception;
}
}
private LoginLogger getLogger() {
if (loginLogger == null ||
!loginLogger.getApplicationId().equals(
pendingLoginRequest.getApplicationId())) {
loginLogger = new LoginLogger(
context,
pendingLoginRequest.getApplicationId());
}
return loginLogger;
}
private void logStartLogin() {
getLogger().logStartLogin(pendingLoginRequest);
}
private void logCompleteLogin(LoginClient.Result.Code result, Map<String, String> resultExtras,
Exception exception) {
if (pendingLoginRequest == null) {
// We don't expect this to happen, but if it does, log an event for diagnostic purposes.
getLogger().logUnexpectedError(
LoginLogger.EVENT_NAME_LOGIN_COMPLETE,
"Unexpected call to logCompleteLogin with null pendingAuthorizationRequest."
);
} else {
getLogger().logCompleteLogin(
pendingLoginRequest.getAuthId(),
pendingLoggingExtras,
result,
resultExtras,
exception);
}
}
private boolean tryLoginActivity(
StartActivityDelegate startActivityDelegate,
LoginClient.Request request) {
Intent intent = getLoginActivityIntent(request);
if (!resolveIntent(intent)) {
return false;
}
try {
startActivityDelegate.startActivityForResult(
intent,
LoginClient.getLoginRequestCode());
} catch (ActivityNotFoundException e) {
return false;
}
return true;
}
private boolean resolveIntent(Intent intent) {
ResolveInfo resolveInfo = FacebookSdk.getApplicationContext().getPackageManager()
.resolveActivity(intent, 0);
if (resolveInfo == null) {
return false;
}
return true;
}
private Intent getLoginActivityIntent(LoginClient.Request request) {
Intent intent = new Intent();
intent.setClass(FacebookSdk.getApplicationContext(), FacebookActivity.class);
intent.setAction(request.getLoginBehavior().toString());
// Let LoginActivity populate extras appropriately
LoginClient.Request authClientRequest = request;
Bundle extras = LoginFragment.populateIntentExtras(authClientRequest);
intent.putExtras(extras);
return intent;
}
static LoginResult computeLoginResult(
final LoginClient.Request request,
final AccessToken newToken
) {
Set<String> requestedPermissions = request.getPermissions();
Set<String> grantedPermissions = new HashSet<String>(newToken.getPermissions());
// If it's a reauth, subset the granted permissions to just the requested permissions
// so we don't report implicit permissions like user_profile as recently granted.
if (request.isRerequest()) {
grantedPermissions.retainAll(requestedPermissions);
}
Set<String> deniedPermissions = new HashSet<String>(requestedPermissions);
deniedPermissions.removeAll(grantedPermissions);
return new LoginResult(newToken, grantedPermissions, deniedPermissions);
}
private void finishLogin(
AccessToken newToken,
FacebookException exception,
boolean isCanceled,
FacebookCallback<LoginResult> callback) {
if (newToken != null) {
AccessToken.setCurrentAccessToken(newToken);
Profile.fetchProfileForCurrentAccessToken();
}
if (callback != null) {
LoginResult loginResult = newToken != null
? computeLoginResult(pendingLoginRequest, newToken)
: null;
// If there are no granted permissions, the operation is treated as cancel.
if (isCanceled
|| (loginResult != null
&& loginResult.getRecentlyGrantedPermissions().size() == 0)) {
callback.onCancel();
return;
}
if (exception != null) {
callback.onError(exception);
} else if (newToken != null) {
callback.onSuccess(loginResult);
}
}
}
private static class ActivityStartActivityDelegate implements StartActivityDelegate {
private final Activity activity;
ActivityStartActivityDelegate(final Activity activity) {
Validate.notNull(activity, "activity");
this.activity = activity;
}
@Override
public void startActivityForResult(Intent intent, int requestCode) {
activity.startActivityForResult(intent, requestCode);
}
@Override
public Activity getActivityContext() {
return activity;
}
}
private static class FragmentStartActivityDelegate implements StartActivityDelegate {
private final Fragment fragment;
FragmentStartActivityDelegate(final Fragment fragment) {
Validate.notNull(fragment, "fragment");
this.fragment = fragment;
}
@Override
public void startActivityForResult(Intent intent, int requestCode) {
fragment.startActivityForResult(intent, requestCode);
}
@Override
public Activity getActivityContext() {
return fragment.getActivity();
}
}
}