/**
 * 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.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import android.webkit.CookieSyncManager;
import com.facebook.android.R;
import com.facebook.internal.AnalyticsEvents;
import com.facebook.internal.NativeProtocol;
import com.facebook.internal.ServerProtocol;
import com.facebook.internal.Utility;
import com.facebook.model.GraphUser;
import com.facebook.widget.WebDialog;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.Serializable;
import java.util.*;

class AuthorizationClient implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final String TAG = "Facebook-AuthorizationClient";
    private static final String WEB_VIEW_AUTH_HANDLER_STORE =
            "com.facebook.AuthorizationClient.WebViewAuthHandler.TOKEN_STORE_KEY";
    private static final String WEB_VIEW_AUTH_HANDLER_TOKEN_KEY = "TOKEN";

    // Constants for logging login-related data. Some of these are only used by Session, but grouped here for
    // maintainability.
    private static final String EVENT_NAME_LOGIN_METHOD_START = "fb_mobile_login_method_start";
    private static final String EVENT_NAME_LOGIN_METHOD_COMPLETE = "fb_mobile_login_method_complete";
    private static final String EVENT_PARAM_METHOD_RESULT_SKIPPED = "skipped";
    static final String EVENT_NAME_LOGIN_START = "fb_mobile_login_start";
    static final String EVENT_NAME_LOGIN_COMPLETE = "fb_mobile_login_complete";
    // Note: to ensure stability of column mappings across the four different event types, we prepend a column
    // index to each name, and we log all columns with all events, even if they are empty.
    static final String EVENT_PARAM_AUTH_LOGGER_ID = "0_auth_logger_id";
    static final String EVENT_PARAM_TIMESTAMP = "1_timestamp_ms";
    static final String EVENT_PARAM_LOGIN_RESULT = "2_result";
    static final String EVENT_PARAM_METHOD = "3_method";
    static final String EVENT_PARAM_ERROR_CODE = "4_error_code";
    static final String EVENT_PARAM_ERROR_MESSAGE = "5_error_message";
    static final String EVENT_PARAM_EXTRAS = "6_extras";
    static final String EVENT_EXTRAS_TRY_LOGIN_ACTIVITY = "try_login_activity";
    static final String EVENT_EXTRAS_TRY_LEGACY = "try_legacy";
    static final String EVENT_EXTRAS_LOGIN_BEHAVIOR = "login_behavior";
    static final String EVENT_EXTRAS_REQUEST_CODE = "request_code";
    static final String EVENT_EXTRAS_IS_LEGACY = "is_legacy";
    static final String EVENT_EXTRAS_PERMISSIONS = "permissions";
    static final String EVENT_EXTRAS_DEFAULT_AUDIENCE = "default_audience";
    static final String EVENT_EXTRAS_MISSING_INTERNET_PERMISSION = "no_internet_permission";
    static final String EVENT_EXTRAS_NOT_TRIED = "not_tried";
    static final String EVENT_EXTRAS_NEW_PERMISSIONS = "new_permissions";
    static final String EVENT_EXTRAS_SERVICE_DISABLED = "service_disabled";
    static final String EVENT_EXTRAS_APP_CALL_ID = "call_id";
    static final String EVENT_EXTRAS_PROTOCOL_VERSION = "protocol_version";
    static final String EVENT_EXTRAS_WRITE_PRIVACY = "write_privacy";

    List<AuthHandler> handlersToTry;
    AuthHandler currentHandler;
    transient Context context;
    transient StartActivityDelegate startActivityDelegate;
    transient OnCompletedListener onCompletedListener;
    transient BackgroundProcessingListener backgroundProcessingListener;
    transient boolean checkedInternetPermission;
    AuthorizationRequest pendingRequest;
    Map<String, String> loggingExtras;
    private transient AppEventsLogger appEventsLogger;

    interface OnCompletedListener {
        void onCompleted(Result result);
    }

    interface BackgroundProcessingListener {
        void onBackgroundProcessingStarted();

        void onBackgroundProcessingStopped();
    }

    interface StartActivityDelegate {
        public void startActivityForResult(Intent intent, int requestCode);

        public Activity getActivityContext();
    }

    void setContext(final Context context) {
        this.context = context;
        // We rely on individual requests to tell us how to start an activity.
        startActivityDelegate = null;
    }

    void setContext(final Activity activity) {
        this.context = activity;

        // If we are used in the context of an activity, we will always use that activity to
        // call startActivityForResult.
        startActivityDelegate = new StartActivityDelegate() {
            @Override
            public void startActivityForResult(Intent intent, int requestCode) {
                activity.startActivityForResult(intent, requestCode);
            }

            @Override
            public Activity getActivityContext() {
                return activity;
            }
        };
    }

    void startOrContinueAuth(AuthorizationRequest request) {
        if (getInProgress()) {
            continueAuth();
        } else {
            authorize(request);
        }
    }

    void authorize(AuthorizationRequest request) {
        if (request == null) {
            return;
        }

        if (pendingRequest != null) {
            throw new FacebookException("Attempted to authorize while a request is pending.");
        }

        if (request.needsNewTokenValidation() && !checkInternetPermission()) {
            // We're going to need INTERNET permission later and don't have it, so fail early.
            return;
        }
        pendingRequest = request;
        handlersToTry = getHandlerTypes(request);
        tryNextHandler();
    }

    void continueAuth() {
        if (pendingRequest == null || currentHandler == null) {
            throw new FacebookException("Attempted to continue authorization without a pending request.");
        }

        if (currentHandler.needsRestart()) {
            currentHandler.cancel();
            tryCurrentHandler();
        }
    }

    boolean getInProgress() {
        return pendingRequest != null && currentHandler != null;
    }

    void cancelCurrentHandler() {
        if (currentHandler != null) {
            currentHandler.cancel();
        }
    }

    boolean onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == pendingRequest.getRequestCode()) {
            return currentHandler.onActivityResult(requestCode, resultCode, data);
        }
        return false;
    }

    private List<AuthHandler> getHandlerTypes(AuthorizationRequest request) {
        ArrayList<AuthHandler> handlers = new ArrayList<AuthHandler>();

        final SessionLoginBehavior behavior = request.getLoginBehavior();
        if (behavior.allowsKatanaAuth()) {
            if (!request.isLegacy()) {
                handlers.add(new GetTokenAuthHandler());
            }
            handlers.add(new KatanaProxyAuthHandler());
        }

        if (behavior.allowsWebViewAuth()) {
            handlers.add(new WebViewAuthHandler());
        }

        return handlers;
    }

    boolean checkInternetPermission() {
        if (checkedInternetPermission) {
            return true;
        }

        int permissionCheck = checkPermission(Manifest.permission.INTERNET);
        if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
            String errorType = context.getString(R.string.com_facebook_internet_permission_error_title);
            String errorDescription = context.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 != null) {
            logAuthorizationMethodComplete(currentHandler.getNameForLogging(), EVENT_PARAM_METHOD_RESULT_SKIPPED,
                    null, null, currentHandler.methodLoggingExtras);
        }

        while (handlersToTry != null && !handlersToTry.isEmpty()) {
            currentHandler = handlersToTry.remove(0);

            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() {
        if (currentHandler.needsInternetPermission() && !checkInternetPermission()) {
            addLoggingExtra(EVENT_EXTRAS_MISSING_INTERNET_PERMISSION, AppEventsConstants.EVENT_PARAM_VALUE_YES,
                    false);
            return false;
        }

        boolean tried = currentHandler.tryAuthorize(pendingRequest);
        if (tried) {
            logAuthorizationMethodStart(currentHandler.getNameForLogging());
        } else {
            // We didn't try it, so we don't get any other completion notification -- log that we skipped it.
            addLoggingExtra(EVENT_EXTRAS_NOT_TRIED, currentHandler.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 && pendingRequest.needsNewTokenValidation()) {
            validateSameFbidAndFinish(outcome);
        } else {
            // We're done, just notify the listener.
            complete(outcome);
        }
    }

    void complete(Result outcome) {
        // This might be null if, for some reason, none of the handlers were successfully tried (in which case
        // we already logged that).
        if (currentHandler != null) {
            logAuthorizationMethodComplete(currentHandler.getNameForLogging(), outcome,
                    currentHandler.methodLoggingExtras);
        }

        if (loggingExtras != null) {
            // Pass this back to the caller for logging at the aggregate level.
            outcome.loggingExtras = loggingExtras;
        }

        handlersToTry = null;
        currentHandler = null;
        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;
    }

    StartActivityDelegate getStartActivityDelegate() {
        if (startActivityDelegate != null) {
            return startActivityDelegate;
        } else if (pendingRequest != null) {
            // Wrap the request's delegate in our own.
            return new StartActivityDelegate() {
                @Override
                public void startActivityForResult(Intent intent, int requestCode) {
                    pendingRequest.getStartActivityDelegate().startActivityForResult(intent, requestCode);
                }

                @Override
                public Activity getActivityContext() {
                    return pendingRequest.getStartActivityDelegate().getActivityContext();
                }
            };
        }
        return null;
    }

    int checkPermission(String permission) {
        return context.checkCallingOrSelfPermission(permission);
    }

    void validateSameFbidAndFinish(Result pendingResult) {
        if (pendingResult.token == null) {
            throw new FacebookException("Can't validate without a token");
        }

        RequestBatch batch = createReauthValidationBatch(pendingResult);

        notifyBackgroundProcessingStart();

        batch.executeAsync();
    }

    RequestBatch createReauthValidationBatch(final Result pendingResult) {
        // We need to ensure that the token we got represents the same fbid as the old one. We issue
        // a "me" request using the current token, a "me" request using the new token, and a "me/permissions"
        // request using the current token to get the permissions of the user.

        final ArrayList<String> fbids = new ArrayList<String>();
        final ArrayList<String> tokenPermissions = new ArrayList<String>();
        final String newToken = pendingResult.token.getToken();

        Request.Callback meCallback = new Request.Callback() {
            @Override
            public void onCompleted(Response response) {
                try {
                    GraphUser user = response.getGraphObjectAs(GraphUser.class);
                    if (user != null) {
                        fbids.add(user.getId());
                    }
                } catch (Exception ex) {
                }
            }
        };

        String validateSameFbidAsToken = pendingRequest.getPreviousAccessToken();
        Request requestCurrentTokenMe = createGetProfileIdRequest(validateSameFbidAsToken);
        requestCurrentTokenMe.setCallback(meCallback);

        Request requestNewTokenMe = createGetProfileIdRequest(newToken);
        requestNewTokenMe.setCallback(meCallback);

        Request requestCurrentTokenPermissions = createGetPermissionsRequest(validateSameFbidAsToken);
        requestCurrentTokenPermissions.setCallback(new Request.Callback() {
            @Override
            public void onCompleted(Response response) {
                try {
                    List<String> permissions = Session.handlePermissionResponse(null, response);
                    if (permissions != null) {
                        tokenPermissions.addAll(permissions);
                    }
                } catch (Exception ex) {
                }
            }
        });

        RequestBatch batch = new RequestBatch(requestCurrentTokenMe, requestNewTokenMe,
                requestCurrentTokenPermissions);
        batch.setBatchApplicationId(pendingRequest.getApplicationId());
        batch.addCallback(new RequestBatch.Callback() {
            @Override
            public void onBatchCompleted(RequestBatch batch) {
                try {
                    Result result = null;
                    if (fbids.size() == 2 && fbids.get(0) != null && fbids.get(1) != null &&
                            fbids.get(0).equals(fbids.get(1))) {
                        // Modify the token to have the right permission set.
                        AccessToken tokenWithPermissions = AccessToken
                                .createFromTokenWithRefreshedPermissions(pendingResult.token,
                                        tokenPermissions);
                        result = Result.createTokenResult(pendingRequest, tokenWithPermissions);
                    } 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()));
                } finally {
                    notifyBackgroundProcessingStop();
                }
            }
        });

        return batch;
    }

    Request createGetPermissionsRequest(String accessToken) {
        Bundle parameters = new Bundle();
        parameters.putString("access_token", accessToken);
        return new Request(null, "me/permissions", parameters, HttpMethod.GET, null);
    }

    Request createGetProfileIdRequest(String accessToken) {
        Bundle parameters = new Bundle();
        parameters.putString("fields", "id");
        parameters.putString("access_token", accessToken);
        return new Request(null, "me", parameters, HttpMethod.GET, null);
    }

    private AppEventsLogger getAppEventsLogger() {
        if (appEventsLogger == null || appEventsLogger.getApplicationId() != pendingRequest.getApplicationId()) {
            appEventsLogger = AppEventsLogger.newLogger(context, pendingRequest.getApplicationId());
        }
        return appEventsLogger;
    }

    private void notifyOnCompleteListener(Result outcome) {
        if (onCompletedListener != null) {
            onCompletedListener.onCompleted(outcome);
        }
    }

    private void notifyBackgroundProcessingStart() {
        if (backgroundProcessingListener != null) {
            backgroundProcessingListener.onBackgroundProcessingStarted();
        }
    }

    private void notifyBackgroundProcessingStop() {
        if (backgroundProcessingListener != null) {
            backgroundProcessingListener.onBackgroundProcessingStopped();
        }
    }

    private void logAuthorizationMethodStart(String method) {
        Bundle bundle = newAuthorizationLoggingBundle(pendingRequest.getAuthId());
        bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis());
        bundle.putString(EVENT_PARAM_METHOD, method);

        getAppEventsLogger().logSdkEvent(EVENT_NAME_LOGIN_METHOD_START, null, bundle);
    }

    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) {
        Bundle bundle = null;
        if (pendingRequest == null) {
            // We don't expect this to happen, but if it does, log an event for diagnostic purposes.
            bundle = newAuthorizationLoggingBundle("");
            bundle.putString(EVENT_PARAM_LOGIN_RESULT, Result.Code.ERROR.getLoggingValue());
            bundle.putString(EVENT_PARAM_ERROR_MESSAGE,
                    "Unexpected call to logAuthorizationMethodComplete with null pendingRequest.");
        } else {
            bundle = newAuthorizationLoggingBundle(pendingRequest.getAuthId());
            if (result != null) {
                bundle.putString(EVENT_PARAM_LOGIN_RESULT, result);
            }
            if (errorMessage != null) {
                bundle.putString(EVENT_PARAM_ERROR_MESSAGE, errorMessage);
            }
            if (errorCode != null) {
                bundle.putString(EVENT_PARAM_ERROR_CODE, errorCode);
            }
            if (loggingExtras != null && !loggingExtras.isEmpty()) {
                JSONObject jsonObject = new JSONObject(loggingExtras);
                bundle.putString(EVENT_PARAM_EXTRAS, jsonObject.toString());
            }
        }
        bundle.putString(EVENT_PARAM_METHOD, method);
        bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis());

        getAppEventsLogger().logSdkEvent(EVENT_NAME_LOGIN_METHOD_COMPLETE, null, bundle);
    }

    static Bundle newAuthorizationLoggingBundle(String authLoggerId) {
        // We want to log all parameters for all events, to ensure stability of columns across different event types.
        Bundle bundle = new Bundle();
        bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis());
        bundle.putString(EVENT_PARAM_AUTH_LOGGER_ID, authLoggerId);
        bundle.putString(EVENT_PARAM_METHOD, "");
        bundle.putString(EVENT_PARAM_LOGIN_RESULT, "");
        bundle.putString(EVENT_PARAM_ERROR_MESSAGE, "");
        bundle.putString(EVENT_PARAM_ERROR_CODE, "");
        bundle.putString(EVENT_PARAM_EXTRAS, "");
        return bundle;
    }

    abstract class AuthHandler implements Serializable {
        private static final long serialVersionUID = 1L;

        Map<String, String> methodLoggingExtras;

        abstract boolean tryAuthorize(AuthorizationRequest request);
        abstract String getNameForLogging();

        boolean onActivityResult(int requestCode, int resultCode, Intent data) {
            return false;
        }

        boolean needsRestart() {
            return false;
        }

        boolean needsInternetPermission() {
            return false;
        }

        void cancel() {
        }

        protected void addLoggingExtra(String key, Object value) {
            if (methodLoggingExtras == null) {
                methodLoggingExtras = new HashMap<String, String>();
            }
            methodLoggingExtras.put(key, value == null ? null : value.toString());
        }
    }

    class WebViewAuthHandler extends AuthHandler {
        private static final long serialVersionUID = 1L;
        private transient WebDialog loginDialog;
        private String applicationId;
        private String e2e;

        @Override
        String getNameForLogging() {
            return "web_view";
        }

        @Override
        boolean needsRestart() {
            // Because we are presenting WebView UI within the current context, we need to explicitly
            // restart the process if the context goes away and is recreated.
            return true;
        }

        @Override
        boolean needsInternetPermission() {
            return true;
        }

        @Override
        void cancel() {
            if (loginDialog != null) {
                loginDialog.dismiss();
                loginDialog = null;
            }
        }

        @Override
        boolean tryAuthorize(final AuthorizationRequest request) {
            applicationId = request.getApplicationId();
            Bundle parameters = new Bundle();
            if (!Utility.isNullOrEmpty(request.getPermissions())) {
                String scope = TextUtils.join(",", request.getPermissions());
                parameters.putString(ServerProtocol.DIALOG_PARAM_SCOPE, scope);
                addLoggingExtra(ServerProtocol.DIALOG_PARAM_SCOPE, scope);
            }

            String previousToken = request.getPreviousAccessToken();
            if (!Utility.isNullOrEmpty(previousToken) && (previousToken.equals(loadCookieToken()))) {
                parameters.putString(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, previousToken);
                // Don't log the actual access token, just its presence or absence.
                addLoggingExtra(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, AppEventsConstants.EVENT_PARAM_VALUE_YES);
            } else {
                // The call to clear cookies will create the first instance of CookieSyncManager if necessary
                Utility.clearFacebookCookies(context);
                addLoggingExtra(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, AppEventsConstants.EVENT_PARAM_VALUE_NO);
            }

            WebDialog.OnCompleteListener listener = new WebDialog.OnCompleteListener() {
                @Override
                public void onComplete(Bundle values, FacebookException error) {
                    onWebDialogComplete(request, values, error);
                }
            };

            e2e = getE2E();
            addLoggingExtra(ServerProtocol.DIALOG_PARAM_E2E, e2e);

            WebDialog.Builder builder =
                    new AuthDialogBuilder(getStartActivityDelegate().getActivityContext(), applicationId, parameters)
                            .setE2E(e2e)
                            .setIsRerequest(request.isRerequest())
                            .setOnCompleteListener(listener);
            loginDialog = builder.build();
            loginDialog.show();

            return true;
        }

        void onWebDialogComplete(AuthorizationRequest request, Bundle values,
                FacebookException error) {
            Result outcome;
            if (values != null) {
                // Actual e2e we got from the dialog should be used for logging.
                if (values.containsKey(ServerProtocol.DIALOG_PARAM_E2E)) {
                    e2e = values.getString(ServerProtocol.DIALOG_PARAM_E2E);
                }

                AccessToken token = AccessToken
                        .createFromWebBundle(request.getPermissions(), values, AccessTokenSource.WEB_VIEW);
                outcome = Result.createTokenResult(pendingRequest, token);

                // Ensure any cookies set by the dialog are saved
                // This is to work around a bug where CookieManager may fail to instantiate if CookieSyncManager
                // has never been created.
                CookieSyncManager syncManager = CookieSyncManager.createInstance(context);
                syncManager.sync();
                saveCookieToken(token.getToken());
            } else {
                if (error instanceof FacebookOperationCanceledException) {
                    outcome = Result.createCancelResult(pendingRequest, "User canceled log in.");
                } else {
                    // Something went wrong, don't log a completion event since it will skew timing results.
                    e2e = null;

                    String errorCode = null;
                    String errorMessage = error.getMessage();
                    if (error instanceof FacebookServiceException) {
                        FacebookRequestError requestError = ((FacebookServiceException)error).getRequestError();
                        errorCode = String.format("%d", requestError.getErrorCode());
                        errorMessage = requestError.toString();
                    }
                    outcome = Result.createErrorResult(pendingRequest, null, errorMessage, errorCode);
                }
            }

            if (!Utility.isNullOrEmpty(e2e)) {
                logWebLoginCompleted(applicationId, e2e);
            }

            completeAndValidate(outcome);
        }

        private void saveCookieToken(String token) {
            Context context = getStartActivityDelegate().getActivityContext();
            SharedPreferences sharedPreferences = context.getSharedPreferences(
                    WEB_VIEW_AUTH_HANDLER_STORE,
                    Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.putString(WEB_VIEW_AUTH_HANDLER_TOKEN_KEY, token);
            if (!editor.commit()) {
                Utility.logd(TAG, "Could not update saved web view auth handler token.");
            }
        }

        private String loadCookieToken() {
            Context context = getStartActivityDelegate().getActivityContext();
            SharedPreferences sharedPreferences = context.getSharedPreferences(
                    WEB_VIEW_AUTH_HANDLER_STORE,
                    Context.MODE_PRIVATE);
            return sharedPreferences.getString(WEB_VIEW_AUTH_HANDLER_TOKEN_KEY, "");
        }
    }

    class GetTokenAuthHandler extends AuthHandler {
        private static final long serialVersionUID = 1L;
        private transient GetTokenClient getTokenClient;

        @Override
        String getNameForLogging() {
            return "get_token";
        }

        @Override
        void cancel() {
            if (getTokenClient != null) {
                getTokenClient.cancel();
                getTokenClient = null;
            }
        }

        @Override
        boolean needsRestart() {
            // if the getTokenClient is null, that means an orientation change has occurred, and we need
            // to recreate the GetTokenClient, so return true to indicate we need a restart
            return getTokenClient == null;
        }

        boolean tryAuthorize(final AuthorizationRequest request) {
            getTokenClient = new GetTokenClient(context, request.getApplicationId());
            if (!getTokenClient.start()) {
                return false;
            }

            notifyBackgroundProcessingStart();

            GetTokenClient.CompletedListener callback = new GetTokenClient.CompletedListener() {
                @Override
                public void completed(Bundle result) {
                    getTokenCompleted(request, result);
                }
            };

            getTokenClient.setCompletedListener(callback);
            return true;
        }

        void getTokenCompleted(AuthorizationRequest request, Bundle result) {
            getTokenClient = null;

            notifyBackgroundProcessingStop();

            if (result != null) {
                ArrayList<String> currentPermissions = result.getStringArrayList(NativeProtocol.EXTRA_PERMISSIONS);
                List<String> permissions = request.getPermissions();
                if ((currentPermissions != null) &&
                        ((permissions == null) || currentPermissions.containsAll(permissions))) {
                    // We got all the permissions we needed, so we can complete the auth now.
                    AccessToken token = AccessToken
                            .createFromNativeLogin(result, AccessTokenSource.FACEBOOK_APPLICATION_SERVICE);
                    Result outcome = Result.createTokenResult(pendingRequest, token);
                    completeAndValidate(outcome);
                    return;
                }

                // We didn't get all the permissions we wanted, so update the request with just the permissions
                // we still need.
                List<String> newPermissions = new ArrayList<String>();
                for (String permission : permissions) {
                    if (!currentPermissions.contains(permission)) {
                        newPermissions.add(permission);
                    }
                }
                if (!newPermissions.isEmpty()) {
                    addLoggingExtra(EVENT_EXTRAS_NEW_PERMISSIONS, TextUtils.join(",", newPermissions));
                }

                request.setPermissions(newPermissions);
            }

            tryNextHandler();
        }
    }

    abstract class KatanaAuthHandler extends AuthHandler {
        private static final long serialVersionUID = 1L;

        protected boolean tryIntent(Intent intent, int requestCode) {
            if (intent == null) {
                return false;
            }

            try {
                getStartActivityDelegate().startActivityForResult(intent, requestCode);
            } catch (ActivityNotFoundException e) {
                // We don't expect this to happen, since we've already validated the intent and bailed out before
                // now if it couldn't be resolved.
                return false;
            }

            return true;
        }
    }

    class KatanaProxyAuthHandler extends KatanaAuthHandler {
        private static final long serialVersionUID = 1L;
        private String applicationId;

        @Override
        String getNameForLogging() {
            return "katana_proxy_auth";
        }

        @Override
        boolean tryAuthorize(AuthorizationRequest request) {
            applicationId = request.getApplicationId();

            String e2e = getE2E();
            Intent intent = NativeProtocol.createProxyAuthIntent(context, request.getApplicationId(),
                    request.getPermissions(), e2e, request.isRerequest());

            addLoggingExtra(ServerProtocol.DIALOG_PARAM_E2E, e2e);

            return tryIntent(intent, request.getRequestCode());
        }

        @Override
        boolean onActivityResult(int requestCode, int resultCode, Intent data) {
            // Handle stuff
            Result outcome;

            if (data == null) {
                // This happens if the user presses 'Back'.
                outcome = Result.createCancelResult(pendingRequest, "Operation canceled");
            } else if (resultCode == Activity.RESULT_CANCELED) {
                outcome = Result.createCancelResult(pendingRequest, data.getStringExtra("error"));
            } else if (resultCode != Activity.RESULT_OK) {
                outcome = Result.createErrorResult(pendingRequest, "Unexpected resultCode from authorization.", null);
            } else {
                outcome = handleResultOk(data);
            }

            if (outcome != null) {
                completeAndValidate(outcome);
            } else {
                tryNextHandler();
            }
            return true;
        }

        private Result handleResultOk(Intent data) {
            Bundle extras = data.getExtras();
            String error = extras.getString("error");
            if (error == null) {
                error = extras.getString("error_type");
            }
            String errorCode = extras.getString("error_code");
            String errorMessage = extras.getString("error_message");
            if (errorMessage == null) {
                errorMessage = extras.getString("error_description");
            }

            String e2e = extras.getString(NativeProtocol.FACEBOOK_PROXY_AUTH_E2E_KEY);
            if (!Utility.isNullOrEmpty(e2e)) {
                logWebLoginCompleted(applicationId, e2e);
            }

            if (error == null && errorCode == null && errorMessage == null) {
                AccessToken token = AccessToken.createFromWebBundle(pendingRequest.getPermissions(), extras,
                        AccessTokenSource.FACEBOOK_APPLICATION_WEB);
                return Result.createTokenResult(pendingRequest, token);
            } else if (ServerProtocol.errorsProxyAuthDisabled.contains(error)) {
                return null;
            } else if (ServerProtocol.errorsUserCanceled.contains(error)) {
                return Result.createCancelResult(pendingRequest, null);
            } else {
                return Result.createErrorResult(pendingRequest, error, errorMessage, errorCode);
            }
        }
    }

    private static String getE2E() {
        JSONObject e2e = new JSONObject();
        try {
            e2e.put("init", System.currentTimeMillis());
        } catch (JSONException e) {
        }
        return e2e.toString();
    }

    private void logWebLoginCompleted(String applicationId, String e2e) {
        AppEventsLogger appEventsLogger = AppEventsLogger.newLogger(context, applicationId);

        Bundle parameters = new Bundle();
        parameters.putString(AnalyticsEvents.PARAMETER_WEB_LOGIN_E2E, e2e);
        parameters.putLong(AnalyticsEvents.PARAMETER_WEB_LOGIN_SWITCHBACK_TIME, System.currentTimeMillis());
        parameters.putString(AnalyticsEvents.PARAMETER_APP_ID, applicationId);

        appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_WEB_LOGIN_COMPLETE, null, parameters);
    }

    static class AuthDialogBuilder extends WebDialog.Builder {
        private static final String OAUTH_DIALOG = "oauth";
        static final String REDIRECT_URI = "fbconnect://success";
        private String e2e;
        private boolean isRerequest;

        public AuthDialogBuilder(Context context, String applicationId, Bundle parameters) {
            super(context, applicationId, OAUTH_DIALOG, parameters);
        }

        public AuthDialogBuilder setE2E(String e2e) {
            this.e2e = e2e;
            return this;
        }

        public AuthDialogBuilder setIsRerequest(boolean isRerequest) {
            this.isRerequest = isRerequest;
            return this;
        }

        @Override
        public WebDialog build() {
            Bundle parameters = getParameters();
            parameters.putString(ServerProtocol.DIALOG_PARAM_REDIRECT_URI, REDIRECT_URI);
            parameters.putString(ServerProtocol.DIALOG_PARAM_CLIENT_ID, getApplicationId());
            parameters.putString(ServerProtocol.DIALOG_PARAM_E2E, e2e);
            parameters.putString(ServerProtocol.DIALOG_PARAM_RESPONSE_TYPE, ServerProtocol.DIALOG_RESPONSE_TYPE_TOKEN);
            parameters.putString(ServerProtocol.DIALOG_PARAM_RETURN_SCOPES, ServerProtocol.DIALOG_RETURN_SCOPES_TRUE);

            // Only set the rerequest auth type for non legacy requests
            if (isRerequest && !Settings.getPlatformCompatibilityEnabled()) {
                parameters.putString(ServerProtocol.DIALOG_PARAM_AUTH_TYPE, ServerProtocol.DIALOG_REREQUEST_AUTH_TYPE);
            }

            return new WebDialog(getContext(), OAUTH_DIALOG, parameters, getTheme(), getListener());
        }
    }

    static class AuthorizationRequest implements Serializable {
        private static final long serialVersionUID = 1L;

        private transient final StartActivityDelegate startActivityDelegate;
        private final SessionLoginBehavior loginBehavior;
        private final int requestCode;
        private boolean isLegacy = false;
        private List<String> permissions;
        private final SessionDefaultAudience defaultAudience;
        private final String applicationId;
        private final String previousAccessToken;
        private final String authId;
        private boolean isRerequest = false;

        AuthorizationRequest(SessionLoginBehavior loginBehavior, int requestCode, boolean isLegacy,
                List<String> permissions, SessionDefaultAudience defaultAudience, String applicationId,
                String validateSameFbidAsToken, StartActivityDelegate startActivityDelegate, String authId) {
            this.loginBehavior = loginBehavior;
            this.requestCode = requestCode;
            this.isLegacy = isLegacy;
            this.permissions = permissions;
            this.defaultAudience = defaultAudience;
            this.applicationId = applicationId;
            this.previousAccessToken = validateSameFbidAsToken;
            this.startActivityDelegate = startActivityDelegate;
            this.authId = authId;
        }

        StartActivityDelegate getStartActivityDelegate() {
            return startActivityDelegate;
        }

        List<String> getPermissions() {
            return permissions;
        }

        void setPermissions(List<String> permissions) {
            this.permissions = permissions;
        }

        SessionLoginBehavior getLoginBehavior() {
            return loginBehavior;
        }

        int getRequestCode() {
            return requestCode;
        }

        SessionDefaultAudience getDefaultAudience() {
            return defaultAudience;
        }

        String getApplicationId() {
            return applicationId;
        }

        boolean isLegacy() {
            return isLegacy;
        }

        void setIsLegacy(boolean isLegacy) {
            this.isLegacy = isLegacy;
        }

        String getPreviousAccessToken() {
            return previousAccessToken;
        }

        boolean needsNewTokenValidation() {
            return previousAccessToken != null && !isLegacy;
        }

        String getAuthId() {
            return authId;
        }

        boolean isRerequest() {
            return isRerequest;
        }

        void setRerequest(boolean isRerequest) {
            this.isRerequest = isRerequest;
        }
    }


    static class Result implements Serializable {
        private static final long serialVersionUID = 1L;

        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 AuthorizationRequest request;
        Map<String, String> loggingExtras;

        private Result(AuthorizationRequest request, Code code, AccessToken token, String errorMessage,
                String errorCode) {
            this.request = request;
            this.token = token;
            this.errorMessage = errorMessage;
            this.code = code;
            this.errorCode = errorCode;
        }

        static Result createTokenResult(AuthorizationRequest request, AccessToken token) {
            return new Result(request, Code.SUCCESS, token, null, null);
        }

        static Result createCancelResult(AuthorizationRequest request, String message) {
            return new Result(request, Code.CANCEL, null, message, null);
        }

        static Result createErrorResult(AuthorizationRequest request, String errorType, String errorDescription) {
            return createErrorResult(request, errorType, errorDescription, null);
        }

        static Result createErrorResult(AuthorizationRequest request, String errorType, String errorDescription,
                String errorCode) {
            String message = TextUtils.join(": ", Utility.asListNoNulls(errorType, errorDescription));
            return new Result(request, Code.ERROR, null, message, errorCode);
        }
    }
}