public class APIErrorResult {
    /**
     * 에러를 일으킨 요청 URL
     */
    private String requestURL;
    // {@link com.kakao.helper.ServerProtocol.ERROR_CODE_KEY}와 같은 변수 이름 유지. for jackson parsing
    /**
     * 숫자로 구성된 에러 코드
     */
    protected int errorCode;

    //{@link com.kakao.helper.ServerProtocol.ERROR_MSG_KEY}와 같은 변수 이름 유지. for jackson parsing
    /**
     *  String으로 구성된 상세한 에러 메시지
     */
    protected String errorMessage;

    // for jackson
    public APIErrorResult(int errorCode, String errorMessage) {
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }

    public APIErrorResult(final String requestURL, final String errorMessage) {
        this.requestURL = requestURL;
        this.errorCode = ErrorCode.CLIENT_ERROR_CODE.getErrorCode();
        this.errorMessage = errorMessage;
    }

    /**
     * 에러를 일으킨 요청 URL을 넘겨준다.
     * @return 요청 URL
     */
    public String getRequestURL() {
        return requestURL;
    }

    /**
     * 에러를 일으킨 요청 URL을 설정 한다.
     * @param requestURL 요청 URL
     */
    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    /**
     * 숫자로 구성된 에러 코드
     * @return 에러 코드
     */
    public int getErrorCodeInt() {
        return errorCode;
    }

    /**
     * Enum 에러 코드
     * @return {@link ErrorCode} 중 하나
     */
    public ErrorCode getErrorCode() {
        return ErrorCode.valueOf(errorCode);
    }

    /**
     * String으로 구성된 상세한 에러 메시지
     * @return 에러 메시지
     */
    public String getErrorMessage() {
        return errorMessage;
    }

    /**
     * 결과 객체를 String으로 표현
     * @return 요청 URL, 에러 코드, 에러 메시지를 포함한 string
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("APIErrorResult{");
        sb.append("requestURL='").append(requestURL).append('\'');
        sb.append(", errorCode=").append(errorCode);
        sb.append(", errorMessage='").append(errorMessage).append('\'');
        sb.append('}');
        return sb.toString();
    }
} public class Session {
    private static Session currentSession;
    private static final String APP_KEY_PROPERTY = "com.kakao.sdk.AppKey";
    private static final String REDIRECT_URL_PREFIX = "kakao";
    private static final String REDIRECT_URL_POSTFIX = "://oauth";


    private final Context context;
    private final String appKey;
    private final String redirectUri;
    private final SharedPreferencesCache appCache;
    private final List<SessionCallback> sessionCallbacks;
    private final Handler sessionCallbackHandler;

    private final Object INSTANCE_LOCK = new Object();
    // 아래 값들은 변경되는 값으로 INSTANCE_LOCK의 보호를 받는다.
    private SessionState state;
    // close시 삭제
    private RequestType requestType;
    private AuthorizationCode authorizationCode;
    private AccessToken accessToken;

    /**
     * 세션이 존재하지 않으면 세션을 생성하고, 기존에 존재하는데 만료되었으면 갱신을 시도한다.
     * 처음 세션을 접근할 때 사용한다.
     * opened 상태 : 다음 acitivity로
     * closed 상태 : 사용자 action을 받아 open 시도
     * opening 상태 : 토큰 갱신 시도
     * @param context 세션을 접근하는 context. 여기로 부터 app key와 redirect uri를 구해온다.
     * @param sessionCallback 토큰 갱신이 필요할 때 갱신의 결과를 받는 콜백
     */
    public static synchronized boolean initializeSession(final Context context, final SessionCallback sessionCallback) {
        if (currentSession == null) {
            currentSession = new Session(context);
        }
        return currentSession.implicitOpen(sessionCallback);
    }

    /**
     * 토큰 갱신이 가능한지 여부를 반환한다.
     * 토큰 갱신은 background로 사용자가 모르도록 진행한다.
     * @param sessionCallback 세션의 변경되었을 때 받게되는 콜백
     * @return 토큰 갱신을 진행할 때는 true, 토큰 갱신을 하지 못할때는 false를 return 한다.
     */
    public boolean implicitOpen(final SessionCallback sessionCallback) {
        if (currentSession.isOpening() && currentSession.accessToken.hasRefreshToken()) {
            open(sessionCallback);
            return true;
        } else {
            return false;
        }
    }

    /**
     * 현재 세션을 반환한다.
     * @return 현재 세션 객체
     */
    public static synchronized Session getCurrentSession() {
        if(currentSession == null)
            throw new IllegalStateException("Session is not initialized. Use Session#initializeSession(Context ,SessionCallback) in login process.");
        return currentSession;
    }

