com.microsoft.aad.adal.AuthenticationContext.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.aad.adal.AuthenticationContext.java

Source

// Copyright  Microsoft Open Technologies, Inc.
//
// All Rights Reserved
//
// 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
//
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
// ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A
// PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT.
//
// See the Apache License, Version 2.0 for the specific language
// governing permissions and limitations under the License.

package com.microsoft.aad.adal;

import java.io.IOException;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.crypto.NoSuchPaddingException;

import com.microsoft.aad.adal.AuthenticationRequest.UserIdentifierType;

import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.content.LocalBroadcastManager;
import android.util.SparseArray;

/**
 * ADAL context to get access token, refresh token, and lookup from cache.
 */
public class AuthenticationContext {

    private static final int EXCLUDE_INDEX = 8;

    private static final String TAG = "AuthenticationContext";

    private Context mContext;

    private String mAuthority;

    private boolean mValidateAuthority;

    private boolean mAuthorityValidated = false;

    private ITokenCacheStore mTokenCacheStore;

    private static final ReentrantReadWriteLock RWL = new ReentrantReadWriteLock();

    private static final Lock READ_LOCK = RWL.readLock();

    private static final Lock WRITE_LOCK = RWL.writeLock();

    /**
     * Delegate map is needed to handle activity recreate without asking
     * developer to handle context instance for config changes.
     */
    static SparseArray<AuthenticationRequestState> mDelegateMap = new SparseArray<AuthenticationRequestState>();

    /**
     * Last set authorization callback.
     */
    private AuthenticationCallback<AuthenticationResult> mAuthorizationCallback;

    /**
     * Instance validation related calls are serviced inside Discovery as a
     * module.
     */
    private IDiscovery mDiscovery = new Discovery();

    /**
     * Web request handler interface to test behaviors.
     */
    private IWebRequestHandler mWebRequest = new WebRequestHandler();

    /**
     * JWS message builder interface to test behaviors.
     */
    private IJWSBuilder mJWSBuilder;

    /**
     * Connection service interface to test different behaviors.
     */
    private IConnectionService mConnectionService = null;

    private IBrokerProxy mBrokerProxy = null;

    /**
     * CorrelationId set by user or generated by ADAL.
     */
    private UUID mRequestCorrelationId = null;

    /**
     * Constructs context to use with known authority to get the token. It uses
     * default cache that stores encrypted tokens.
     * 
     * @param appContext It needs to have handle to the {@link Context} to use
     *            the SharedPreferences as a Default cache storage. It does not
     *            need to be activity.
     * @param authority Authority url to send code and token requests
     * @param validateAuthority validate authority before sending token request
     * @throws NoSuchPaddingException Algorithm padding does not exist in the
     *             device
     * @throws NoSuchAlgorithmException Encryption Algorithm does not exist in
     *             the device. Please see the log record for details.
     */
    public AuthenticationContext(Context appContext, String authority, boolean validateAuthority)
            throws NoSuchAlgorithmException, NoSuchPaddingException {
        // Fixes are required for SDK 16-18
        // The fixes need to be applied before any use of Java Cryptography
        // Architecture primitives. Default cache uses encryption
        PRNGFixes.apply();
        initialize(appContext, authority, new DefaultTokenCacheStore(appContext), validateAuthority, true);
    }

    /**
     * Constructs context to use with known authority to get the token. It uses
     * provided cache.
     * 
     * @param appContext {@link Context}
     * @param authority Authority Url
     * @param validateAuthority true/false for validation
     * @param tokenCacheStore Set to null if you don't want cache.
     */
    public AuthenticationContext(Context appContext, String authority, boolean validateAuthority,
            ITokenCacheStore tokenCacheStore) {
        initialize(appContext, authority, tokenCacheStore, validateAuthority, false);
    }

    /**
     * It will verify the authority and use the given cache. If cache is null,
     * it will not use cache.
     * 
     * @param appContext {@link Context}
     * @param authority Authority Url
     * @param tokenCacheStore Cache {@link ITokenCacheStore} used to store
     *            tokens. Set to null if you don't want cache.
     */
    public AuthenticationContext(Context appContext, String authority, ITokenCacheStore tokenCacheStore) {
        initialize(appContext, authority, tokenCacheStore, true, false);
    }

    private void initialize(Context appContext, String authority, ITokenCacheStore tokenCacheStore,
            boolean validateAuthority, boolean defaultCache) {
        if (appContext == null) {
            throw new IllegalArgumentException("appContext");
        }
        if (authority == null) {
            throw new IllegalArgumentException("authority");
        }
        mBrokerProxy = new BrokerProxy(appContext);
        if (!defaultCache && !mBrokerProxy.canUseLocalCache()) {
            throw new UnsupportedOperationException("Local cache is not supported for broker usage");
        }
        mContext = appContext;
        mConnectionService = new DefaultConnectionService(mContext);
        checkInternetPermission();
        mAuthority = extractAuthority(authority);
        mValidateAuthority = validateAuthority;
        mTokenCacheStore = tokenCacheStore;
        mJWSBuilder = new JWSBuilder();
    }

    /**
     * Returns referenced cache. You can use default cache, which uses
     * SharedPreferences and handles synchronization by itself.
     * 
     * @return ITokenCacheStore Current cache used
     */
    public ITokenCacheStore getCache() {
        if (mBrokerProxy.canSwitchToBroker()) {
            // return cache implementation related to broker so that app can
            // clear tokens for related accounts
            return new ITokenCacheStore() {

                /**
                 * default serial #
                 */
                private static final long serialVersionUID = 1L;

                @Override
                public void setItem(String key, TokenCacheItem item) {
                    throw new UnsupportedOperationException(
                            "Broker cache does not support direct setItem operation");
                }

                @Override
                public void removeItem(String key) {
                    throw new UnsupportedOperationException(
                            "Broker cache does not support direct removeItem operation");
                }

                @Override
                public void removeAll() {
                    mBrokerProxy.removeAccounts();
                }

                @Override
                public TokenCacheItem getItem(String key) {
                    throw new UnsupportedOperationException(
                            "Broker cache does not support direct getItem operation");
                }

                @Override
                public boolean contains(String key) {
                    throw new UnsupportedOperationException("Broker cache does not support contains operation");
                }
            };
        }
        return mTokenCacheStore;
    }

    /**
     * Gets authority that is used for this object of AuthenticationContext.
     * 
     * @return Authority
     */
    public String getAuthority() {
        return mAuthority;
    }

    /**
     * @return True when authority is valid
     */
    public boolean getValidateAuthority() {
        return mValidateAuthority;
    }

    /**
     * Gets username for current broker user.
     * 
     * @return Username
     */
    public String getBrokerUser() {
        if (mBrokerProxy != null) {
            return mBrokerProxy.getCurrentUser();
        }

        return null;
    }

    /*
     * Gets user info from broker. This should not be called on main thread.
     * @return user {@link UserInfo}
     * 
     * @throws IOException
     * @throws AuthenticatorException
     * @throws OperationCanceledException
     */
    public UserInfo[] getBrokerUsers() throws OperationCanceledException, AuthenticatorException, IOException {
        return mBrokerProxy != null ? mBrokerProxy.getBrokerUsers() : null;
    }

    /**
     * Get expected redirect Uri for your app to use in broker. You need to
     * register this redirectUri in order to get token from Broker.
     * 
     * @return RedirectUri string to use for broker requests.
     */
    public String getRedirectUriForBroker() {
        PackageHelper helper = new PackageHelper(mContext);
        String packageName = mContext.getPackageName();

        // First available signature. Applications can be signed with multiple
        // signatures.
        String signatureDigest = helper.getCurrentSignatureForPackage(packageName);
        String redirectUri = PackageHelper.getBrokerRedirectUrl(packageName, signatureDigest);
        Logger.v(TAG, "Broker redirectUri:" + redirectUri + " packagename:" + packageName + " signatureDigest:"
                + signatureDigest);
        return redirectUri;
    }

