/**
* 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.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.text.TextUtils;
import com.facebook.AccessToken;
import com.facebook.FacebookException;
import com.facebook.R;
import com.facebook.appevents.AppEventsConstants;
import com.facebook.internal.CallbackManagerImpl;
import com.facebook.internal.Utility;
import com.facebook.internal.Validate;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
class LoginClient implements Parcelable {
LoginMethodHandler[] handlersToTry;
int currentHandler = -1;
Fragment fragment;
OnCompletedListener onCompletedListener;
BackgroundProcessingListener backgroundProcessingListener;
boolean checkedInternetPermission;
Request pendingRequest;
Map<String, String> loggingExtras;
private LoginLogger loginLogger;
public interface OnCompletedListener {
void onCompleted(Result result);
}
interface BackgroundProcessingListener {
void onBackgroundProcessingStarted();
void onBackgroundProcessingStopped();
}
public LoginClient(Fragment fragment) {
this.fragment = fragment;
}
public Fragment getFragment() {
return fragment;
}
void setFragment(Fragment fragment) {
if (this.fragment != null) {
throw new FacebookException("Can't set fragment once it is already set.");
}
this.fragment = fragment;
}
FragmentActivity getActivity() {
return fragment.getActivity();
}
public Request getPendingRequest() {
return pendingRequest;
}
public static int getLoginRequestCode() {
return CallbackManagerImpl.RequestCodeOffset.Login.toRequestCode();
}
void startOrContinueAuth(Request request) {
if (!getInProgress()) {
authorize(request);
}
}
void authorize(Request request) {
if (request == null) {
return;
}
if (pendingRequest != null) {
throw new FacebookException("Attempted to authorize while a request is pending.");
}
if (AccessToken.getCurrentAccessToken() != null && !checkInternetPermission()) {
// We're going to need INTERNET permission later and don't have it, so fail early.
return;
}
pendingRequest = request;
handlersToTry = getHandlersToTry(request);
tryNextHandler();
}
boolean getInProgress() {
return pendingRequest != null && currentHandler >= 0;
}
void cancelCurrentHandler() {
if (currentHandler >= 0) {
getCurrentHandler().cancel();
}
}
LoginMethodHandler getCurrentHandler() {
if (currentHandler >= 0) {
return handlersToTry[currentHandler];
} else {
return null;
}
}
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
if (pendingRequest != null) {
return getCurrentHandler()
.onActivityResult(requestCode, resultCode, data);
}
return false;
}
protected LoginMethodHandler [] getHandlersToTry(Request request) {
ArrayList<LoginMethodHandler> handlers = new ArrayList<LoginMethodHandler>();
final LoginBehavior behavior = request.getLoginBehavior();
if (behavior.allowsGetTokenAuth()) {
handlers.add(new GetTokenLoginMethodHandler(this));
}
if (behavior.allowsKatanaAuth()) {
handlers.add(new KatanaProxyLoginMethodHandler(this));
}
if (behavior.allowsFacebookLiteAuth()) {
handlers.add(new FacebookLiteLoginMethodHandler(this));
}
if (behavior.allowsCustomTabAuth()) {
handlers.add(new CustomTabLoginMethodHandler(this));
}
if (behavior.allowsWebViewAuth()) {
handlers.add(new WebViewLoginMethodHandler(this));
}
if (behavior.allowsDeviceAuth()) {
handlers.add(new DeviceAuthMethodHandler(this));
}
LoginMethodHandler [] result = new LoginMethodHandler[handlers.size()];
handlers.toArray(result);
return result;
}
boolean checkInternetPermission() {
if (checkedInternetPermission) {
return true;
}
int permissionCheck = checkPermission(Manifest.permission.INTERNET);
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
Activity activity = getActivity();
String errorType = activity.getString(R.string.com_facebook_internet_permission_error_title);
String errorDescription = activity.getString(R.string.com_facebook_internet_permission_error_message);
complete(Result.createErrorResult(pendingRequest, errorType, errorDescription));
return false;
}
checkedInternetPermission = true;
return true;
}
void tryNextHandler() {
if (currentHandler >= 0) {
logAuthorizationMethodComplete(
getCurrentHandler().getNameForLogging(),
LoginLogger.EVENT_PARAM_METHOD_RESULT_SKIPPED,
null,
null,
getCurrentHandler().methodLoggingExtras);
}
while (handlersToTry != null && currentHandler < (handlersToTry.length - 1)) {
currentHandler++;
boolean started = tryCurrentHandler();
if (started) {
return;
}
}
if (pendingRequest != null) {
// We went through all handlers without successfully attempting an auth.
completeWithFailure();
}
}
private void completeWithFailure() {
complete(Result.createErrorResult(pendingRequest, "Login attempt failed.", null));
}
private void addLoggingExtra(String key, String value, boolean accumulate) {
if (loggingExtras == null) {
loggingExtras = new HashMap<String, String>();
}
if (loggingExtras.containsKey(key) && accumulate) {
value = loggingExtras.get(key) + "," + value;
}
loggingExtras.put(key, value);
}
boolean tryCurrentHandler() {
LoginMethodHandler handler = getCurrentHandler();
if (handler.needsInternetPermission() && !checkInternetPermission()) {
addLoggingExtra(
LoginLogger.EVENT_EXTRAS_MISSING_INTERNET_PERMISSION,
AppEventsConstants.EVENT_PARAM_VALUE_YES,
false
);
return false;
}
boolean tried = handler.tryAuthorize(pendingRequest);
if (tried) {
getLogger().logAuthorizationMethodStart(pendingRequest.getAuthId(),
handler.getNameForLogging());
} else {
// We didn't try it, so we don't get any other completion
// notification -- log that we skipped it.
getLogger().logAuthorizationMethodNotTried(pendingRequest.getAuthId(),
handler.getNameForLogging());
addLoggingExtra(
LoginLogger.EVENT_EXTRAS_NOT_TRIED,
handler.getNameForLogging(),
true
);
}
return tried;
}
void completeAndValidate(Result outcome) {
// Do we need to validate a successful result (as in the case of a reauth)?
if (outcome.token != null && AccessToken.getCurrentAccessToken() != null) {
validateSameFbidAndFinish(outcome);
} else {
// We're done, just notify the listener.
complete(outcome);
}
}
void complete(Result outcome) {
LoginMethodHandler handler = getCurrentHandler();
// This might be null if, for some reason, none of the handlers were successfully tried
// (in which case we already logged that).
if (handler != null) {
logAuthorizationMethodComplete(handler.getNameForLogging(), outcome,
handler.methodLoggingExtras);
}
if (loggingExtras != null) {
// Pass this back to the caller for logging at the aggregate level.
outcome.loggingExtras = loggingExtras;
}
handlersToTry = null;
currentHandler = -1;
pendingRequest = null;
loggingExtras = null;
notifyOnCompleteListener(outcome);
}
OnCompletedListener getOnCompletedListener() {
return onCompletedListener;
}
void setOnCompletedListener(OnCompletedListener onCompletedListener) {
this.onCompletedListener = onCompletedListener;
}
BackgroundProcessingListener getBackgroundProcessingListener() {
return backgroundProcessingListener;
}
void setBackgroundProcessingListener(
BackgroundProcessingListener backgroundProcessingListener) {
this.backgroundProcessingListener = backgroundProcessingListener;
}
int checkPermission(String permission) {
return getActivity().checkCallingOrSelfPermission(permission);
}
void validateSameFbidAndFinish(Result pendingResult) {
if (pendingResult.token == null) {
throw new FacebookException("Can't validate without a token");
}
AccessToken previousToken = AccessToken.getCurrentAccessToken();
AccessToken newToken = pendingResult.token;
try {
Result result;
if (previousToken != null && newToken != null &&
previousToken.getUserId().equals(newToken.getUserId())) {
result = Result.createTokenResult(pendingRequest, pendingResult.token);
} else {
result = Result
.createErrorResult(
pendingRequest,
"User logged in as different Facebook user.",
null);
}
complete(result);
} catch (Exception ex) {
complete(Result.createErrorResult(
pendingRequest,
"Caught exception",
ex.getMessage()));
}
}
private static AccessToken createFromTokenWithRefreshedPermissions(
AccessToken token,
Collection<String> grantedPermissions,
Collection<String> declinedPermissions) {
return new AccessToken(
token.getToken(),
token.getApplicationId(),
token.getUserId(),
grantedPermissions,
declinedPermissions,
token.getSource(),
token.getExpires(),
token.getLastRefresh());
}
private LoginLogger getLogger() {
if (loginLogger == null ||
!loginLogger.getApplicationId().equals(pendingRequest.getApplicationId())) {
loginLogger = new LoginLogger(getActivity(), pendingRequest.getApplicationId());
}
return loginLogger;
}
private void notifyOnCompleteListener(Result outcome) {
if (onCompletedListener != null) {
onCompletedListener.onCompleted(outcome);
}
}
void notifyBackgroundProcessingStart() {
if (backgroundProcessingListener != null) {
backgroundProcessingListener.onBackgroundProcessingStarted();
}
}
void notifyBackgroundProcessingStop() {
if (backgroundProcessingListener != null) {
backgroundProcessingListener.onBackgroundProcessingStopped();
}
}
private void logAuthorizationMethodComplete(
String method,
Result result,
Map<String, String> loggingExtras) {
logAuthorizationMethodComplete(method,
result.code.getLoggingValue(),
result.errorMessage,
result.errorCode,
loggingExtras);
}
private void logAuthorizationMethodComplete(
String method,
String result,
String errorMessage,
String errorCode,
Map<String, String> loggingExtras) {
if (pendingRequest == null) {
// We don't expect this to happen, but if it does, log an event for diagnostic purposes.
getLogger().logUnexpectedError(
LoginLogger.EVENT_NAME_LOGIN_METHOD_COMPLETE,
"Unexpected call to logCompleteLogin with null pendingAuthorizationRequest.",
method
);
} else {
getLogger().logAuthorizationMethodComplete(pendingRequest.getAuthId(), method, result,
errorMessage, errorCode, loggingExtras);
}
}
static String getE2E() {
JSONObject e2e = new JSONObject();
try {
e2e.put("init", System.currentTimeMillis());
} catch (JSONException e) {
}
return e2e.toString();
}
public static class Request implements Parcelable {
private final LoginBehavior loginBehavior;
private Set<String> permissions;
private final DefaultAudience defaultAudience;
private final String applicationId;
private final String authId;
private boolean isRerequest = false;
private String deviceRedirectUriString;
Request(
LoginBehavior loginBehavior,
Set<String> permissions,
DefaultAudience defaultAudience,
String applicationId,
String authId) {
this.loginBehavior = loginBehavior;
this.permissions = permissions != null ? permissions : new HashSet<String>();
this.defaultAudience = defaultAudience;
this.applicationId = applicationId;
this.authId = authId;
}
Set<String> getPermissions() {
return permissions;
}
void setPermissions(Set<String> permissions) {
Validate.notNull(permissions, "permissions");
this.permissions = permissions;
}
LoginBehavior getLoginBehavior() {
return loginBehavior;
}
DefaultAudience getDefaultAudience() {
return defaultAudience;
}
String getApplicationId() {
return applicationId;
}
String getAuthId() {
return authId;
}
boolean isRerequest() {
return isRerequest;
}
void setRerequest(boolean isRerequest) {
this.isRerequest = isRerequest;
}
String getDeviceRedirectUriString() {
return this.deviceRedirectUriString;
}
void setDeviceRedirectUriString(String deviceRedirectUriString) {
this.deviceRedirectUriString = deviceRedirectUriString;
}
boolean hasPublishPermission() {
for (String permission : permissions) {
if (LoginManager.isPublishPermission(permission)) {
return true;
}
}
return false;
}
private Request(Parcel parcel) {
String enumValue = parcel.readString();
this.loginBehavior = enumValue != null ? LoginBehavior.valueOf(enumValue) : null;
ArrayList<String> permissionsList = new ArrayList<>();
parcel.readStringList(permissionsList);
this.permissions = new HashSet<String>(permissionsList);
enumValue = parcel.readString();
this.defaultAudience = enumValue != null ? DefaultAudience.valueOf(enumValue) : null;
this.applicationId = parcel.readString();
this.authId = parcel.readString();
this.isRerequest = parcel.readByte() != 0;
this.deviceRedirectUriString = parcel.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(loginBehavior != null ? loginBehavior.name() : null);
dest.writeStringList(new ArrayList<String>(permissions));
dest.writeString(defaultAudience != null ? defaultAudience.name() : null);
dest.writeString(applicationId);
dest.writeString(authId);
dest.writeByte((byte)(isRerequest ? 1 : 0));
dest.writeString(deviceRedirectUriString);
}
public static final Parcelable.Creator<Request> CREATOR = new Parcelable.Creator() {
@Override
public Request createFromParcel(Parcel source) {
return new Request(source);
}
@Override
public Request[] newArray(int size) {
return new Request[size];
}
};
}
public static class Result implements Parcelable {
enum Code {
SUCCESS("success"),
CANCEL("cancel"),
ERROR("error");
private final String loggingValue;
Code(String loggingValue) {
this.loggingValue = loggingValue;
}
// For consistency across platforms, we want to use specific string values when logging
// these results.
String getLoggingValue() {
return loggingValue;
}
}
final Code code;
final AccessToken token;
final String errorMessage;
final String errorCode;
final Request request;
public Map<String, String> loggingExtras;
Result(
Request request,
Code code,
AccessToken token,
String errorMessage,
String errorCode) {
Validate.notNull(code, "code");
this.request = request;
this.token = token;
this.errorMessage = errorMessage;
this.code = code;
this.errorCode = errorCode;
}
static Result createTokenResult(Request request, AccessToken token) {
return new Result(request, Code.SUCCESS, token, null, null);
}
static Result createCancelResult(Request request, String message) {
return new Result(request, Code.CANCEL, null, message, null);
}
static Result createErrorResult(
Request request,
String errorType,
String errorDescription) {
return createErrorResult(request, errorType, errorDescription, null);
}
static Result createErrorResult(
Request request,
String errorType,
String errorDescription,
String errorCode) {
String message = TextUtils.join(
": ",
Utility.asListNoNulls(errorType, errorDescription));
return new Result(request, Code.ERROR, null, message, errorCode);
}
private Result(Parcel parcel) {
this.code = Code.valueOf(parcel.readString());
this.token = parcel.readParcelable(AccessToken.class.getClassLoader());
this.errorMessage = parcel.readString();
this.errorCode = parcel.readString();
this.request = parcel.readParcelable(Request.class.getClassLoader());
this.loggingExtras = Utility.readStringMapFromParcel(parcel);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(code.name());
dest.writeParcelable(token, flags);
dest.writeString(errorMessage);
dest.writeString(errorCode);
dest.writeParcelable(request, flags);
Utility.writeStringMapToParcel(dest, loggingExtras);
}
public static final Parcelable.Creator<Result> CREATOR = new Parcelable.Creator() {
@Override
public Result createFromParcel(Parcel source) {
return new Result(source);
}
@Override
public Result[] newArray(int size) {
return new Result[size];
}
};
}
// Parcelable implementation
public LoginClient(Parcel source) {
Object [] o = source.readParcelableArray(LoginMethodHandler.class.getClassLoader());
handlersToTry = new LoginMethodHandler[o.length];
for (int i = 0; i < o.length; ++i) {
handlersToTry[i] = (LoginMethodHandler) o[i];
handlersToTry[i].setLoginClient(this);
}
currentHandler = source.readInt();
pendingRequest = source.readParcelable(Request.class.getClassLoader());
loggingExtras = Utility.readStringMapFromParcel(source);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelableArray(handlersToTry, flags);
dest.writeInt(currentHandler);
dest.writeParcelable(pendingRequest, flags);
Utility.writeStringMapToParcel(dest, loggingExtras);
}
public static final Parcelable.Creator<LoginClient> CREATOR = new Parcelable.Creator() {
@Override
public LoginClient createFromParcel(Parcel source) {
return new LoginClient(source);
}
@Override
public LoginClient[] newArray(int size) {
return new LoginClient[size];
}
};
}