content: /**
   * 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> grantedPermissions = new ArrayList<String>();
          final ArrayList<String> declinedPermissions = 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 {
                      Session.PermissionsPair permissionsPair = Session.handlePermissionResponse(response);
                      if (permissionsPair != null) {
                          grantedPermissions.addAll(permissionsPair.getGrantedPermissions());
                          declinedPermissions.addAll(permissionsPair.getDeclinedPermissions());
                      }
                  } 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,
                                          grantedPermissions, declinedPermissions);
                          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);
          }
      }
  }
  