    /**
     * acquire Token will start interactive flow if needed. It checks the cache
     * to return existing result if not expired. It tries to use refresh token
     * if available. If it fails to get token with refresh token, it will remove
     * this refresh token from cache and start authentication.
     * 
     * @param activity required to launch authentication activity.
     * @param resource required resource identifier.
     * @param clientId required client identifier
     * @param redirectUri Optional. It will use package name info if not
     *            provided.
     * @param loginHint Optional login hint
     * @param callback required
     */
    public void acquireToken(Activity activity, String resource, String clientId, String redirectUri,
            String loginHint, AuthenticationCallback<AuthenticationResult> callback) {

        redirectUri = checkInputParameters(resource, clientId, redirectUri, PromptBehavior.Auto, callback);

        final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri,
                loginHint, PromptBehavior.Auto, null, getRequestCorrelationId());
        request.setUserIdentifierType(UserIdentifierType.LoginHint);
        acquireTokenLocal(wrapActivity(activity), false, request, callback);
    }

    /**
     * acquire Token will start interactive flow if needed. It checks the cache
     * to return existing result if not expired. It tries to use the refresh
     * token if available. If it fails to get token with refresh token, it will
     * remove this refresh token from cache and fall back on the UI.
     * 
     * @param activity Calling activity
     * @param resource required resource identifier.
     * @param clientId required client identifier
     * @param redirectUri Optional. It will use packagename and provided suffix
     *            for this.
     * @param loginHint Optional. This parameter will be used to pre-populate
     *            the username field in the authentication form. Please note
     *            that the end user can still edit the username field and
     *            authenticate as a different user. This parameter can be null.
     * @param extraQueryParameters Optional. This parameter will be appended as
     *            is to the query string in the HTTP authentication request to
     *            the authority. The parameter can be null.
     * @param callback required {@link AuthenticationCallback} object for async
     *            call.
     */
    public void acquireToken(Activity activity, String resource, String clientId, String redirectUri,
            String loginHint, String extraQueryParameters, AuthenticationCallback<AuthenticationResult> callback) {

        redirectUri = checkInputParameters(resource, clientId, redirectUri, PromptBehavior.Auto, callback);

        final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri,
                loginHint, PromptBehavior.Auto, extraQueryParameters, getRequestCorrelationId());
        request.setUserIdentifierType(UserIdentifierType.LoginHint);
        acquireTokenLocal(wrapActivity(activity), false, request, callback);
    }

    /**
     * acquire Token will start interactive flow if needed. It checks the cache
     * to return existing result if not expired. It tries to use refresh token
     * if available. If it fails to get token with refresh token, behavior will
     * depend on options. If {@link PromptBehavior} is AUTO, it will remove this
     * refresh token from cache and fall back on the UI. Default is AUTO. if
     * {@link PromptBehavior} is Always, it will display prompt screen.
     * 
     * @param activity Calling activity
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param redirectUri Optional. It will use packagename and provided suffix
     *            for this.
     * @param prompt Optional. {@link PromptBehavior} added as query parameter
     *            to authorization url
     * @param callback required {@link AuthenticationCallback} object for async
     *            call.
     */
    public void acquireToken(Activity activity, String resource, String clientId, String redirectUri,
            PromptBehavior prompt, AuthenticationCallback<AuthenticationResult> callback) {

        redirectUri = checkInputParameters(resource, clientId, redirectUri, prompt, callback);

        final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri,
                null, prompt, null, getRequestCorrelationId());

        acquireTokenLocal(wrapActivity(activity), false, request, callback);
    }

    /**
     * acquire Token will start interactive flow if needed. It checks the cache
     * to return existing result if not expired. It tries to use refresh token
     * if available. If it fails to get token with refresh token, behavior will
     * depend on options. If promptbehavior is AUTO, it will remove this refresh
     * token from cache and fall back on the UI if activitycontext is not null.
     * Default is AUTO.
     * 
     * @param activity Calling activity
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param redirectUri Optional. It will use packagename and provided suffix
     *            for this.
     * @param prompt Optional. added as query parameter to authorization url
     * @param extraQueryParameters Optional. added to authorization url
     * @param callback required {@link AuthenticationCallback} object for async
     *            call.
     */
    public void acquireToken(Activity activity, String resource, String clientId, String redirectUri,
            PromptBehavior prompt, String extraQueryParameters,
            AuthenticationCallback<AuthenticationResult> callback) {

        redirectUri = checkInputParameters(resource, clientId, redirectUri, prompt, callback);

        final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri,
                null, prompt, extraQueryParameters, getRequestCorrelationId());

        acquireTokenLocal(wrapActivity(activity), false, request, callback);
    }

    /**
     * acquire Token will start interactive flow if needed. It checks the cache
     * to return existing result if not expired. It tries to use refresh token
     * if available. If it fails to get token with refresh token, behavior will
     * depend on options. If promptbehavior is AUTO, it will remove this refresh
     * token from cache and fall back on the UI if activitycontext is not null.
     * Default is AUTO.
     * 
     * @param activity Calling activity
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param redirectUri Optional. It will use packagename and provided suffix
     *            for this.
     * @param loginHint Optional. It is used for cache and as a loginhint at
     *            authentication.
     * @param prompt Optional. added as query parameter to authorization url
     * @param extraQueryParameters Optional. added to authorization url
     * @param callback required {@link AuthenticationCallback} object for async
     *            call.
     */
    public void acquireToken(Activity activity, String resource, String clientId, String redirectUri,
            String loginHint, PromptBehavior prompt, String extraQueryParameters,
            AuthenticationCallback<AuthenticationResult> callback) {

        redirectUri = checkInputParameters(resource, clientId, redirectUri, prompt, callback);

        final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri,
                loginHint, prompt, extraQueryParameters, getRequestCorrelationId());
        request.setUserIdentifierType(UserIdentifierType.LoginHint);
        acquireTokenLocal(wrapActivity(activity), false, request, callback);
    }

    /**
     * It will start interactive flow if needed. It checks the cache to return
     * existing result if not expired. It tries to use refresh token if
     * available. If it fails to get token with refresh token, behavior will
     * depend on options. If promptbehavior is AUTO, it will remove this refresh
     * token from cache and fall back on the UI. Default is AUTO.
     * 
     * @param fragment It accepts both type of fragments.
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param redirectUri Optional. It will use packagename and provided suffix
     *            for this.
     * @param loginHint Optional. It is used for cache and as a loginhint at
     *            authentication.
     * @param prompt Optional. added as query parameter to authorization url
     * @param extraQueryParameters Optional. added to authorization url
     * @param callback required {@link AuthenticationCallback} object for async
     *            call.
     */
    public void acquireToken(IWindowComponent fragment, String resource, String clientId, String redirectUri,
            String loginHint, PromptBehavior prompt, String extraQueryParameters,
            AuthenticationCallback<AuthenticationResult> callback) {

        redirectUri = checkInputParameters(resource, clientId, redirectUri, prompt, callback);

        final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri,
                loginHint, prompt, extraQueryParameters, getRequestCorrelationId());
        request.setUserIdentifierType(UserIdentifierType.LoginHint);
        acquireTokenLocal(fragment, false, request, callback);
    }

    /**
     * This uses new dialog based prompt. It will create a handler to run the
     * dialog related code. It will start interactive flow if needed. It checks
     * the cache to return existing result if not expired. It tries to use
     * refresh token if available. If it fails to get token with refresh token,
     * behavior will depend on options. If promptbehavior is AUTO, it will
     * remove this refresh token from cache and fall back on the UI. Default is
     * AUTO.
     * 
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param redirectUri Optional. It will use packagename and provided suffix
     *            for this.
     * @param loginHint Optional. It is used for cache and as a loginhint at
     *            authentication.
     * @param prompt Optional. added as query parameter to authorization url
     * @param extraQueryParameters Optional. added to authorization url
     * @param callback required {@link AuthenticationCallback} object for async
     *            call.
     */
    public void acquireToken(String resource, String clientId, String redirectUri, String loginHint,
            PromptBehavior prompt, String extraQueryParameters,
            AuthenticationCallback<AuthenticationResult> callback) {

        redirectUri = checkInputParameters(resource, clientId, redirectUri, prompt, callback);

        final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri,
                loginHint, prompt, extraQueryParameters, getRequestCorrelationId());
        request.setUserIdentifierType(UserIdentifierType.LoginHint);
        acquireTokenLocal(null, true, request, callback);
    }

    private IWindowComponent wrapActivity(final Activity activity) {
        return new IWindowComponent() {
            Activity refActivity = activity;

            @Override
            public void startActivityForResult(Intent intent, int requestCode) {
                refActivity.startActivityForResult(intent, requestCode);
            }
        };
    }

    private String checkInputParameters(String resource, String clientId, String redirectUri,
            PromptBehavior behavior, AuthenticationCallback<AuthenticationResult> callback) {
        if (mContext == null) {
            throw new AuthenticationException(ADALError.DEVELOPER_CONTEXT_IS_NOT_PROVIDED);
        }

        if (StringExtensions.IsNullOrBlank(resource)) {
            throw new IllegalArgumentException("resource");
        }

        if (StringExtensions.IsNullOrBlank(clientId)) {
            throw new IllegalArgumentException("clientId");
        }

        if (callback == null) {
            throw new IllegalArgumentException("callback");
        }

        if (StringExtensions.IsNullOrBlank(redirectUri)) {
            redirectUri = getRedirectFromPackage();
        }

        return redirectUri;
    }

    /**
     * This is sync function. It will first look at the cache and automatically
     * checks for the token expiration. Additionally, if no suitable access
     * token is found in the cache, but refresh token is available, the function
     * will use the refresh token automatically. This method will not show UI
     * for the user. If prompt is needed, the method will return an exception
     * 
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param userId UserID obtained from
     *            {@link AuthenticationResult #getUserInfo()}
     * @return A {@link Future} object representing the
     *         {@link AuthenticationResult} of the call. It contains Access
     *         Token,the Access Token's expiration time, Refresh token, and
     *         {@link UserInfo}.
     */
    public AuthenticationResult acquireTokenSilentSync(String resource, String clientId, String userId) {
        Future<AuthenticationResult> futureResult = acquireTokenSilent(resource, clientId, userId, null);
        try {
            return futureResult.get();
        } catch (InterruptedException e) {
            convertExceptionForSync(e);
        } catch (ExecutionException e) {
            convertExceptionForSync(e);
        }

        return null;
    }

    private void convertExceptionForSync(Exception e) {
        // change to unchecked exception
        if (e.getCause() != null) {

            if (e.getCause() instanceof AuthenticationException) {
                throw (AuthenticationException) e.getCause();
            } else if (e.getCause() instanceof IllegalArgumentException) {
                throw (IllegalArgumentException) e.getCause();
            } else {
                throw new AuthenticationException(ADALError.ERROR_SILENT_REQUEST, e.getCause().getMessage(),
                        e.getCause());
            }
        }

        throw new AuthenticationException(ADALError.ERROR_SILENT_REQUEST, e.getMessage(), e);
    }

    /**
     * The function will first look at the cache and automatically checks for
     * the token expiration. Additionally, if no suitable access token is found
     * in the cache, but refresh token is available, the function will use the
     * refresh token automatically. This method will not show UI for the user.
     * If prompt is needed, the method will return an exception
     * 
     * @param resource required resource identifier.
     * @param clientId required client identifier.
     * @param userId UserId obtained from {@link UserInfo} inside
     *            {@link AuthenticationResult}
     * @param callback required {@link AuthenticationCallback} object for async
     *            call.
     * @return A {@link Future} object representing the
     *         {@link AuthenticationResult} of the call. It contains Access
     *         Token,the Access Token's expiration time, Refresh token, and
     *         {@link UserInfo}.
     */
    public Future<AuthenticationResult> acquireTokenSilent(String resource, String clientId, String userId,
            AuthenticationCallback<AuthenticationResult> callback) {
        if (StringExtensions.IsNullOrBlank(resource)) {
            throw new IllegalArgumentException("resource");
        }
        if (StringExtensions.IsNullOrBlank(clientId)) {
            throw new IllegalArgumentException("clientId");
        }

        final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, userId,
                getRequestCorrelationId());
        request.setSilent(true);
        request.setPrompt(PromptBehavior.Auto);
        request.setUserIdentifierType(UserIdentifierType.UniqueId);
        return acquireTokenLocal(null, false, request, callback);
    }

    /**
     * acquire token using refresh token if cache is not used. Otherwise, use
     * acquireToken to let the ADAL handle the cache lookup and refresh token
     * request.
     * 
     * @param refreshToken Required.
     * @param clientId Required.
     * @param callback Required
     */
    public void acquireTokenByRefreshToken(String refreshToken, String clientId,
            AuthenticationCallback<AuthenticationResult> callback) {
        // Authenticator is not supported if user is managing the cache
        refreshTokenWithoutCache(refreshToken, clientId, null, callback);
    }

    /**
     * acquire token using refresh token if cache is not used. Otherwise, use
     * acquireToken to let the ADAL handle the cache lookup and refresh token
     * request.
     * 
     * @param refreshToken Required.
     * @param clientId Required.
     * @param resource Required resource identifier.
     * @param callback Required
     */
    public void acquireTokenByRefreshToken(String refreshToken, String clientId, String resource,
            AuthenticationCallback<AuthenticationResult> callback) {
        // Authenticator is not supported if user is managing the cache
        refreshTokenWithoutCache(refreshToken, clientId, resource, callback);
    }

    /**
     * This method wraps the implementation for onActivityResult at the related
     * Activity class. This method is called at UI thread.
     * 
     * @param requestCode Request code provided at the start of the activity.
     * @param resultCode Result code set from the activity.
     * @param data {@link Intent}
     */
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        // This is called at UI thread when Activity sets result back.
        // ResultCode is set back from AuthenticationActivity. RequestCode is
        // set when we start the activity for result.
        if (requestCode == AuthenticationConstants.UIRequest.BROWSER_FLOW) {
            if (data == null) {
                // If data is null, RequestId is unknown. It could not find
                // callback to respond to this request.
                Logger.e(TAG, "onActivityResult BROWSER_FLOW data is null.", "",
                        ADALError.ON_ACTIVITY_RESULT_INTENT_NULL);
            } else {
                Bundle extras = data.getExtras();
                final int requestId = extras.getInt(AuthenticationConstants.Browser.REQUEST_ID);
                final AuthenticationRequestState waitingRequest = getWaitingRequest(requestId);
                if (waitingRequest != null) {
                    Logger.v(TAG, "onActivityResult RequestId:" + requestId);
                } else {
                    Logger.e(TAG, "onActivityResult did not find waiting request for RequestId:" + requestId, "",
                            ADALError.ON_ACTIVITY_RESULT_INTENT_NULL);
                    // There is no matching callback to send error
                    return;
                }

                // Cancel or browser error can use recorded request to figure
                // out original correlationId send with request.
                String correlationInfo = getCorrelationInfoFromWaitingRequest(waitingRequest);
                if (resultCode == AuthenticationConstants.UIResponse.TOKEN_BROKER_RESPONSE) {
                    String accessToken = data.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_ACCESS_TOKEN);
                    String accountName = data.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_NAME);
                    mBrokerProxy.saveAccount(accountName);
                    long expireTime = data.getLongExtra(AuthenticationConstants.Broker.ACCOUNT_EXPIREDATE, 0);
                    Date expire = new Date(expireTime);
                    UserInfo userinfo = UserInfo.getUserInfoFromBrokerResult(data.getExtras());
                    AuthenticationResult brokerResult = new AuthenticationResult(accessToken, null, expire, false,
                            userinfo, "", "");
                    if (brokerResult != null && brokerResult.getAccessToken() != null) {
                        waitingRequest.mDelagete.onSuccess(brokerResult);
                        return;
                    }
                } else if (resultCode == AuthenticationConstants.UIResponse.BROWSER_CODE_CANCEL) {
                    // User cancelled the flow by clicking back button or
                    // activating another activity
                    Logger.v(TAG, "User cancelled the flow RequestId:" + requestId + correlationInfo);
                    waitingRequestOnError(waitingRequest, requestId, new AuthenticationCancelError(
                            "User cancelled the flow RequestId:" + requestId + correlationInfo));
                } else if (resultCode == AuthenticationConstants.UIResponse.BROWSER_CODE_AUTHENTICATION_EXCEPTION) {
                    Serializable authException = extras
                            .getSerializable(AuthenticationConstants.Browser.RESPONSE_AUTHENTICATION_EXCEPTION);
                    if (authException != null && authException instanceof AuthenticationException) {
                        AuthenticationException exception = (AuthenticationException) authException;
                        Logger.w(TAG, "Webview returned exception", exception.getMessage(),
                                ADALError.WEBVIEW_RETURNED_AUTHENTICATION_EXCEPTION);
                        waitingRequestOnError(waitingRequest, requestId, exception);
                    } else {
                        waitingRequestOnError(waitingRequest, requestId, new AuthenticationException(
                                ADALError.WEBVIEW_RETURNED_INVALID_AUTHENTICATION_EXCEPTION));
                    }
                } else if (resultCode == AuthenticationConstants.UIResponse.BROWSER_CODE_ERROR) {
                    String errCode = extras.getString(AuthenticationConstants.Browser.RESPONSE_ERROR_CODE);
                    String errMessage = extras.getString(AuthenticationConstants.Browser.RESPONSE_ERROR_MESSAGE);
                    Logger.v(TAG, "Error info:" + errCode + " " + errMessage + " for requestId: " + requestId
                            + correlationInfo);
                    waitingRequestOnError(waitingRequest, requestId, new AuthenticationException(
                            ADALError.SERVER_INVALID_REQUEST, errCode + " " + errMessage));
                } else if (resultCode == AuthenticationConstants.UIResponse.BROWSER_CODE_COMPLETE) {
                    final AuthenticationRequest authenticationRequest = (AuthenticationRequest) extras
                            .getSerializable(AuthenticationConstants.Browser.RESPONSE_REQUEST_INFO);
                    final String endingUrl = extras.getString(AuthenticationConstants.Browser.RESPONSE_FINAL_URL);
                    if (endingUrl.isEmpty()) {
                        AuthenticationException e = new AuthenticationException(
                                ADALError.WEBVIEW_RETURNED_EMPTY_REDIRECT_URL,
                                "Webview did not reach the redirectUrl. " + authenticationRequest.getLogInfo());
                        Logger.e(TAG, e.getMessage(), "", e.getCode());
                        waitingRequestOnError(waitingRequest, requestId, e);
                    } else {
                        // Browser has the url and it will exchange auth code
                        // for token
                        final CallbackHandler callbackHandle = new CallbackHandler(mHandler,
                                waitingRequest.mDelagete);

                        // Executes all the calls inside the Runnable to return
                        // immediately to
                        // UI thread. All UI
                        // related actions will be performed using the Handler.
                        sThreadExecutor.submit(new Runnable() {

                            @Override
                            public void run() {
                                Logger.v(TAG, "Processing url for token. " + authenticationRequest.getLogInfo());
                                Oauth2 oauthRequest = new Oauth2(authenticationRequest, mWebRequest);
                                AuthenticationResult result = null;
                                try {
                                    result = oauthRequest.getToken(endingUrl);
                                    Logger.v(TAG, "OnActivityResult processed the result. "
                                            + authenticationRequest.getLogInfo());
                                } catch (Exception exc) {
                                    String msg = "Error in processing code to get token. "
                                            + authenticationRequest.getLogInfo();
                                    Logger.e(TAG, msg, ExceptionExtensions.getExceptionMessage(exc),
                                            ADALError.AUTHORIZATION_CODE_NOT_EXCHANGED_FOR_TOKEN, exc);

                                    // Call error at UI thread
                                    waitingRequestOnError(callbackHandle, waitingRequest, requestId,
                                            new AuthenticationException(
                                                    ADALError.AUTHORIZATION_CODE_NOT_EXCHANGED_FOR_TOKEN, msg,
                                                    exc));
                                    return;
                                }

                                try {
                                    if (result != null) {
                                        Logger.v(TAG, "OnActivityResult is setting the token to cache. "
                                                + authenticationRequest.getLogInfo());

                                        if (!StringExtensions.IsNullOrBlank(result.getAccessToken())) {
                                            setItemToCache(authenticationRequest, result, true);
                                        }

                                        if (waitingRequest != null && waitingRequest.mDelagete != null) {
                                            Logger.v(TAG, "Sending result to callback. "
                                                    + authenticationRequest.getLogInfo());
                                            callbackHandle.onSuccess(result);
                                        }
                                    } else {
                                        callbackHandle.onError(new AuthenticationException(
                                                ADALError.AUTHORIZATION_CODE_NOT_EXCHANGED_FOR_TOKEN));
                                    }
                                } finally {
                                    removeWaitingRequest(requestId);
                                }
                            }
                        });
                    }
                }
            }
        }
    }

    private static boolean isUserMisMatch(final AuthenticationRequest request, final AuthenticationResult result) {
        if (result.getUserInfo() != null && !StringExtensions.IsNullOrBlank(result.getUserInfo().getUserId())
                && !StringExtensions.IsNullOrBlank(request.getUserId())) {
            // Verify if IdToken is present and userid is specified
            return !request.getUserId().equalsIgnoreCase(result.getUserInfo().getUserId());
        }

        // it should verify loginhint as well if specified
        if (result.getUserInfo() != null && !StringExtensions.IsNullOrBlank(result.getUserInfo().getDisplayableId())
                && !StringExtensions.IsNullOrBlank(request.getLoginHint())) {
            // Verify if IdToken is present and userid is specified
            return !request.getLoginHint().equalsIgnoreCase(result.getUserInfo().getDisplayableId());
        }

        return false;
    }

    /**
     * If request has correlationID, ADAL should report that instead of current
     * CorrelationId.
     * 
     * @param waitingRequest
     * @return
     */
    private String getCorrelationInfoFromWaitingRequest(final AuthenticationRequestState waitingRequest) {
        UUID requestCorrelationID = getRequestCorrelationId();
        if (waitingRequest.mRequest != null) {
            requestCorrelationID = waitingRequest.mRequest.getCorrelationId();
        }

        String correlationInfo = String.format(" CorrelationId: %s", requestCorrelationID.toString());
        return correlationInfo;
    }

    private void waitingRequestOnError(final AuthenticationRequestState waitingRequest, int requestId,
            AuthenticationException exc) {

        if (waitingRequest != null && waitingRequest.mDelagete != null) {
            Logger.v(TAG, "Sending error to callback" + getCorrelationInfoFromWaitingRequest(waitingRequest));
            waitingRequest.mDelagete.onError(exc);
        }
        if (exc != null && exc.getCode() != ADALError.AUTH_FAILED_CANCELLED) {
            removeWaitingRequest(requestId);
        }
    }

    private void waitingRequestOnError(CallbackHandler handler, final AuthenticationRequestState waitingRequest,
            int requestId, final AuthenticationException exc) {

        if (waitingRequest != null && waitingRequest.mDelagete != null) {
            Logger.v(TAG, "Sending error to callback" + getCorrelationInfoFromWaitingRequest(waitingRequest));
            handler.onError(exc);
        }
        if (exc != null && exc.getCode() != ADALError.AUTH_FAILED_CANCELLED) {
            removeWaitingRequest(requestId);
        }
    }

    private void removeWaitingRequest(int requestId) {
        Logger.v(TAG, "Remove waiting request: " + requestId);

        WRITE_LOCK.lock();
        try {
            mDelegateMap.remove(requestId);
        } finally {
            WRITE_LOCK.unlock();
        }
    }

    private AuthenticationRequestState getWaitingRequest(int requestId) {
        Logger.v(TAG, "Get waiting request: " + requestId);
        AuthenticationRequestState request = null;

        READ_LOCK.lock();
        try {
            request = mDelegateMap.get(requestId);
        } finally {
            READ_LOCK.unlock();
        }

        if (request == null && mAuthorizationCallback != null && requestId == mAuthorizationCallback.hashCode()) {
            // it does not have the caller callback. It will check the last
            // callback if set
            Logger.e(TAG,
                    "Request callback is not available for requestid:" + requestId + ". It will use last callback.",
                    "", ADALError.CALLBACK_IS_NOT_FOUND);
            request = new AuthenticationRequestState(0, null, mAuthorizationCallback);
        }

        return request;
    }

    private void putWaitingRequest(int requestId, AuthenticationRequestState requestState) {
        Logger.v(TAG, "Put waiting request: " + requestId + getCorrelationInfoFromWaitingRequest(requestState));
        if (requestState != null) {
            WRITE_LOCK.lock();

            try {
                mDelegateMap.put(requestId, requestState);
            } finally {
                WRITE_LOCK.unlock();
            }
        }
    }

    /**
     * Active authentication activity can be cancelled if it exists. It may not
     * be cancelled if activity is not launched yet. RequestId is the hashcode
     * of your AuthenticationCallback.
     * 
     * @param requestId Hash code value of your callback to cancel activity
     *            launch
     * @return true: if there is a valid waiting request and cancel message send
     *         successfully. false: Request does not exist or cancel message not
     *         send
     */
    public boolean cancelAuthenticationActivity(int requestId) {

        AuthenticationRequestState request = getWaitingRequest(requestId);

        if (request == null || request.mDelagete == null) {
            // there is not any waiting callback
            Logger.v(TAG, "Current callback is empty. There is not any active authentication.");
            return true;
        }

        String currentCorrelationInfo = getCorrelationInfoFromWaitingRequest(request);
        Logger.v(TAG, "Current callback is not empty. There is an active authentication Activity."
                + currentCorrelationInfo);

        // intent to cancel. Authentication activity registers for this message
        // at onCreate event.
        final Intent intent = new Intent(AuthenticationConstants.Browser.ACTION_CANCEL);
        final Bundle extras = new Bundle();
        intent.putExtras(extras);
        intent.putExtra(AuthenticationConstants.Browser.REQUEST_ID, requestId);
        // send intent to cancel any active authentication activity.
        // it may not cancel it, if activity takes some time to launch.

        boolean cancelResult = LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
        if (cancelResult) {
            // clear callback if broadcast message was successful
            Logger.v(TAG, "Cancel broadcast message was successful." + currentCorrelationInfo);
            request.mCancelled = true;
            request.mDelagete.onError(new AuthenticationCancelError("Cancel broadcast message was successful."));
        } else {
            // Activity is not launched yet or receiver is not registered
            Logger.w(TAG, "Cancel broadcast message was not successful." + currentCorrelationInfo, "",
                    ADALError.BROADCAST_CANCEL_NOT_SUCCESSFUL);
        }

        return cancelResult;
    }

    /**
     * Singled threaded Executor for async work.
     */
    private static ExecutorService sThreadExecutor = Executors.newSingleThreadExecutor();

    private Handler mHandler;

    class CallbackHandler {
        private Handler mRefHandler;

        private AuthenticationCallback<AuthenticationResult> callback;

        public CallbackHandler(Handler ref, AuthenticationCallback<AuthenticationResult> callbackExt) {
            mRefHandler = ref;
            callback = callbackExt;
        }

        public void onError(final AuthenticationException e) {
            if (mRefHandler != null && callback != null) {
                mRefHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onError(e);
                        return;
                    }
                });
            } else {
                throw e;
            }
        }

        public void onSuccess(final AuthenticationResult result) {
            if (mRefHandler != null && callback != null) {
                mRefHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onSuccess(result);
                        return;
                    }
                });
            }
        }
    }

    private Future<AuthenticationResult> acquireTokenLocal(final IWindowComponent activity, final boolean useDialog,
            final AuthenticationRequest request, final AuthenticationCallback<AuthenticationResult> externalCall) {
        getHandler();
        final CallbackHandler callbackHandle = new CallbackHandler(mHandler, externalCall);

        // Executes all the calls inside the Runnable to return immediately to
        // user. All UI
        // related actions will be performed using Handler.
        Logger.setCorrelationId(getRequestCorrelationId());
        Logger.v(TAG, "Sending async task from thread:" + android.os.Process.myTid());
        return sThreadExecutor.submit(new Callable<AuthenticationResult>() {

            @Override
            public AuthenticationResult call() {
                Logger.v(TAG, "Running task in thread:" + android.os.Process.myTid());
                return acquireTokenLocalCall(callbackHandle, activity, useDialog, request);
            }
        });
    }

    /**
     * Only gets token from activity defined in this package.
     * 
     * @param activity
     * @param request
     * @param prompt
     * @param callback
     * @return
     */
    private AuthenticationResult acquireTokenLocalCall(final CallbackHandler callbackHandle,
            final IWindowComponent activity, final boolean useDialog, final AuthenticationRequest request) {
        URL authorityUrl = StringExtensions.getUrl(mAuthority);
        if (authorityUrl == null) {
            callbackHandle.onError(new AuthenticationException(ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_URL));
            return null;
        }

        if (mValidateAuthority && !mAuthorityValidated) {
            try {
                final URL authorityUrlInCallback = authorityUrl;
                // Discovery call creates an Async Task to send
                // Web Requests
                // using a handler
                boolean result = validateAuthority(authorityUrl);
                if (result) {
                    mAuthorityValidated = true;
                    Logger.v(TAG, "Authority is validated: " + authorityUrlInCallback.toString());
                } else {
                    Logger.v(TAG,
                            "Call external callback since instance is invalid" + authorityUrlInCallback.toString());
                    callbackHandle.onError(
                            new AuthenticationException(ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_INSTANCE));
                    return null;
                }
            } catch (Exception exc) {
                Logger.e(TAG, "Authority validation has an error.", "",
                        ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_INSTANCE, exc);
                callbackHandle
                        .onError(new AuthenticationException(ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_INSTANCE));
                return null;
            }
        }

        // Validated the authority or skipped the validation
        return acquireTokenAfterValidation(callbackHandle, activity, useDialog, request);
    }

    private boolean promptUser(PromptBehavior prompt) {
        return prompt == PromptBehavior.Always || prompt == PromptBehavior.REFRESH_SESSION;
    }

    private AuthenticationResult acquireTokenAfterValidation(CallbackHandler callbackHandle,
            final IWindowComponent activity, final boolean useDialog, final AuthenticationRequest request) {
        Logger.v(TAG, "Token request started");

        // BROKER flow intercepts here
        // cache and refresh call happens through the authenticator service
        if (mBrokerProxy.canSwitchToBroker()) {
            Logger.v(TAG, "It switched to broker for context: " + mContext.getPackageName());
            AuthenticationResult result = null;
            request.setVersion(getVersionName());
            // Don't send background request, if prompt flag is always or
            // refresh_session
            if (!promptUser(request.getPrompt())) {
                try {
                    result = mBrokerProxy.getAuthTokenInBackground(request);
                } catch (AuthenticationException ex) {
                    // pass back to caller for known exceptions such as failure
                    // to encrypt
                    if (callbackHandle.callback != null) {
                        callbackHandle.onError(ex);
                        return null;
                    } else {
                        throw ex;
                    }
                }
            }

            if (result != null && result.getAccessToken() != null && !result.getAccessToken().isEmpty()) {
                Logger.v(TAG, "Token is returned from background call ");
                if (callbackHandle.callback != null) {
                    callbackHandle.onSuccess(result);
                }
                return result;
            }

            // Launch broker activity
            // if cache and refresh request is not handled.
            // Initial request to authenticator needs to launch activity to
            // record calling uid for the account. This happens for Prompt auto
            // or always behavior.
            if (!request.isSilent() && callbackHandle.callback != null && activity != null) {

                // Only happens with callback since silent call does not show UI
                Logger.v(TAG, "Launch activity for Authenticator");
                mAuthorizationCallback = callbackHandle.callback;
                request.setRequestId(callbackHandle.callback.hashCode());
                Logger.v(TAG,
                        "Starting Authentication Activity with callback:" + callbackHandle.callback.hashCode());
                putWaitingRequest(callbackHandle.callback.hashCode(), new AuthenticationRequestState(
                        callbackHandle.callback.hashCode(), request, callbackHandle.callback));
                if (result != null && result.isInitialRequest()) {
                    Logger.v(TAG, "Initial request to authenticator");
                    // Record the initial request but not force a prompt
                }

                // onActivityResult will receive the response
                // Activity needs to launch to record calling app for this
                // account
                Intent brokerIntent = mBrokerProxy.getIntentForBrokerActivity(request);
                if (brokerIntent != null) {
                    try {
                        Logger.v(TAG, "Calling activity pid:" + android.os.Process.myPid() + " tid:"
                                + android.os.Process.myTid() + "uid:" + android.os.Process.myUid());
                        activity.startActivityForResult(brokerIntent,
                                AuthenticationConstants.UIRequest.BROWSER_FLOW);
                    } catch (ActivityNotFoundException e) {
                        Logger.e(TAG, "Activity login is not found after resolving intent", "",
                                ADALError.DEVELOPER_ACTIVITY_IS_NOT_RESOLVED, e);
                        callbackHandle
                                .onError(new AuthenticationException(ADALError.BROKER_ACTIVITY_IS_NOT_RESOLVED));
                    }
                } else {
                    callbackHandle
                            .onError(new AuthenticationException(ADALError.DEVELOPER_ACTIVITY_IS_NOT_RESOLVED));
                }
            } else {

                // User does not want to launch activity
                String msg = "Prompt is not allowed and failed to get token:";
                Logger.e(TAG, msg, "", ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED);
                callbackHandle.onError(
                        new AuthenticationException(ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED, msg));
            }

            // It will start activity if callback is provided. Return null here.
            return null;
        } else {
            return localFlow(callbackHandle, activity, useDialog, request);
        }
    }

    private AuthenticationResult localFlow(CallbackHandler callbackHandle, final IWindowComponent activity,
            final boolean useDialog, final AuthenticationRequest request) {
        // Lookup access token from cache
        AuthenticationResult cachedItem = getItemFromCache(request);
        if (cachedItem != null && isUserMisMatch(request, cachedItem)) {
            if (callbackHandle.callback != null) {
                callbackHandle.onError(new AuthenticationException(ADALError.AUTH_FAILED_USER_MISMATCH));
                return null;
            } else {
                throw new AuthenticationException(ADALError.AUTH_FAILED_USER_MISMATCH);
            }
        }

        if (!promptUser(request.getPrompt()) && isValidCache(cachedItem)) {
            Logger.v(TAG, "Token is returned from cache");
            if (callbackHandle.callback != null) {
                callbackHandle.onSuccess(cachedItem);
            }
            return cachedItem;
        }

        Logger.v(TAG, "Checking refresh tokens");
        RefreshItem refreshItem = getRefreshToken(request);
        if (!promptUser(request.getPrompt()) && refreshItem != null
                && !StringExtensions.IsNullOrBlank(refreshItem.mRefreshToken)) {
            Logger.v(TAG, "Refresh token is available and it will attempt to refresh token");
            return refreshToken(callbackHandle, activity, useDialog, request, refreshItem, true);
        } else {
            Logger.v(TAG, "Refresh token is not available");
            if (!request.isSilent() && callbackHandle.callback != null && (activity != null || useDialog)) {
                // start activity if other options are not available
                // delegate map is used to remember callback if another
                // instance of authenticationContext is created for config
                // change or similar at client app.
                mAuthorizationCallback = callbackHandle.callback;
                request.setRequestId(callbackHandle.callback.hashCode());
                Logger.v(TAG,
                        "Starting Authentication Activity with callback:" + callbackHandle.callback.hashCode());
                putWaitingRequest(callbackHandle.callback.hashCode(), new AuthenticationRequestState(
                        callbackHandle.callback.hashCode(), request, callbackHandle.callback));

                if (useDialog) {
                    AuthenticationDialog dialog = new AuthenticationDialog(mHandler, mContext, this, request);
                    dialog.show();
                } else {
                    // onActivityResult will receive the response
                    if (!startAuthenticationActivity(activity, request)) {
                        callbackHandle
                                .onError(new AuthenticationException(ADALError.DEVELOPER_ACTIVITY_IS_NOT_RESOLVED));
                    }
                }
            } else {

                // User does not want to launch activity
                Logger.e(TAG, "Prompt is not allowed and failed to get token:", "",
                        ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED);
                callbackHandle
                        .onError(new AuthenticationException(ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED));
            }
        }

        return null;
    }

    protected boolean isRefreshable(AuthenticationResult cachedItem) {
        return cachedItem != null && !StringExtensions.IsNullOrBlank(cachedItem.getRefreshToken());
    }

    private boolean isValidCache(AuthenticationResult cachedItem) {
        if (cachedItem != null && !StringExtensions.IsNullOrBlank(cachedItem.getAccessToken())
                && !cachedItem.isExpired()) {
            return true;
        }

        return false;
    }

    /**
     * get token from cache to return it, if not expired.
     * 
     * @param request
     * @return AuthenticationResult
     */
    private AuthenticationResult getItemFromCache(final AuthenticationRequest request) {
        if (mTokenCacheStore != null) {

            // get token if resourceid matches to cache key.
            TokenCacheItem item = null;
            if (request.getUserIdentifierType() == UserIdentifierType.LoginHint) {
                item = mTokenCacheStore.getItem(CacheKey.createCacheKey(request, request.getLoginHint()));
            }

            if (request.getUserIdentifierType() == UserIdentifierType.UniqueId) {
                item = mTokenCacheStore.getItem(CacheKey.createCacheKey(request, request.getUserId()));
            }

            if (request.getUserIdentifierType() == UserIdentifierType.NoUser) {
                item = mTokenCacheStore.getItem(CacheKey.createCacheKey(request, null));
            }

            if (item != null) {
                Logger.v(TAG, "getItemFromCache accessTokenId:" + getTokenHash(item.getAccessToken())
                        + " refreshTokenId:" + getTokenHash(item.getRefreshToken()));
                return AuthenticationResult.createResult(item);
            }
        }
        return null;
    }

    private String getTokenHash(String token) {
        try {
            return StringExtensions.createHash(token);
        } catch (NoSuchAlgorithmException e) {
            Logger.e(TAG, "Digest error", "", ADALError.DEVICE_NO_SUCH_ALGORITHM, e);
        } catch (UnsupportedEncodingException e) {
            Logger.e(TAG, "Digest error", "", ADALError.ENCODING_IS_NOT_SUPPORTED, e);
        }

        return "";
    }

    /**
     * If refresh token fails, this needs to be removed from cache to not use
     * this again for next try. Error in refreshToken call will result in
     * another call to acquireToken. It may try multi resource refresh token for
     * second attempt.
     */
    private class RefreshItem {
        String mRefreshToken;

        String mKey;

        boolean mMultiResource;

        UserInfo mUserInfo;

        String mRawIdToken;

        String mKeyWithUserId;

        String mKeyWithDisplayableId;

        public RefreshItem(String keyInCache, AuthenticationRequest request, TokenCacheItem item,
                boolean multiResource) {
            mKey = keyInCache;
            mMultiResource = multiResource;

            if (item != null) {
                mRefreshToken = item.getRefreshToken();
                mUserInfo = item.getUserInfo();
                mRawIdToken = item.getRawIdToken();
                if (item.getUserInfo() != null) {
                    mKeyWithUserId = CacheKey.createCacheKey(request, item.getUserInfo().getUserId());
                    mKeyWithDisplayableId = CacheKey.createCacheKey(request, item.getUserInfo().getDisplayableId());
                }
            }
        }

        public RefreshItem(String refreshToken) {
            mMultiResource = false;
            mRefreshToken = refreshToken;
        }
    }

    private RefreshItem getRefreshToken(final AuthenticationRequest request) {
        RefreshItem refreshItem = null;
        if (mTokenCacheStore != null) {
            boolean multiResource = false;
            // target refreshToken for this resource first. CacheKey will
            // include the resourceId in the cachekey
            Logger.v(TAG, "Looking for regular refresh token");
            String userId = request.getUserId();
            if (StringExtensions.IsNullOrBlank(userId)) {
                // acquireTokenSilent expects userid field from UserInfo
                userId = request.getLoginHint();
            }
            String keyUsed = CacheKey.createCacheKey(request, userId);
            TokenCacheItem item = mTokenCacheStore.getItem(keyUsed);
            if (item == null || StringExtensions.IsNullOrBlank(item.getRefreshToken())) {
                // if not present, check multiResource item in cache. Cache key
                // will not include resourceId in the cache key string.
                Logger.v(TAG, "Looking for Multi Resource Refresh token");
                keyUsed = CacheKey.createMultiResourceRefreshTokenKey(request, userId);
                item = mTokenCacheStore.getItem(keyUsed);
                multiResource = true;
            }

            if (item != null && !StringExtensions.IsNullOrBlank(item.getRefreshToken())) {
                String refreshTokenHash = getTokenHash(item.getRefreshToken());

                Logger.v(TAG, "Refresh token is available and id:" + refreshTokenHash + " Key used:" + keyUsed);
                refreshItem = new RefreshItem(keyUsed, request, item, multiResource);
            }
        }

        return refreshItem;
    }

    private void setItemToCache(final AuthenticationRequest request, AuthenticationResult result,
            boolean afterPrompt) throws AuthenticationException {
        if (mTokenCacheStore != null) {

            // User can ask for token without login hint. Next call from same
            // method should use token from cache.
            Logger.v(TAG, "Setting item to cache");

            // Calculate token hashcode
            logReturnedToken(request, result);

            // acquireTokenSilent uses userid to request items
            String userKey = request.getUserId();

            if (afterPrompt) {
                // User can change the username and enter a different one at
                // prompt. Use idtoken if present instead of loginhint after
                // prompt.
                if (result.getUserInfo() != null
                        && !StringExtensions.IsNullOrBlank(result.getUserInfo().getDisplayableId())) {
                    Logger.v(TAG, "Updating cache for username:" + result.getUserInfo().getDisplayableId());
                    setItemToCacheForUser(request, result, result.getUserInfo().getDisplayableId());
                }
            } else if (StringExtensions.IsNullOrBlank(userKey)) {
                userKey = request.getLoginHint();
            }

            // It will store in the cache for empty idtokens as well
            setItemToCacheForUser(request, result, userKey);

            // Set item with userid if idtoken is present.
            if (result.getUserInfo() != null && !StringExtensions.IsNullOrBlank(result.getUserInfo().getUserId())) {
                Logger.v(TAG, "Updating userId:" + result.getUserInfo().getUserId());
                setItemToCacheForUser(request, result, result.getUserInfo().getUserId());
            }
        }
    }

    private void setItemToCacheForUser(final AuthenticationRequest request, AuthenticationResult result,
            String userId) {
        mTokenCacheStore.setItem(CacheKey.createCacheKey(request, userId),
                new TokenCacheItem(request, result, false));

        // Store broad refresh token if available
        if (result.getIsMultiResourceRefreshToken()) {
            Logger.v(TAG, "Setting Multi Resource Refresh token to cache");
            mTokenCacheStore.setItem(CacheKey.createMultiResourceRefreshTokenKey(request, userId),
                    new TokenCacheItem(request, result, true));
        }
    }

    /**
     * Calculate hash for accessToken and log that.
     * 
     * @param request
     * @param result
     */
    private void logReturnedToken(final AuthenticationRequest request, final AuthenticationResult result) {
        if (result != null && result.getAccessToken() != null) {
            String accessTokenHash = getTokenHash(result.getAccessToken());
            String refreshTokenHash = getTokenHash(result.getRefreshToken());
            Logger.v(TAG, String.format("Access TokenID %s and Refresh TokenID %s returned. CorrelationId: %s",
                    accessTokenHash, refreshTokenHash, request.getCorrelationId()));
        }
    }

    private void setItemToCacheFromRefresh(final RefreshItem refreshItem, final AuthenticationRequest request,
            AuthenticationResult result) throws AuthenticationException {
        if (mTokenCacheStore != null) {
            // Use same key to store refreshed result. This key may belong to
            // normal token or MRRT token.
            Logger.v(TAG, "Setting refresh item to cache for key:" + refreshItem.mKey);
            logReturnedToken(request, result);

            // Update for cache key
            mTokenCacheStore.setItem(refreshItem.mKey,
                    new TokenCacheItem(request, result, refreshItem.mMultiResource));

            setItemToCache(request, result, false);
        }
    }

    private void removeItemFromCache(final RefreshItem refreshItem) throws AuthenticationException {
        if (mTokenCacheStore != null) {
            Logger.v(TAG, "Remove refresh item from cache:" + refreshItem.mKey);
            mTokenCacheStore.removeItem(refreshItem.mKey);
            // clean up keys related to userid/displayableid for same request
            mTokenCacheStore.removeItem(refreshItem.mKeyWithUserId);
            mTokenCacheStore.removeItem(refreshItem.mKeyWithDisplayableId);
        }
    }

    /**
     * refresh token if possible. if it fails, it calls acquire token after
     * removing refresh token from cache.
     * 
     * @param callbackHandle
     * @param activity Activity to use in case refresh token does not succeed
     *            and prompt is not set to never.
     * @param request incoming request
     * @param refreshItem refresh item info to remove this refresh token from
     *            cache
     * @param useCache refresh request can be explicit without cache usage.
     *            Error message should return without trying prompt.
     * @param externalCallback
     * @return
     */
    private AuthenticationResult refreshToken(final CallbackHandler callbackHandle, final IWindowComponent activity,
            final boolean useDialog, final AuthenticationRequest request, final RefreshItem refreshItem,
            final boolean useCache) {

        Logger.v(TAG, "Process refreshToken for " + request.getLogInfo() + " refreshTokenId:"
                + getTokenHash(refreshItem.mRefreshToken));

        // Removes refresh token from cache, when this call is complete. Request
        // may be interrupted, if app is shutdown by user. Detect connection
        // state to not remove refresh token if user turned Airplane mode or
        // similar.
        if (!mConnectionService.isConnectionAvailable()) {
            AuthenticationException exc = new AuthenticationException(ADALError.DEVICE_CONNECTION_IS_NOT_AVAILABLE,
                    "Connection is not available to refresh token");
            Logger.w(TAG, "Connection is not available to refresh token", request.getLogInfo(),
                    ADALError.DEVICE_CONNECTION_IS_NOT_AVAILABLE);
            callbackHandle.onError(exc);
            return null;
        }

        AuthenticationResult result = null;
        try {
            Oauth2 oauthRequest = new Oauth2(request, mWebRequest, mJWSBuilder);
            result = oauthRequest.refreshToken(refreshItem.mRefreshToken);
            if (result != null && StringExtensions.IsNullOrBlank(result.getRefreshToken())) {
                Logger.v(TAG, "Refresh token is not returned or empty");
                result.setRefreshToken(refreshItem.mRefreshToken);
            }
        } catch (Exception exc) {
            // Server side error or similar
            Logger.e(TAG, "Error in refresh token for request:" + request.getLogInfo(),
                    ExceptionExtensions.getExceptionMessage(exc), ADALError.AUTH_FAILED_NO_TOKEN, exc);

            AuthenticationException authException = new AuthenticationException(ADALError.AUTH_FAILED_NO_TOKEN,
                    ExceptionExtensions.getExceptionMessage(exc), exc);
            callbackHandle.onError(authException);
            return null;
        }

        if (useCache) {
            if (result == null || StringExtensions.IsNullOrBlank(result.getAccessToken())) {
                String errLogInfo = result == null ? "" : result.getErrorLogInfo();
                Logger.w(TAG, "Refresh token did not return accesstoken.", request.getLogInfo() + errLogInfo,
                        ADALError.AUTH_FAILED_NO_TOKEN);

                // remove item from cache to avoid same usage of
                // refresh token in next acquireToken call
                removeItemFromCache(refreshItem);
                return acquireTokenLocalCall(callbackHandle, activity, useDialog, request);
            } else {
                Logger.v(TAG, "It finished refresh token request:" + request.getLogInfo());
                if (result.getUserInfo() == null && refreshItem.mUserInfo != null) {
                    Logger.v(TAG, "UserInfo is updated from cached result:" + request.getLogInfo());
                    result.setUserInfo(refreshItem.mUserInfo);
                    result.setIdToken(refreshItem.mRawIdToken);
                }

                // it replaces multi resource refresh token as
                // well with the new one since it is not stored
                // with resource.
                Logger.v(TAG, "Cache is used. It will set item to cache" + request.getLogInfo());
                setItemToCacheFromRefresh(refreshItem, request, result);

                // return result obj which has error code and
                // error description that is returned from
                // server response
                if (callbackHandle.callback != null) {
                    callbackHandle.onSuccess(result);
                }
                return result;
            }
        } else {

            // User is not using cache and explicitly
            // calling with refresh token. User should received
            // error code and error description in
            // Authentication result for Oauth errors
            Logger.v(TAG, "Cache is not used for Request:" + request.getLogInfo());
            if (callbackHandle.callback != null) {
                callbackHandle.onSuccess(result);
            }
            return result;
        }
    }

    private boolean validateAuthority(final URL authorityUrl) {

        // This is not calling outer callback. It is using
        // authenticationCallback, so handler is not needed here
        if (mDiscovery != null) {
            Logger.v(TAG, "Start validating authority");

            // Set CorrelationId for Instance Discovery
            mDiscovery.setCorrelationId(getRequestCorrelationId());
            try {
                boolean result = mDiscovery.isValidAuthority(authorityUrl);
                Logger.v(TAG, "Finish validating authority:" + authorityUrl + " result:" + result);
                return result;
            } catch (Exception exc) {
                Logger.e(TAG, "Instance validation returned error", "",
                        ADALError.DEVELOPER_AUTHORITY_CAN_NOT_BE_VALIDED, exc);

            }
        }
        return false;
    }

    private String getRedirectFromPackage() {
        return mContext.getApplicationContext().getPackageName();
    }

    /**
     * @param activity
     * @param request
     * @return false: if intent is not resolved or error in starting. true: if
     *         intent is sent to start the activity.
     */
    private boolean startAuthenticationActivity(final IWindowComponent activity, AuthenticationRequest request) {
        Intent intent = getAuthenticationActivityIntent(activity, request);

        if (!resolveIntent(intent)) {
            Logger.e(TAG, "Intent is not resolved", "", ADALError.DEVELOPER_ACTIVITY_IS_NOT_RESOLVED);
            return false;
        }

        try {
            // Start activity from callers context so that caller can intercept
            // when it is done
            activity.startActivityForResult(intent, AuthenticationConstants.UIRequest.BROWSER_FLOW);
        } catch (ActivityNotFoundException e) {
            Logger.e(TAG, "Activity login is not found after resolving intent", "",
                    ADALError.DEVELOPER_ACTIVITY_IS_NOT_RESOLVED, e);
            return false;
        }

        return true;
    }

    /**
     * Resolve activity from the package. If developer did not declare the
     * activity, it will not resolve.
     * 
     * @param intent
     * @return true if activity is defined in the package.
     */
    private final boolean resolveIntent(Intent intent) {
        ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivity(intent, 0);
        if (resolveInfo == null) {
            return false;
        }

        return true;
    }

    /**
     * Get intent to start authentication activity.
     * 
     * @param request
     * @return intent for authentication activity
     */
    private final Intent getAuthenticationActivityIntent(IWindowComponent activity, AuthenticationRequest request) {
        Intent intent = new Intent();
        if (AuthenticationSettings.INSTANCE.getActivityPackageName() != null) {
            // This will use the activity from another given package.
            intent.setClassName(AuthenticationSettings.INSTANCE.getActivityPackageName(),
                    AuthenticationActivity.class.getName());
        } else {
            // This will lookup the authentication activity within this context
            intent.setClass(mContext, AuthenticationActivity.class);
        }

        intent.putExtra(AuthenticationConstants.Browser.REQUEST_MESSAGE, request);
        return intent;
    }

    /**
     * Get the CorrelationId set by user.
     * 
     * @return UUID
     */
    public UUID getRequestCorrelationId() {
        if (mRequestCorrelationId == null) {
            return UUID.randomUUID();
        }

        return mRequestCorrelationId;
    }

    /**
     * set CorrelationId to requests.
     * 
     * @param mRequestCorrelationId
     */
    public void setRequestCorrelationId(UUID requestCorrelationId) {
        this.mRequestCorrelationId = requestCorrelationId;
        Logger.setCorrelationId(requestCorrelationId);
    }

    /**
     * Developer is using refresh token call to do refresh without cache usage.
     * App context or activity is not needed. Async requests are created,so this
     * needs to be called at UI thread.
     */
    private void refreshTokenWithoutCache(final String refreshToken, final String clientId, final String resource,
            final AuthenticationCallback<AuthenticationResult> externalCallback) {
        Logger.setCorrelationId(getRequestCorrelationId());
        Logger.v(TAG, "Refresh token without cache");

        if (StringExtensions.IsNullOrBlank(refreshToken)) {
            throw new IllegalArgumentException("Refresh token is not provided");
        }

        if (StringExtensions.IsNullOrBlank(clientId)) {
            throw new IllegalArgumentException("ClientId is not provided");
        }

        if (externalCallback == null) {
            throw new IllegalArgumentException("Callback is not provided");
        }

        final CallbackHandler callbackHandle = new CallbackHandler(getHandler(), externalCallback);

        // Execute all the calls inside Runnable to return immediately. All UI
        // related actions will be performed using Handler.
        sThreadExecutor.submit(new Runnable() {
            @Override
            public void run() {
                final URL authorityUrl = StringExtensions.getUrl(mAuthority);
                if (authorityUrl == null) {
                    callbackHandle
                            .onError(new AuthenticationException(ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_URL));

                    return;
                }

                final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId,
                        getRequestCorrelationId());

                // It is not using cache and refresh is not expected to
                // show authentication activity.
                request.setSilent(true);
                final RefreshItem refreshItem = new RefreshItem(refreshToken);

                if (mValidateAuthority) {
                    Logger.v(TAG, "Validating authority");

                    try {
                        if (validateAuthority(authorityUrl)) {
                            Logger.v(TAG, "Authority is validated" + authorityUrl.toString());
                        } else {
                            Logger.v(TAG, "Call callback since instance is invalid:" + authorityUrl.toString());
                            callbackHandle.onError(new AuthenticationException(
                                    ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_INSTANCE));
                            return;
                        }
                    } catch (Exception exc) {
                        Logger.e(TAG, "Authority validation is failed",
                                ExceptionExtensions.getExceptionMessage(exc), ADALError.SERVER_INVALID_REQUEST,
                                exc);
                        callbackHandle.onError(new AuthenticationException(ADALError.SERVER_INVALID_REQUEST,
                                "Authority validation is failed"));
                        return;
                    }
                }

                // Follow refresh logic now. Authority is valid or
                // skipped validation
                refreshToken(callbackHandle, null, false, request, refreshItem, false);
            }
        });
    }

    private synchronized Handler getHandler() {
        if (mHandler == null) {
            // Use current main looper
            mHandler = new Handler(mContext.getMainLooper());
        }

        return mHandler;
    }

    private static String extractAuthority(String authority) {
        if (!StringExtensions.IsNullOrBlank(authority)) {

            // excluding the starting https:// or http://
            int thirdSlash = authority.indexOf("/", EXCLUDE_INDEX);

            // third slash is not the last character
            if (thirdSlash >= 0 && thirdSlash != (authority.length() - 1)) {
                int fourthSlash = authority.indexOf("/", thirdSlash + 1);
                if (fourthSlash < 0 || fourthSlash > thirdSlash + 1) {
                    if (fourthSlash >= 0) {
                        return authority.substring(0, fourthSlash);
                    }

                    return authority;
                }
            }
        }

        throw new IllegalArgumentException("authority");
    }

    private void checkInternetPermission() {
        PackageManager pm = mContext.getPackageManager();
        if (PackageManager.PERMISSION_GRANTED != pm.checkPermission("android.permission.INTERNET",
                mContext.getPackageName())) {
            throw new AuthenticationException(ADALError.DEVELOPER_INTERNET_PERMISSION_MISSING);
        }
    }

    class DefaultConnectionService implements IConnectionService {

        private Context mConnectionContext;

        DefaultConnectionService(Context ctx) {
            mConnectionContext = ctx;
        }

        public boolean isConnectionAvailable() {
            ConnectivityManager connectivityManager = (ConnectivityManager) mConnectionContext
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo();
            boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();
            return isConnected;
        }
    }

    /**
     * Version name for ADAL not for the app itself.
     * 
     * @return Version
     */
    public static String getVersionName() {
        // Package manager does not report for ADAL
        // AndroidManifest files are not merged, so it is returning hard coded
        // value
        return "1.1.1";
    }
}