    /**
     * 세션 오픈을 진행한다.
     * {@link SessionState#OPENED} 상태이면 바로 종료.
     * {@link SessionState#CLOSED} 상태이면 authorization code 요청. 에러/취소시 {@link SessionState#CLOSED}
     * {@link SessionState#OPENING} 상태이면 code 또는 refresh token 이용하여  access token 을 받아온다. 에러/취소시 {@link SessionState#CLOSED}, refresh 취소시에만 {@link SessionState#OPENING} 유지.
     * param으로 받은 콜백으로 그 결과를 전달한다.
     * @param sessionCallback 오픈 결과를 받은 콜백
     */
    public void open(final SessionCallback sessionCallback) {
        // 이미 open이 되어 있다.
        if (getState().isOpened()) {
            return;
        }

        addCallback(sessionCallback);

        //끝나지 않은 request가 있다.
        if(getRequestType() != null){
            Logger.getInstance().d(getRequestType() + " is still doing.");
            return;
        }
        try {
            checkLoginActivity();
            synchronized (INSTANCE_LOCK){
                switch (state) {
                    case CLOSED:
                        if (appKey != null && redirectUri != null) {
                            this.requestType = RequestType.GETTING_AUTHORIZATION_CODE;
                            final AuthorizationCodeRequest authorizationCodeRequest = AuthorizationCodeRequest.createNewRequest(appKey, redirectUri);
                            requestLogin(getLoginActivityIntent(authorizationCodeRequest));
                        } else {
                            internalClose(new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, "can not request authorization code because appKey or redirectUri is invalid."), false);
                        }
                        break;
                    case OPENING:
                        if(accessToken.hasRefreshToken()){
                            this.requestType = RequestType.REFRESHING_ACCESS_TOKEN;
                            final  AccessTokenRequest accessTokenRequest = AccessTokenRequest.createRequestWithRefreshToken(context, appKey, redirectUri, accessToken.getRefreshTokenString());
                            requestLogin(getLoginActivityIntent(accessTokenRequest));
                        } else if(authorizationCode.hasAuthorizationCode()){
                            this.requestType = RequestType.GETTING_ACCESS_TOKEN;
                            final AccessTokenRequest accessTokenRequest = AccessTokenRequest.createRequestWithAuthorizationCode(context, appKey, redirectUri, authorizationCode.getAuthorizationCode());
                            requestLogin(getLoginActivityIntent(accessTokenRequest));
                        } else {
                            internalClose(new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, "can not request access token because both authorization code and refresh token are invalid."), false);
                        }
                        break;
                    default:
                        throw new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, "current session state is not possible to open. state = " + state);
                }
            }
        } catch (KakaoException e) {
            internalClose(e, false);
        }
    }

    /**
     * 명시적 강제 close(로그아웃/탈퇴). request중 인 것들은 모두 실패를 받게 된다.
     * token을 삭제하기 때문에 authorization code부터(로그인 버튼) 다시 받아서 세션을 open 해야한다.
     * @param sessionCallback close 결과를 받고자 하는 callback
     */
    public void close(final SessionCallback sessionCallback) {
        if(sessionCallback != null)
            addCallback(sessionCallback);

        internalClose(null, true);
    }

    /**
     * 현재 세션이 가지고 있는 access token이 유효한지를 검사후 세션의 상태를 반환한다.
     * 만료되었다면 opened 상태가 아닌 opening상태가 반환된다.
     * @return 세션의 상태
     */
    public final SessionState checkState() {
        synchronized (INSTANCE_LOCK) {
            if(state.isOpened() && !accessToken.hasValidAccessToken()){
                synchronized (INSTANCE_LOCK) {
                    state = SessionState.OPENING;
                    requestType = null;
                    authorizationCode = AuthorizationCode.createEmptyCode();
                }
            }
            return state;
        }
    }

    /**
     * 현재 세션의 상태
     * @return 세션의 상태
     */
    public SessionState getState() {
        synchronized (INSTANCE_LOCK) {
            return state;
        }
    }

    /**
     * 현재 세션이 열린 상태인지 여부를 반환한다.
     * @return 세션이 열린 상태라면 true, 그외의 경우 false를 반환한다.
     */
    public final boolean isOpened() {
        final SessionState state = checkState();
        return state == SessionState.OPENED;
    }

    private boolean isOpening() {
        final SessionState state = checkState();
        return state == SessionState.OPENING;
    }

    /**
     * 현재 세션이 닫힌 상태인지 여부를 반환한다.
     * @return 세션이 닫힌 상태라면 true, 그외의 경우 false를 반환한다.
     */
    public final boolean isClosed() {
        final SessionState state = checkState();
        return state == SessionState.CLOSED;
    }

    /**
     * 현재 진행 중인 요청 타입
     * @return 현재 진행 중인 요청 타입
     */
    public final RequestType getRequestType() {
        synchronized (INSTANCE_LOCK) {
            return requestType;
        }
    }

    /**
     * 현재 세션이 가지고 있는 access token을 반환한다.
     * @return access token
     */
    public final String getAccessToken() {
        synchronized (INSTANCE_LOCK) {
            return (accessToken == null) ? null : accessToken.getAccessTokenString();
        }
    }

    /**
     * 앱 캐시를 반환한다.
     * @return 앱 캐시
     */
    public static SharedPreferencesCache getAppCache() {
        final Session session = Session.getCurrentSession();
        return session.appCache;
    }

    /**
     * authorization code 결과를 받아 처리한다. (authcode 저장, state 변경, accesstoken요청)
     */
    public void onAuthCodeCompleted(final AuthorizationResult result) {
        AuthorizationCode authCode = null;
        KakaoException exception = null;
        if(getRequestType() == null){
            exception = new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, "session is closed during requesting authorization code. result will be ignored. state = " + getState());
        } else if (result == null) {
            exception = new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, "the result of authorization code request is null.");
        } else {
            final String resultRedirectURL = result.getRedirectURL();
            if(result.isSuccess()){
                // 기대 했던 redirect uri 일치
                if (resultRedirectURL != null && resultRedirectURL.startsWith(redirectUri)) {
                    authCode = AuthorizationCode.createFromRedirectedUri(result.getRedirectUri());
                    // authorization code가 포함되지 않음
                    if (!authCode.hasAuthorizationCode()) {
                        authCode = null;
                        exception = new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, "the result of authorization code request does not have authorization code.");
                    }
                // 기대 했던 redirect uri 불일치
                } else {
                    exception = new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, "the result of authorization code request mismatched the registered redirect uri. msg = " + result.getResultMessage());
                }
            } else if (result.isCanceled()) {
                exception = new KakaoException(ERROR_TYPE.CANCELED_OPERATiON, result.getResultMessage());
            } else {
                exception = new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, result.getResultMessage());
            }
        }

        synchronized (INSTANCE_LOCK) {
            final SessionState previousState = state;
            if (authCode != null) {
                this.authorizationCode = authCode;
                state = SessionState.OPENING;
                // log만 남기고 callback은 호출되지 않는다.
                onStateChange(previousState, state, requestType, null, false);
                //  request가 성공적으로 끝났으니 request는 reset
                requestType = null;
            } else {
                internalClose(exception, false);
                return;
            }
        }
        // request AccessToken
        open(null);
    }

    /**
     * access token 결과를 받아 처리한다. (access token 저장, state 변경)
     */
    public void onAccessTokenCompleted(final AuthorizationResult result) {
        AccessToken resultAccessToken = null;
        KakaoException exception = null;
        if(getRequestType() == null){
            exception = new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, "session is closed during requesting access token. result will be ignored. state = " + getState());
        } else if (result == null) {
            exception = new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, "the result of access token request is null.");
        } else {
            if (result.isSuccess()) {
                resultAccessToken = result.getAccessToken();
                if (!resultAccessToken.hasValidAccessToken()) {
                    resultAccessToken = null;
                    exception = new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, "the result of access token request is invalid access token.");
                }
            } else if (result.isCanceled()) {
                exception = new KakaoException(ERROR_TYPE.CANCELED_OPERATiON, result.getResultMessage());
            } else {
                exception = new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, result.getResultMessage());
            }
        }

        synchronized (INSTANCE_LOCK) {
            final SessionState previousState = state;
            if (resultAccessToken != null) {
                // refresh 요청에는 refresh token이 내려오지 않을 수 있으므로 accessToken = resultAccessToken을 하면 안된다.
                accessToken.updateAccessToken(resultAccessToken);
                //authorization code는 한번 밖에 사용하지 못한다.
                authorizationCode = AuthorizationCode.createEmptyCode();
                saveTokenToCache(accessToken);
                state = SessionState.OPENED;
                onStateChange(previousState, state, requestType, null, false);
                requestType = null;
            } else {
                // refresh token으로 요청했는데 취소를 한 경우는 다음에 다시 refresh token을 사용할 수 있으므로 close시키진 않는다.
                if(!(getRequestType().isRefreshingTokenRequest() && exception.isCancledOperation())){
                    internalClose(exception, false);
                }
            }
        }
    }

    private Session(final Context context){
        if(context == null)
            throw new KakaoException(ERROR_TYPE.ILLEGAL_ARGUMENT, "cannot create Session without Context.");

        this.context = context;
        this.appKey = Utility.getMetadata(context, APP_KEY_PROPERTY);
        if(appKey == null)
            throw new KakaoException(ERROR_TYPE.MISS_CONFIGURATION, String.format("need to declare %s in your AndroidManifest.xml", APP_KEY_PROPERTY));
        this.redirectUri = REDIRECT_URL_PREFIX + this.appKey + REDIRECT_URL_POSTFIX;
        this.appCache = new SharedPreferencesCache(context, appKey);
        this.sessionCallbacks = new ArrayList<SessionCallback>();
        this.sessionCallbackHandler = new Handler(Looper.getMainLooper()); //세션 callback은 main thread에서 호출되도록 한다.

        final Bundle loadedFromCache = appCache.load();
        synchronized (INSTANCE_LOCK) {
            authorizationCode = AuthorizationCode.createEmptyCode();
            accessToken = AccessToken.createFromCache(loadedFromCache);
            if (accessToken.hasValidAccessToken()) {
                this.state = SessionState.OPENED;
            } else if (accessToken.hasRefreshToken()) {
                this.state = SessionState.OPENING;
            } else {
                this.state = SessionState.CLOSED;
                internalClose(null, false);
            }
        }
    }

    /**
     * @param callback 추가할 세션 콜백
     */
    private void addCallback(final SessionCallback callback) {
        synchronized (sessionCallbacks) {
            if (callback != null && !sessionCallbacks.contains(callback)) {
                sessionCallbacks.add(callback);
            }
        }
    }

    private void removeCallbacks(final List<SessionCallback> sessionCallbacksToBeRemoved) {
        synchronized (sessionCallbacks) {
            sessionCallbacks.removeAll(sessionCallbacksToBeRemoved);
        }
    }

    private void checkLoginActivity() throws KakaoException {
        Intent intent = new Intent();
        intent.setClass(context, LoginActivity.class);
        if (!resolveIntent(intent)) {
            throw new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, String.format("need to declare %s as an activity in your AndroidManifest.xml",
                LoginActivity.class.getName()));
        }
    }

    private boolean resolveIntent(final Intent intent) {
        final ResolveInfo resolveInfo = Utility.resolveIntent(context, intent);
        return resolveInfo != null;
    }

    private Intent getLoginActivityIntent(final AuthorizationCodeRequest authCodeRequest) {
        final Intent intent = new Intent(context, LoginActivity.class);
        intent.putExtra(LoginActivity.CODE_REQUEST_KEY, authCodeRequest);
        return intent;
    }

    private Intent getLoginActivityIntent(final AccessTokenRequest accessTokenRequest) {
        final Intent intent = new Intent(context, LoginActivity.class);
        intent.putExtra(LoginActivity.TOKEN_REQUEST_KEY, accessTokenRequest);
        return intent;
    }

    private void requestLogin(final Intent intent) {
        boolean found = startLoginActivity(intent);
        if (!found) {
            internalClose(new KakaoException(ERROR_TYPE.AUTHORIZATION_FAILED, "failed to find LoginActivity"), false);
        }
    }

    private boolean startLoginActivity(final Intent intent) {
        try {
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent);
        } catch (ActivityNotFoundException e) {
            return false;
        }
        return true;
    }

    /**
     * 세션을 close하여 처음부터 새롭게 세션 open을 진행한다.
     * @param kakaoException  exception이 발생하여 close하는 경우 해당 exception을 넘긴다.
     * @param forced 강제 close 여부. 강제 close이면 이미 close가 되었어도 callback을 호출한다.
     */
    private void internalClose(final KakaoException kakaoException, final boolean forced) {
        synchronized (INSTANCE_LOCK) {
            final SessionState previous = state;
            state = SessionState.CLOSED;
            requestType = null;
            authorizationCode = AuthorizationCode.createEmptyCode();
            accessToken = AccessToken.createEmptyToken();
            onStateChange(previous, state, requestType, kakaoException, forced);
        }
        if (this.appCache != null) {
            this.appCache.clearAll();
        }
        // 해당 도메인 cookie만 지우려고 했으나 CookieManager가 관리하는 cookie가 한 app에 대한 cookie여서 모두 날려도 되겠다.
        // CookieManager를 쓰려면 CookieSyncManager를 만들어야 하는 버그가 있다.
        CookieSyncManager.createInstance(context.getApplicationContext());
        CookieManager.getInstance().removeAllCookie();
    }

    private void saveTokenToCache(final AccessToken newToken) {
        if (newToken != null && appCache != null) {
            newToken.saveAccessTokenToCache(appCache);
        }
    }

    private void onStateChange(final SessionState previousState, final SessionState newState,
                               final RequestType requestType, final KakaoException exception,
                               boolean forced) {
        if (!forced && (previousState == newState) && exception == null) {
            return;
        }

        Logger.getInstance().d(String.format("Session State changed : %s -> %s \n ex = %s, request_type = %s",previousState, newState ,(exception != null ? ", ex=" + exception.getMessage() : ""), requestType));

        // 사용자에게 opening을 state를 알려줄 필요는 없는듯.
        if( newState.isOpening())
            return;

        final List<SessionCallback> dumpSessionCallbacks = new ArrayList<SessionCallback>(sessionCallbacks);
        Runnable runCallbacks = new Runnable() {
            public void run() {
                for(SessionCallback callback : dumpSessionCallbacks){
                    if (newState.isOpened())
                        callback.onSessionOpened();
                    else if (newState.isClosed())
                        callback.onSessionClosed(exception);

                }
                removeCallbacks(dumpSessionCallbacks);
            }
        };
        //세션 callback은 main thread에서 호출되도록 한다.
        sessionCallbackHandler.post(runCallbacks);
    }

    /**
     * @author MJ
     */
    private static enum SessionState {
        /**
         * memory와 cache에 session 정보가 없는 전혀 상태.
         * 처음 session에 접근할 때 또는 session을 close(예를 들어 로그아웃, 탈퇴)한 상태.
         * open({@link com.kakao.Session.RequestType#GETTING_AUTHORIZATION_CODE}) : 성공 - {@link #OPENING}, 실패 - 그대로 CLOSED
         * close(명시적 close) : 그대로 CLOSED
         */
        CLOSED,
        /**
         * {@link #CLOSED}상태에서 token을 발급 받기 위해 authorization code를 발급 받아 valid한 authorization code를 가지고 있는 상태.
         * 또는 토큰이 만료되었으나 refresh token을 가지고 있는 상태.
         * open({@link com.kakao.Session.RequestType#GETTING_ACCESS_TOKEN} 또는 {@link com.kakao.Session.RequestType#REFRESHING_ACCESS_TOKEN}) :  성공 - {@link #OPENED}, 실패 - {@link #CLOSED}
         * close(명시적 close) : {@link #CLOSED}
         */
        OPENING,
        /**
         * access token을 성공적으로 발급 받아 valid access token을 가지고 있는 상태.
         * 토크 만료 : {@link #OPENING}
         * close(명시적 close) : {@link #CLOSED}
         */
        OPENED;

        private boolean isClosed(){
            return this == SessionState.CLOSED;
        }

        private boolean isOpening(){
            return this == SessionState.OPENING;
        }

        private boolean isOpened(){
            return this == SessionState.OPENED;
        }
    }

    private enum RequestType {
        GETTING_AUTHORIZATION_CODE,
        GETTING_ACCESS_TOKEN,
        REFRESHING_ACCESS_TOKEN;

        private boolean isAuthorizationCodeRequest() {
            return this == RequestType.GETTING_AUTHORIZATION_CODE;
        }

        private boolean isAccessTokenRequest() {
            return this == RequestType.GETTING_ACCESS_TOKEN;
        }

        private boolean isRefreshingTokenRequest() {
            return this == RequestType.REFRESHING_ACCESS_TOKEN;
        }
    }
} public interface SessionCallback {
    /**
     * access token을 성공적으로 발급 받아 valid access token을 가지고 있는 상태.
     * 일반적으로 로그인 후의 다음 activity로 이동한다.
     */
    public void onSessionOpened();

    /**
     * memory와 cache에 session 정보가 전혀 없는 상태.
     * 일반적으로 로그인 버튼이 보이고 사용자가 클릭시 동의를 받아 access token 요청을 시도한다.
     * @param exception   close된 이유가 에러가 발생한 경우에 해당 exception.
     *
     */
    public void onSessionClosed(final KakaoException exception);
} public class KakaoException extends RuntimeException {
    private static final long serialVersionUID = 3738785191273730776L;
    private ERROR_TYPE errorType;

    public enum ERROR_TYPE{
        ILLEGAL_ARGUMENT,
        MISS_CONFIGURATION,
        CANCELED_OPERATiON,
        AUTHORIZATION_FAILED,
    }

    public KakaoException(final ERROR_TYPE errorType, final String msg) {
        super(msg);
        this.errorType = errorType;
    }

    public boolean isCancledOperation() {
        return errorType == ERROR_TYPE.CANCELED_OPERATiON;
    }

    public KakaoException(final String msg) {
        super(msg);
    }

    public KakaoException(final Throwable t) {
        super(t);
    }

    public KakaoException(final String s, final Throwable t) {
        super(s, t);
    }

    public String getMessage(){
        if(errorType != null)
            return errorType.toString() +" : " + super.getMessage();
        else
            return super.getMessage();
    }
} public final class ServerProtocol {
    public static enum DeployPhase {
        Local, Alpha, Sandbox, Beta, Release
    }

    private static final DeployPhase DEPLOY_PHASE = DeployPhase.Release;

    public static final String SDK_VERSION = "1.0.16";
    public static final String OS_ANDROID = "android";

    public static final String URL_SCHEME = "https";
    public static final String AUTH_AUTHORITY = initAuthAuthority();
    public static final String API_AUTHORITY = initAPIAuthority();

    //KAHeader
    public static final String KA_HEADER_KEY ="KA";
    public static final String KA_SDK_KEY ="sdk/";
    public static final String KA_OS_KEY ="os/";
    public static final String KA_LANG_KEY ="lang/";
    public static final String KA_RES_KEY ="res/";
    public static final String KA_DEVICE_KEY ="device/";

    //Authorization: Bearer
    public static final String AUTHORIZATION_HEADER_KEY ="Authorization";
    public static final String AUTHORIZATION_BEARER ="Bearer";

    // oauth url
    public static final String AUTHORIZE_CODE_PATH = "oauth/authorize";
    public static final String ACCESS_TOKEN_PATH = "oauth/token";

    //oauth param
    public static final String CLIENT_ID_KEY = "client_id";
    public static final String REDIRECT_URI_KEY = "redirect_uri";
    public static final String RESPONSE_TYPE_KEY = "response_type";
    public static final String GRANT_TYPE_KEY = "grant_type";
    public static final String CODE_KEY = "code";
    public static final String ACCESS_TOKEN_KEY = "access_token";
    public static final String REFRESH_TOKEN_KEY = "refresh_token";
    public static final String EXPIRES_AT_KEY = "expires_in";
    public static final String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
    public static final String ANDROID_KEY_HASH = "android_key_hash";

    // oauth response
    public static final String ERROR_KEY = "error";
    public static final String ERROR_DESCRIPTION_KEY = "error_description";

    // api url
    private static final String API_VERSION = "v1";
    // usermgmt
    public static final String USER_ME_PATH = API_VERSION + "/user/me";
    public static final String USER_LOGOUT_PATH = API_VERSION + "/user/logout";
    public static final String USER_SIGNUP_PATH = API_VERSION + "/user/signup";
    public static final String USER_UNLINK_PATH = API_VERSION + "/user/unlink";
    public static final String USER_UPDATE_PROFILE_PATH = API_VERSION + "/user/update_profile";
    // story
    public static final String STORY_PROFILE_PATH = API_VERSION + "/api/story/profile";
    public static final String STORY_UPLOAD_PATH = API_VERSION + "/api/story/upload";
    public static final String STORY_POST_PATH = API_VERSION + "/api/story/post";
    //talk
    public static final String TALK_PROFILE_PATH = API_VERSION + "/api/talk/profile";


    //api response
    /**
     * {@link com.kakao.APIErrorResult} ????????? ?????? ????????? ???.
     */
    public static final String ERROR_CODE_KEY ="code";
    public static final String ERROR_MSG_KEY ="msg";

    // usermgmt param
    public static final String USER_ID_KEY ="id";
    public static final String PROPERTIES_KEY = "properties";
    /**
     * Policy.RESERVED_PROPERTY_NAMES ??? match
     */
    public static final String NICK_NAME_KEY = "nickname";
    public static final String PROFILE_THUMBNAIL_IMAGE_KEY = "thumbnail_image";
    public static final String PROFILE_IMAGE_KEY = "profile_image";

    // story param
    public static final String BODY_ENCODING ="UTF-8";
    public static final String FILE_KEY ="file";
    public static final String CONTENT_KEY ="content";
    public static final String PERMISSION_KEY ="permission";
    public static final String IMAGE_URL_KEY ="image_url";
    public static final String APP_CAPTION_TITLE_KEY ="app_caption_title";
    public static final String ANDROID_EXEC_PARAM_KEY ="android_exec_param";
    public static final String IOS_EXEC_PARAM_KEY ="ios_exec_param";

    private static String initAuthAuthority() {
        switch (DEPLOY_PHASE) {
            case Local:
                return "localhost:";
            case Alpha:
                return "alpha-kauth.kakao.com";
            case Sandbox:
                return "sandbox-kauth.kakao.com";
            case Beta:
                return "beta-kauth.kakao.com";
            case Release:
                return "kauth.kakao.com";
            default:
                return null;
        }
    }

    private static String initAPIAuthority() {
        switch (DEPLOY_PHASE) {
            case Local:
                return "localhost:";
            case Alpha:
                return "alpha-kapi.kakao.com";
            case Sandbox:
                return "sandbox-kapi.kakao.com";
            case Beta:
                return "beta-kapi.kakao.com";
            case Release:
                return "kapi.kakao.com";
            default:
                return null;
        }
    }

} public abstract class HttpRequestBuilder {

    public static HttpRequestBuilder get(final String url) {
        return new HttpGetRequestBuilder(url);
    }

    public static HttpRequestBuilder post(final String url) {
        return new HttpPostRequestBuilder(url);
    }


    protected final String url;
    protected List<NameValuePair> parameters;
    protected Map<String, String> headers;
    protected String charset;

    protected HttpRequestBuilder(final String url) {
        if (url == null || url.length() == 0) {
            throw new IllegalArgumentException("url == null or empty");
        }
        this.url = url;
        this.parameters = new ArrayList<NameValuePair>();
        this.headers = new HashMap<String, String>();
        this.charset = Charset.forName("UTF-8").name();
    }

    public HttpRequestBuilder addQueryParameter(final String key, final String value) {
        this.parameters.add(new BasicNameValuePair(key, value));
        return this;
    }

    public HttpRequestBuilder addHeader(final String key, final String value) {
        this.headers.put(key, value);
        return this;
    }

    public abstract Request build();

    public HttpRequestBuilder setBodyEncoding(String bodyEncoding) {
        this.charset = bodyEncoding;
        return this;
    }

    public abstract void addBodyPart(BodyPart part);


    private static class HttpGetRequestBuilder extends HttpRequestBuilder {

        protected HttpGetRequestBuilder(String url) {
            super(url);
        }

        @Override
        public Request build() {
            final Uri.Builder builder = new Uri.Builder();
            final Uri uri = Uri.parse(url);

            builder.scheme(uri.getScheme()).path(uri.getPath()).authority(uri.getAuthority());
            for (NameValuePair pair : parameters) {
                builder.appendQueryParameter(pair.getName(), pair.getValue());
            }

            final String buildUrl = builder.build().toString();

            return new Request() {
                @Override
                public String getUrl() {
                    return buildUrl;
                }

                @Override
                public Map<String, String> getHeaders() {
                    return headers;
                }

                @Override
                public String getMethod() {
                    return "GET";
                }

                @Override
                public boolean hasPayload() {
                    return false;
                }

                @Override
                public String getCharSet() {
                    return charset;
                }

                @Override
                public Object getPayload() {
                    return null;
                }
            };
        }

        @Override
        public void addBodyPart(BodyPart part) {
            throw new UnsupportedOperationException();
        }
    }

    private static class HttpPostRequestBuilder extends HttpRequestBuilder {

        protected Multipart multipart;

        protected HttpPostRequestBuilder(String url) {
            super(url);
            multipart = null;
        }

        @Override
        public Request build() {
            final Object[] payload = new Object[]{null};

            if (multipart != null) {
                payload[0] = multipart;
            } else {
                if (parameters != null && parameters.size() > 0) {
                    UrlEncodedFormEntity entity = null;
                    try {
                        entity = new UrlEncodedFormEntity(parameters, charset);
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    }
                    payload[0] = entity;
                }
            }

            return new Request() {
                @Override
                public String getUrl() {
                    return url;
                }

                @Override
                public Map<String, String> getHeaders() {
                    return headers;
                }

                @Override
                public String getMethod() {
                    return "POST";
                }

                @Override
                public boolean hasPayload() {
                    return payload[0] != null;
                }

                @Override
                public String getCharSet() {
                    return charset;
                }

                @Override
                public Object getPayload() {
                    return payload[0];
                }
            };
        }

        @Override
        public void addBodyPart(BodyPart part) {
            if (multipart == null) {
                multipart = new Multipart(charset);
            }

            multipart.addBodyPart(part);
        }
    }
} public class HttpRequestTask<T> implements Runnable {
    public static final int SUCCESS = 1;
    public static final int ERROR = 2;
    public static final int NEED_TO_LOGIN = 4;

    public static final HashMap<String,String> KA_HEADER = createKAHeader();

    private static final AsyncHttpClient ASYNC_HTTP_CLIENT = newAsyncHttpClient();

    private static final int DEFAULT_CONNECTION_TO_IN_MS = 5000;
    private static final int DEFAULT_REQUEST_TO_IN_MS = 5000;

    private final KakaoAsyncHandler<T> asyncHandler;
    protected final Request request;

    public HttpRequestTask(final Request request, final KakaoAsyncHandler<T> asyncHandler){
        this.request = request;
        this.asyncHandler = asyncHandler;
    }

    private static HashMap<String, String> createKAHeader() {
        HashMap<String, String>  kaHeader = new HashMap<String, String>();
        StringBuilder kaHeaderBuilder = new StringBuilder();
        kaHeaderBuilder.append(ServerProtocol.KA_SDK_KEY).append(ServerProtocol.SDK_VERSION).append(" ")
            .append(ServerProtocol.KA_OS_KEY).append(ServerProtocol.OS_ANDROID).append("-").append(SystemInfo.OS_VERSION).append(" ")
            .append(ServerProtocol.KA_LANG_KEY).append(SystemInfo.LANGUAGE).append("-").append(SystemInfo.COUNTRY_CODE).append(" ")
            .append(ServerProtocol.KA_RES_KEY).append(SystemInfo.getDisplayResolution()).append(" ")
            .append(ServerProtocol.KA_DEVICE_KEY).append(SystemInfo.DEVICE_MODEL);

        kaHeader.put(ServerProtocol.KA_HEADER_KEY, kaHeaderBuilder.toString());
        return kaHeader;
    }

    public static String createBaseURL(final String authority, final String requestPath) {
        Uri uri = Utility.buildUri(authority, requestPath);
        return uri.toString();
    }

    @Override
    public void run() {
        try {
            preRequest();
            ASYNC_HTTP_CLIENT.executeRequest(request, asyncHandler);
        } catch (Exception e) {
            Logger.getInstance().e(e);
            failedRequest(e);
        }
    }

    protected void preRequest() {
    }

    protected void failedRequest(Exception e) {
    }

    private static AsyncHttpClient newAsyncHttpClient() {
        return new AsyncHttpClient(
                HttpTaskManager.getHttpExecutor(),
                DEFAULT_CONNECTION_TO_IN_MS,
                DEFAULT_REQUEST_TO_IN_MS
        );
    }

    public static void addQueryParams(HttpRequestBuilder requestBuilder, Bundle parameters) {
        Set<String> keys = parameters.keySet();
        for (String key : keys) {
            Object value = parameters.get(key);

            if (value == null) {
                value = "";
            }

            if (isSupportedParameterType(value)) {
                value = parameterToString(value);
            } else {
                throw new IllegalArgumentException(String.format("Unsupported parameter type for GET request: %s",
                    value.getClass().getSimpleName()));
            }

            requestBuilder.addQueryParameter(key, value.toString());
        }
    }

    private static boolean isSupportedParameterType(final Object value) {
        return value instanceof String || value instanceof Boolean || value instanceof Number;
    }

    private static String parameterToString(final Object value) {
        if (value instanceof String) {
            return (String) value;
        } else if (value instanceof Boolean || value instanceof Number) {
            return value.toString();
        }
        throw new IllegalArgumentException("Unsupported parameter type.");
    }


} public abstract class HttpResponseHandler<T> extends Handler {

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case HttpRequestTask.SUCCESS:
                onHttpSuccess((T) msg.obj);
                break;
            case HttpRequestTask.ERROR:
                onHttpFailure((APIErrorResult) msg.obj);
                break;
            case HttpRequestTask.NEED_TO_LOGIN:
                onHttpSessionClosedFailure((APIErrorResult) msg.obj);
                break;
        }
    }

    /**
     * 요청한 request가 성공한 경우 호출된다.
     * @param resultObj 성공한 결과
     */
    protected abstract void onHttpSuccess(final T resultObj);

    /**
     * access token, refresh token이 모두 만료되어 다시 로그인이 필요한 경우 호출된다.
     * 다시 로그인 창으로 redirect하도록 한다.
     */
    protected abstract void onHttpSessionClosedFailure(final APIErrorResult errorResult);

    /**
     * request 요청을 완료했으나 서버에서 실패 결과를 준 경우 또는
     * request 요청을 완료하지 못했거나 완료했는데 결과처리 중에 문제가 발생한 경우 호출된다.
     * @param errorResult 실패한 결과
     */
    protected abstract void onHttpFailure(final APIErrorResult errorResult);
} public class HttpTaskManager {
    private static final int DEFAULT_CORE_POOL_SIZE = 0;
    private static final int DEFAULT_MAXIMUM_POOL_SIZE = Integer.MAX_VALUE;
    private static final long DEFAULT_KEEP_ALIVE = 60L;
    private static final BlockingQueue<Runnable> DEFAULT_WORK_QUEUE = new SynchronousQueue<Runnable>();

    private static final ThreadFactory DEFAULT_THREAD_FACTORY = new ThreadFactory() {
        private final AtomicInteger counter = new AtomicInteger(0);
        public Thread newThread(Runnable runnable) {
            return new Thread(runnable, "KakaoHttpTask #" + counter.incrementAndGet());
        }
    };

    private static final ExecutorService defaultExecutor = new ThreadPoolExecutor(DEFAULT_CORE_POOL_SIZE, DEFAULT_MAXIMUM_POOL_SIZE,
        DEFAULT_KEEP_ALIVE, TimeUnit.SECONDS, DEFAULT_WORK_QUEUE, DEFAULT_THREAD_FACTORY);

    private static ExecutorService httpExecutor = defaultExecutor;

    public static ExecutorService getHttpExecutor() {
        return httpExecutor;
    }

    // default configuration을 쓰고 싶지 않으면 HttpTaskManager를 처음 사용하기 전에 호출해 준다.
    public static void setHttpExecutor(ExecutorService httpExecutor) {
        HttpTaskManager.httpExecutor = httpExecutor;
    }

    public static void execute(HttpRequestTask httpRequest){
        httpExecutor.execute(httpRequest);
    }
} public abstract class KakaoAsyncHandler<T> {
    protected final HttpResponseHandler<T> httpResponseHandler;
    protected final Request request;
    protected final Class<T> returnType;

    public KakaoAsyncHandler(final Request request, final HttpResponseHandler<T> httpResponseHandler, final Class<T> returnType) {
        this.request = request;
        this.httpResponseHandler = httpResponseHandler;
        this.returnType = returnType;
    }

    public Void onCompleted(final Response response) throws Exception {
        final URI requestUri = response.getUri();
        try {
            if (!response.hasResponseStatus()) {
                sendError(response, "the response didn't have a response status");
                return null;
            }

            final int httpStatusCode = response.getStatusCode();
            if (httpStatusCode != HttpStatus.SC_OK) {
                return handleFailureHttpStatus(response, requestUri, httpStatusCode);
            } else {
                if(returnType.equals(Void.class)){
                    httpResponseHandler.sendMessage(Message.obtain(httpResponseHandler, HttpRequestTask.SUCCESS, 0, 0));
                } else {
                    Object result = null;
                    if (checkResponseBody(response)) return null;
                    if (APIErrorResult.class.equals(returnType)) {
                        JSONObject json = new JSONObject(response.getResponseBody());
                        result = new APIErrorResult(
                                json.getInt("code"),
                                json.getString("msg")
                        );
                    } else if (User.class.equals(returnType)) {
                        JSONObject json = new JSONObject(response.getResponseBody());
                        result = new User(json.getLong("id"));
                    } else if (KakaoTalkProfile.class.equals(returnType)) {
                        JSONObject json = new JSONObject(response.getResponseBody());
                        result = new KakaoTalkProfile(
                                json.getString("nickName"),
                                json.getString("profileImageURL"),
                                json.getString("thumbnailURL"),
                                json.getString("countryISO")
                        );
                    } else if (KakaoStoryUpload.class.equals(returnType)) {
                        JSONObject json = new JSONObject(response.getResponseBody());
                        result = new KakaoStoryUpload(
                                json.getString("url")
                        );
                    } else if (KakaoStoryProfile.class.equals(returnType)) {
                        JSONObject json = new JSONObject(response.getResponseBody());

                        result = new KakaoStoryProfile(
                                json.getString("nickName"),
                                json.getString("profileImageURL"),
                                json.getString("thumbnailURL"),
                                json.getString("bgImageURL"),
                                json.getString("birthday"),
                                "+".equals(json.getString("birthdayType")) ? KakaoStoryProfile.BirthdayType.SOLAR : KakaoStoryProfile.BirthdayType.LUNAR
                        );
                    } else if (Map.class.equals(returnType)) {
                        JSONObject json = new JSONObject(response.getResponseBody());
                        result = JsonHelper.MapFromJson(json);
                    } else if (List.class.equals(returnType)) {
                        JSONArray json = new JSONArray(response.getResponseBody());
                        result = JsonHelper.ListFromJson(json);
                    } else {
                        throw new IllegalStateException("unknown result type " + returnType);
                    }
                    httpResponseHandler.sendMessage(Message.obtain(httpResponseHandler, HttpRequestTask.SUCCESS, 0, 0, result));
                }
                return null;
            }
        } catch (Exception e) {
            sendError(response, e.toString());
            return null;
        }
    }

    public void onThrowable(final Throwable t) {
        httpResponseHandler.sendMessage(Message.obtain(httpResponseHandler, HttpRequestTask.ERROR, 0, 0,
            new APIErrorResult(request.getUrl(), "error occurred during http request. t= "+ t.toString())));
    }

    protected void sendError(final Response response, final String msg) {
        httpResponseHandler.sendMessage(Message.obtain(httpResponseHandler, HttpRequestTask.ERROR, 0, 0,
            new APIErrorResult(request.getUrl(), "http status =  "+ response.getStatusText() + " msg = " + msg)));
    }

    protected boolean checkResponseBody(final Response response) {
        if (!response.hasResponseBody()) {
            sendError(response, "the response didn't have a body");
            return true;
        }
        return false;
    }

    protected abstract Void handleFailureHttpStatus(Response response, URI requestUri, int httpStatusCode) throws IOException;
} public interface Request {

    /**
     * @return 요청 주소
     */
    String getUrl();

    /**
     * @return 요청에 포함할 Header
     */
    Map<String, String> getHeaders();

    /**
     * @return 요청의 HTTP Verb
     */
    String getMethod();

    /**
     * @return POST할 데이터가 있는지 여부
     */
    boolean hasPayload();

    /**
     * @return 요청시 사용할 {@link java.nio.charset.Charset}의 이름
     */
    String getCharSet();

    /**
     * @return POST할 내용
     */
    Object getPayload();
} public class Response {

    private final int statusCode;
    private String statusText;
    private final Map<String, String> headers;
    private final String responseBody;
    private final URI uri;

    public Response(URI uri, int statusCode, String statusText, Map<String, String> headers, String responseBody) {
        this.uri = uri;
        this.statusCode = statusCode;
        this.statusText = statusText;
        this.headers = headers;
        this.responseBody = responseBody;
    }

    public URI getUri() {
        return uri;
    }

    public int getStatusCode() {
        return statusCode;
    }

    public String getStatusText() {
        return statusText;
    }

    public Map<String, String> getHeaders() {
        return headers;
    }

    public String getResponseBody() {
        return responseBody;
    }

    public boolean hasResponseStatus() {
        return true;
    }

    public boolean hasResponseBody() {
        return responseBody != null && responseBody.length() > 0;
    }
}
import android.os.Message;
import com.kakao.APIErrorResult;
import com.kakao.Session;
import com.kakao.SessionCallback;
import com.kakao.exception.KakaoException;
import com.kakao.helper.ServerProtocol;
import com.kakao.http.HttpRequestBuilder;
import com.kakao.http.HttpRequestTask;
import com.kakao.http.HttpResponseHandler;
import com.kakao.http.HttpTaskManager;
import com.kakao.http.KakaoAsyncHandler;
import com.kakao.http.Request;
import com.kakao.http.Response;
import org.apache.http.HttpStatus;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.Map.Entry;
/**
 * Copyright 2014 Minyoung Jeong <kkungkkung@gmail.com>
 * Copyright 2014 Kakao Corp.
 *
 * Redistribution and modification in source or binary forms are not permitted without specific prior written permission. 
 *
 * 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.kakao.rest;




/**
 * API 요청을 위한 HttpRequestTask로 세션 만료시 재갱신하여 재요청해주고,
 * 서버 에러가 발생한 경우 APIErrorResult 객체를 반환하도록 한다.
 * @param <T> : 요청이 성공한 경우 return 객체 type. HttpResponseHandler<T>, Class<T>
 * @author MJ
 */
public class APIHttpRequestTask<T> extends HttpRequestTask<T> {
    private final HttpResponseHandler<T> HttpResponseHandler;

    public APIHttpRequestTask(final Request request, final HttpResponseHandler<T> httpResponseHandler, final Class<T> returnType) {
        super(request, new APIAsyncHandler<T>(request, httpResponseHandler, returnType));
        this.HttpResponseHandler = httpResponseHandler;
    }

    public static void checkSessionAndExecute(final APIHttpRequestTask requestTask, final HttpResponseHandler responseHandler) {
        if (Session.getCurrentSession().isOpened())
            HttpTaskManager.execute(requestTask);
        else if (!requestAccessTokenWithRefreshToken(requestTask, responseHandler)) {
            failedToRefreshAccessToken(requestTask, responseHandler, "session is closed before sending the request");
        }
    }

    private static void failedToRefreshAccessToken(final APIHttpRequestTask requestTask, final HttpResponseHandler responseHandler, final String errorMsg) {
        String requestUrl = null;
        if(requestTask != null && requestTask.request != null)
            requestUrl = requestTask.request.getUrl();
        final APIErrorResult clientError = new APIErrorResult(requestUrl, errorMsg);
        responseHandler.sendMessage(Message.obtain(responseHandler, NEED_TO_LOGIN, 0, 0, clientError));
    }

    public static void addQueryParam(final HttpRequestBuilder requestBuilder, final String propertiesKey, final Map properties) throws JSONException {
        JSONObject json = new JSONObject();
        for (Object key : properties.keySet()) {
            json.put((String)key, properties.get(key));
        }

        requestBuilder.addQueryParameter(propertiesKey, json.toString());
    }

    private static class APIAsyncHandler<T> extends KakaoAsyncHandler<T> {