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 abstract class KakaoStoryHttpResponseHandler<T> extends HttpResponseHandler<T> {
    /**
     *  카카오계정에 연결한 카카오스토리 사용자가 아니여서 요청이 실패한 경우 호출된다.
     */
    protected abstract void onNotKakaoStoryUser();

    /**
     * 세션이 닫혀서 또는 카카오스토리 사용자가 아닌 경우를 제외한 이유로 요청이 실패한 경우 호출된다.
     * 아래 에러 종류에 따라 적절한 처리를 한다.
     * {@link ErrorCode#INVALID_PARAM_CODE},
     * {@link ErrorCode#INVALID_SCOPE_CODE},
     * {@link ErrorCode#NOT_SUPPORTED_API_CODE} : 카카오계정으로 로그인하지 않고 다른 타입의 계정으로 로그인한 사용자가 카카오서비스(카카오스토리,카카오톡) API를 호출한 경우, 해당 API를 개발자 싸이트에서 disable 해놓은 경우 발생한다.
     * {@link ErrorCode#INTERNAL_ERROR_CODE},
     * {@link ErrorCode#CLIENT_ERROR_CODE},
     * {@link ErrorCode#EXCEED_LIMIT_CODE},
     * {@link ErrorCode#KAKAO_MAINTENANCE_CODE},
     * @param errorResult 실패한 원인이 담긴 결과
     */
    protected abstract void onFailure(final APIErrorResult errorResult);

    @Override
    protected void onHttpFailure(final APIErrorResult errorResult) {
        Logger.getInstance().d("failed : " + errorResult);
        if (errorResult.getErrorCode() == ErrorCode.NOT_EXIST_KAKAOSTORY_USER_CODE) {
            onNotKakaoStoryUser();
        } else {
            onFailure(errorResult);
        }
    }
} public class KakaoStoryPostParamBuilder {
    /**
     * 스토리 포스트시 공개 범위
     */
    public enum PERMISSION {
        /**
         * 전체 공개
         */
        PUBLIC("A"),
        /**
         * 친구 공개
         */
        FRIENDS("F");

        private final String value;

        PERMISSION(String value) {
            this.value = value;
        }
    }

    private final String content;
    private final PERMISSION permission;
    private String imageURL;
    private String androidExecuteParam;
    private String iosExecuteParam;

    /**
     * 친구 공개 포스팅 Builder를 만든다.
     * @param content 포스팅에 포함할 텍스트
     */
    public KakaoStoryPostParamBuilder(final String content) {
        this(content, PERMISSION.FRIENDS);
    }

    /**
     * 공개범위를 지정한 포스팅 Builder를 만든다.
     * @param content 포스팅에 포함할 텍스트
     * @param permission 포스팅 공개 여부
     */
    public KakaoStoryPostParamBuilder(final String content, final PERMISSION permission) {
        this.content = content;
        this.permission = permission;
    }

    /**
     * 포스팅에 이미지 추가한다.
     * @param imageURL 포스팅에 포함할 이미지 경로. upload API를 통해 얻은 결과
     * @return 이미지 추가 후 builder
     */
    public KakaoStoryPostParamBuilder setImageURL(final String imageURL) {
        this.imageURL = imageURL;
        return this;
    }

    /**
     * Android 앱연결 링크에 추가할 파라미터 설정한다.
     * 기본적으로는 kakao[appkey]://kakaostory로 연결되나 그 뒤에 파라미터를 붙이고 싶을 때 사용한다.
     * @param androidExecuteParam 추가할 파라미터
     * @return 파라미터 추가후 builder
     */
    public KakaoStoryPostParamBuilder setAndroidExecuteParam(final String androidExecuteParam) {
        this.androidExecuteParam = androidExecuteParam;
        return this;
    }
    /**
     * iOS 앱연결 링크에 추가할 파라미터 설정한다.
     * 기본적으로는 kakao[appkey]://kakaostory로 연결되나 그 뒤에 파라미터를 붙이고 싶을 때 사용한다.
     * @param iosExecuteParam 추가할 파라미터
     * @return 파라미터 추가후 builder
     */
    public KakaoStoryPostParamBuilder setIOSExecuteParam(final String iosExecuteParam) {
        this.iosExecuteParam = iosExecuteParam;
        return this;
    }

    /**
     * 지금까지 추가된 설정을 Bundle로 만들어준다.
     * @return 스토리 포스트 설정을 Bundle로 반환
     */
    public Bundle build(){
        final Bundle parameters = new Bundle();
        parameters.putString(ServerProtocol.CONTENT_KEY, content);
        parameters.putString(ServerProtocol.PERMISSION_KEY, permission.value);
        if(imageURL != null)
            parameters.putString(ServerProtocol.IMAGE_URL_KEY, imageURL);
        if(androidExecuteParam != null)
            parameters.putString(ServerProtocol.ANDROID_EXEC_PARAM_KEY, androidExecuteParam);
        if(iosExecuteParam != null)
            parameters.putString(ServerProtocol.IOS_EXEC_PARAM_KEY, iosExecuteParam);
        return parameters;
    }
} public enum PERMISSION {
    /**
     * 전체 공개
     */
    PUBLIC("A"),
    /**
     * 친구 공개
     */
    FRIENDS("F");

    private final String value;

    PERMISSION(String value) {
        this.value = value;
    }
} public class KakaoStoryProfile {
    private String nickName;
    private String profileImageURL;
    private String thumbnailURL;
    private String bgImageURL;
    private String birthday;
    private BirthdayType birthdayType;

    private Bitmap bgImageBitmap;
    private Calendar birthdayCalendar;

    public KakaoStoryProfile(String nickName, String profileImageURL, String thumbnailURL, String bgImageURL, String birthday, BirthdayType birthdayType) {
        this.nickName = nickName;
        this.profileImageURL = profileImageURL;
        this.thumbnailURL = thumbnailURL;
        this.bgImageURL = bgImageURL;
        this.birthday = birthday;
        this.birthdayType = birthdayType;
    }

    /**
     * 카카오스토리의 생일 타입
     */
    public enum BirthdayType {
        /**
         * 양력 생일
         */
        SOLAR("+"),
        /**
         * 음력 생일
         */
        LUNAR("-");
        private final String displaySymbol;

        BirthdayType(String s) {
            this.displaySymbol = s;
        }

        /**
         * 양력 생일은 '+', 음력 생일은 '-'로 반환한다.
         * @return '+','-'로 생일 타입을 반환한다.
         */
        public String getDisplaySymbol() {
            return displaySymbol;
        }
    }

    // for jackson
    public KakaoStoryProfile() {
    }

    /**
     * 카카오스토리 별명
     * @return 카카오스토리 별명
     */
    public String getNickName() {
        return nickName;
    }

    /**
     * 480px * 480px ~ 1024px * 1024px 크기의 카카오스토리 프로필 이미지 URL
     * @return 카카오스토리 프로필 이미지 URL
     */
    public String getProfileImageURL() {
        return profileImageURL;
    }

    /**
     * 160px * 160px 크기의 카카오스토리 썸네일 프로필 이미지 URL
     * @return 카카오스토리 썸네일 프로필 이미지 URL
     */
    public String getThumbnailURL() {
        return thumbnailURL;
    }

    /**
     * 480px * 480px ~ 1024px * 1024px 크기의 카카오스토리 배경 이미지 URL
     * @return 카카오스토리 배경 이미지 URL
     */
    public String getBgImageURL() {
        return bgImageURL;
    }

    /**
     * 카카오스토리 생일 MMdd 형태
     * @return 카카오스토리 생일 MMdd 형태
     */
    public String getBirthday() {
        return birthday;
    }

    /**
     * 카카오스토리 생일을 Calendar 타입
     * @return 카카오스토리 생일을 Calendar 형식으로 반환
     */
    public Calendar getBirthdayCalendar() {
        return birthdayCalendar;
    }

    /**
     * 카카오스토리 생일 타입. 양력 또는 음력
     * @return 카카오스토리 생일 타입. 양력 또는 음력
     */
    public BirthdayType getBirthdayType() {
        return birthdayType;
    }

    /**
     * jackson에서 객체를 만들 때 사용한다.
     * @param birthday MMdd 형태의 생일 string 값
     */
    public void setBirthday(final String birthday) {
        if(birthday == null)
            return;
        final SimpleDateFormat form = new SimpleDateFormat("MMdd");
        birthdayCalendar  = Calendar.getInstance();
        try {
            birthdayCalendar.setTime(form.parse(birthday));
        } catch (java.text.ParseException e) {
            Logger.getInstance().d(e);
        }
        this.birthday = birthday;
    }

    /**
     * 각 정보를 string으로 표현하여 반환한다.
     * @return 각 정보를 포함한 string
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("KakaoStoryProfile{");
        sb.append("nickName='").append(nickName).append('\'');
        sb.append(", profileImageURL='").append(profileImageURL).append('\'');
        sb.append(", thumbnailURL='").append(thumbnailURL).append('\'');
        sb.append(", bgImageURL='").append(bgImageURL).append('\'');
        sb.append(", birthday='").append(birthday).append('\'');
        sb.append(", birthdayType=").append(birthdayType);
        sb.append('}');
        return sb.toString();
    }
} public enum BirthdayType {
    /**
     * 양력 생일
     */
    SOLAR("+"),
    /**
     * 음력 생일
     */
    LUNAR("-");
    private final String displaySymbol;

    BirthdayType(String s) {
        this.displaySymbol = s;
    }

    /**
     * 양력 생일은 '+', 음력 생일은 '-'로 반환한다.
     * @return '+','-'로 생일 타입을 반환한다.
     */
    public String getDisplaySymbol() {
        return displaySymbol;
    }
} public class KakaoStoryService {

    /**
     * 카카오스토리 프로필 요청
     * @param responseHandler 프로필 요청 결과에 대한 handler
     */
    public static void requestProfile(final KakaoStoryHttpResponseHandler<KakaoStoryProfile> responseHandler) {
        String url = HttpRequestTask.createBaseURL(ServerProtocol.API_AUTHORITY, ServerProtocol.STORY_PROFILE_PATH);
        HttpRequestBuilder requestBuilder = HttpRequestBuilder.post(url);
        APIHttpRequestTask.addCommon(requestBuilder);
        APIHttpRequestTask.checkSessionAndExecute(
                new APIHttpRequestTask<KakaoStoryProfile>(
                        requestBuilder.build(),
                        responseHandler,
                        KakaoStoryProfile.class
                ),
                responseHandler
        );
    }

    /**
     * 카카오스토리에 포스팅 요청전에 이미지 업로드 요청
     * @param responseHandler 이미지 업로드 요청 결과에 대한 handler
     * @param file 업로드할 이미지 파일
     * @throws FileNotFoundException 업로드할 파일이 존재하는 않는 경우 발생한다.
     */
    public static void requestUpload(final KakaoStoryHttpResponseHandler<KakaoStoryUpload> responseHandler, final File file) throws FileNotFoundException {
        String url = HttpRequestTask.createBaseURL(ServerProtocol.API_AUTHORITY, ServerProtocol.STORY_UPLOAD_PATH);
        HttpRequestBuilder requestBuilder = HttpRequestBuilder.post(url);
        APIHttpRequestTask.addCommon(requestBuilder);

        requestBuilder.setBodyEncoding(ServerProtocol.BODY_ENCODING);
        requestBuilder.addBodyPart(new FilePart(ServerProtocol.FILE_KEY, file));

        APIHttpRequestTask.checkSessionAndExecute(new APIHttpRequestTask<KakaoStoryUpload>(requestBuilder.build(), responseHandler, KakaoStoryUpload.class), responseHandler);
    }

    /**
     * 카카오스토리에 포스팅 요청
     * @param responseHandler 포스팅 요청 결과에 대한 handler
     * @param parameters 포스트때 사용할 정보
     */
    public static void requestPost(final KakaoStoryHttpResponseHandler<Void> responseHandler,
                                   final Bundle parameters) {
        String url = HttpRequestTask.createBaseURL(ServerProtocol.API_AUTHORITY, ServerProtocol.STORY_POST_PATH);
        HttpRequestBuilder requestBuilder = HttpRequestBuilder.post(url);
        APIHttpRequestTask.addCommon(requestBuilder);

        APIHttpRequestTask.addQueryParams(requestBuilder, parameters);
        APIHttpRequestTask.checkSessionAndExecute(new APIHttpRequestTask<Void>(requestBuilder.build(), responseHandler, Void.class), responseHandler);
    }

} public class KakaoStoryUpload {
    private String url;

    // for jackson
    public KakaoStoryUpload(final String url) {
        this.url = url;
    }

    /**
     * 업로드된 이미지의 URL
     * @return 업로드된 이미지의 URL
     */
    public String getUrl() {
        return url;
    }

    /**
     * 각 정보를 string으로 표현하여 반환한다.
     * @return 각 정보를 포함한 string
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("StoryUpload{");
        sb.append("url='").append(url).append('\'');
        sb.append('}');
        return sb.toString();
    }
} public abstract class LogoutResponseCallback extends UserResponseCallback {
    /**
     * 로그아웃을 성공적으로 마친 경우로
     * 일반적으로 로그인창으로 이동하도록 구현한다.
     * @param userId 로그아웃된 사용자의 id
     */
    protected abstract void onSuccess(final long userId);

    /**
     * 로그아웃 요청중 exception이 발생하였으나 강제로 세션을 클로즈 한 후 callback이 불리므로 로그인 창으로 이동하도록 구현한다.
     * 실패한 아래의 이유들은 디버깅용으로 사용할 수 있다.
     * {@link ErrorCode#INVALID_PARAM_CODE},
     * {@link ErrorCode#INVALID_SCOPE_CODE},
     * {@link ErrorCode#NOT_SUPPORTED_API_CODE},
     * {@link ErrorCode#INTERNAL_ERROR_CODE},
     * {@link ErrorCode#INVALID_TOKEN_CODE},
     * {@link ErrorCode#CLIENT_ERROR_CODE},
     * {@link ErrorCode#EXCEED_LIMIT_CODE},
     * {@link ErrorCode#KAKAO_MAINTENANCE_CODE}
     * @param errorResult 실패한 이유
     */
    protected abstract void onFailure(final APIErrorResult errorResult);

    /**
     * {@link UserResponseCallback}를 구현한 것으로 로그아웃이 성공했을 때 호출된다.
     * 세션을 닫고 캐시를 삭제한 후에 사용자 콜백 {@link #onSuccess(long)}을 호출한다.
     * return값으로 온 user객체가 비정상이더라도 강제 로그아웃을 진행한다.
     * @param user 로그아웃된 사용자
     */
    @Override
    protected void onSuccessUser(final User user) {
        if (user == null || user.getId() <= 0)
            forceToLogout("LogoutResponseCallback : onSuccessUser is called but the result user is null.", new APIErrorResult(null, "the result of logout request is null."));
        else {
            Logger.getInstance().d("LogoutResponseCallback: logout successfully. user = " + user);
            Session.getCurrentSession().close(null);
            onSuccess(user.getId());
        }
    }

    /**
     * {@link com.kakao.http.HttpResponseHandler}를 구현한 것으로 로그아웃 요청 전 또는 요청 중에 세션이 닫혀 로그인이 필요할 때 호출된다.
     * 로그아웃이 세션을 닫는 것이 목적이기 때문에 강제 로그아웃을 진행한다.
     * @param errorResult 세션이 닫힌 이유
     */
    @Override
    protected void onHttpSessionClosedFailure(final APIErrorResult errorResult) {
        forceToLogout("LogoutResponseCallback: session is closed before requesting logout. logout forcefully", errorResult);
    }

    /**
     * {@link com.kakao.http.HttpResponseHandler}를 구현한 것으로 로그아웃 요청 중에 서버에서 로그아웃을 수행하지 못했다는 결과를 받았을 때 호출된다.
     * 로그아웃이 세션을 닫는 것이 목적이기 때문에 강제 로그아웃을 진행한다.
     * @param errorResult 실패한 결과
     */
    @Override
    protected void onHttpFailure(final APIErrorResult errorResult) {
        forceToLogout("LogoutResponseCallback: server error occurred during requesting logout. logout forcefully. ", errorResult);
    }

    /**
     * 강제로 로그아웃을 진행하고 사용자가 정의한 {@link #onFailure(APIErrorResult)}를 호출한다.
     * @param msg 강제로그아웃하는 메시지
     * @param errorResult 로그아웃 실패 이유
     */
    private void forceToLogout(final String msg, final APIErrorResult errorResult) {
        Logger.getInstance().d(msg + errorResult);
        Session.getCurrentSession().close(null);
        onFailure(errorResult);
    }
} public class UserManagement {

    /**
     * 사용자정보 요청
     * @param responseHandler me 요청 결과에 대한 handler
     */
    public static void requestMe(final MeResponseCallback responseHandler) {
        final String url = HttpRequestTask.createBaseURL(ServerProtocol.API_AUTHORITY, ServerProtocol.USER_ME_PATH);
        request(responseHandler, url);
    }

    /**
     * 로그아웃 요청
     * @param responseHandler logout 요청 결과에 대한 handler
     */
    public static void requestLogout(final LogoutResponseCallback responseHandler) {
        final String url = HttpRequestTask.createBaseURL(ServerProtocol.API_AUTHORITY, ServerProtocol.USER_LOGOUT_PATH);
        request(responseHandler, url);
    }

    /**
     * Unlink 요청
     * @param responseHandler unlink 요청 결과에 대한 handler
     */
    public static void requestUnlink(final UnlinkResponseCallback responseHandler) {
        final String url = HttpRequestTask.createBaseURL(ServerProtocol.API_AUTHORITY, ServerProtocol.USER_UNLINK_PATH);
        request(responseHandler, url);
    }

    /**
     * 가입 요청
     * @param responseHandler signup 요청 결과에 대한 handler
     * @param properties 가입시 받은 사용자 정보
     */
    public static void requestSignup(final SignupResponseCallback responseHandler, final Map properties) {
        final String url = HttpRequestTask.createBaseURL(ServerProtocol.API_AUTHORITY, ServerProtocol.USER_SIGNUP_PATH);
        request(responseHandler, url, properties);
    }

    /**
     * 사용자정보 저장 요청
     * @param responseHandler updateProfile 요청 결과에 대한 handler
     * @param properties 저장할 사용자 정보
     */
    public static void requestUpdateProfile(final UpdateProfileResponseCallback responseHandler, final Map properties) {
        final String url = HttpRequestTask.createBaseURL(ServerProtocol.API_AUTHORITY, ServerProtocol.USER_UPDATE_PROFILE_PATH);
        request(responseHandler, url, properties);
    }

    private static void request(final HttpResponseHandler<Map> responseHandler, final String url) {
        final HttpRequestBuilder requestBuilder = HttpRequestBuilder.get(url);
        APIHttpRequestTask.addCommon(requestBuilder);
        APIHttpRequestTask.checkSessionAndExecute(new APIHttpRequestTask<Map>(requestBuilder.build(), responseHandler, Map.class), responseHandler);
    }

    private static void request(final HttpResponseHandler<Map> responseHandler, final String url, final Map properties) {
        final HttpRequestBuilder requestBuilder = HttpRequestBuilder.get(url);
        APIHttpRequestTask.addCommon(requestBuilder);
        try {
            APIHttpRequestTask.addQueryParam(requestBuilder, ServerProtocol.PROPERTIES_KEY, properties);
            APIHttpRequestTask.checkSessionAndExecute(new APIHttpRequestTask<Map>(requestBuilder.build(), responseHandler, Map.class), responseHandler);
        } catch (JSONException e) {
            responseHandler.sendMessage(Message.obtain(responseHandler, HttpRequestTask.ERROR, 0, 0,
                    new APIErrorResult(requestBuilder.build().getUrl(), e.getMessage())));
        }
    }
} public class UserProfile implements Parcelable{
    private static final String CACHE_USER_PREFIX = "com.kakao.user.";
    private static final String CACHE_USER_ID = "com.kakao.user.userId";
    private static final String CACHE_NICKNAME = "com.kakao.user.nickname";
    private static final String CACHE_THUMBNAIL_PATH = "com.kakao.user.thumbbnailpath";
    private static final String CACHE_PROFILE_PATH = "com.kakao.user.profilepath";

    private final long id;
    private String nickname;
    private String thumbnailImagePath;
    private String profileImagePath;
    // predefined property를 제외한 사용자가 정의한 property
    private Map<String, String> properties = new HashMap<String, String>();

    private UserProfile(final long id, final String nickname, final String thumbnailImagePath, final String profileImagePath,
                       final Map<String, String> properties) {
        this.id = id;
        this.nickname = nickname;
        this.thumbnailImagePath = thumbnailImagePath;
        this.profileImagePath = profileImagePath;
        if(properties != null)
            this.properties = properties;
    }

    /**
     * UserManagement API 요청을 시도한 사용자 id
     *
     * @return UserManagement API 요청을 시도한 사용자 id
     */
    public long getId() {
        return id;
    }

    /**
     * 현재까지 저장되어 있는 사용자의 모든 정보를 key, value로 구성된 json type으로 반환
     * @return 앱에 저장된 사용자의 모든 정보
     */
    public Map<String, String> getProperties() {
        return properties;
    }

    /**
     * 앱 등록 당시 정의한 사용자의 정보 중 key에 해당하는 정보
     * @param propertyKey 알고 싶은 사용자 정보의 key
     * @return 해당 key의 정보
     */
    public String getProperty(final String propertyKey) {
        if(properties != null)
            return properties.get(propertyKey);
        else
            return null;
    }

    /**
     * 사용자 별명
     * @return 사용자 별명
     */
    public String getNickname() {
        if (nickname == null)
            return "undefined";
        else
            return nickname;
    }

    /**
     * 110px * 110px(톡에서 가지고 온 경우) 또는 160px * 160px(스토리에서 가지고 온 경우) 크기의 사용자의 썸네일 프로필 이미지 경로
     * @return 사용자의 썸네일 프로필 이미지 경로
     */
    public String getThumbnailImagePath() {
        return thumbnailImagePath;
    }

    /**
     * 480px * 480px ~ 1024px * 1024px 크기의 사용자의 프로필 이미지 경로
     * @return 사용자의 프로필 이미지 경로
     */
    public String getProfileImagePath() {
        return profileImagePath;
    }

    /**
     * 사용자의 프로필 정보를 String으로 변환한다
     * @return 사용자의 프로필 정보를 String으로 변환힌 값
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("UserProfile{");
        sb.append("nickname='").append(nickname).append('\'');
        sb.append(", thumbnailImagePath='").append(thumbnailImagePath).append('\'');
        sb.append(", profileImagePath='").append(profileImagePath).append('\'');
        sb.append(", properties=").append(properties);
        sb.append('}');
        return sb.toString();
    }

    /**
     * 캐시로 부터 사용자정보를 읽어온다.
     * @return 캐시에서 읽은 사용자정보
     */
    public static UserProfile loadFromCache() {
        SharedPreferencesCache cache = Session.getAppCache();
        if(cache == null)
            return null;

        Bundle bundle = cache.load();

        final long userId = bundle.getLong(CACHE_USER_ID);
        bundle.remove(CACHE_USER_ID);
        final String nickname = bundle.getString(CACHE_NICKNAME);
        bundle.remove(CACHE_NICKNAME);
        final String thumbnailPath = bundle.getString(CACHE_THUMBNAIL_PATH);
        bundle.remove(CACHE_THUMBNAIL_PATH);
        final String profilePath = bundle.getString(CACHE_PROFILE_PATH);
        bundle.remove(CACHE_PROFILE_PATH);

        Map<String, String> properties = new HashMap<String, String>();
        if(!bundle.isEmpty()){
            for(String key : bundle.keySet()){
                if(key.startsWith(CACHE_USER_PREFIX))
                    properties.put(key, bundle.getString(key));
            }
        }

        return new UserProfile(userId, nickname, thumbnailPath, profilePath, properties);
    }

    /**
     * 요청 결과로 부터 UserProfile 객체를 만든다.
     * @param userProfileMap 사용자정보 요청결과 json으로 부터 얻은 Map
     * @return 요청 결과로 부터 만든 UserProfile 객체
     */
    public static UserProfile createFromResponse(final Map userProfileMap) {
        final Number userIdNumber = (Number) userProfileMap.get(ServerProtocol.USER_ID_KEY);
        final long userId = userIdNumber.longValue();

        final Map<String, String> properties = (Map<String, String>) userProfileMap.get(ServerProtocol.PROPERTIES_KEY);

        return createFromInput(userId, properties);
    }

    /**
     * 사용자정보 저장 요청 후 결과로 받은 사용자 id와 update한 사용자 정보로 부터 UserProfile 객체를 만든다.
     * @param userId 사용자 id
     * @param properties update한 사용자 정보
     * @return input으로부터 만든 UserProfile 객체
     */
    private static UserProfile createFromInput(final long userId, final Map<String, String> properties) {
        String nickname = null;
        String thumbnailPath = null;
        String profilePath = null;
        if (properties != null) {
            nickname = properties.remove(ServerProtocol.NICK_NAME_KEY);
            thumbnailPath = properties.remove(ServerProtocol.PROFILE_THUMBNAIL_IMAGE_KEY);
            profilePath = properties.remove(ServerProtocol.PROFILE_IMAGE_KEY);
        }
        return new UserProfile(userId, nickname, thumbnailPath, profilePath, properties);
    }

    /**
     * 원본 사용자정보와 update할 사용자 정보를 받아 원본에 update할 정보만 update한 결과를 준다.
     * @param originUserProfile 원본 사용자정보 객체
     * @param properties update할 사용자 정보
     * @return input을 merge한 결과
     */
    public static UserProfile updateUserProfile(final UserProfile originUserProfile, final Map<String, String> properties) {
        UserProfile userProfile = new UserProfile(originUserProfile.getId(), originUserProfile.getNickname(),
            originUserProfile.getThumbnailImagePath(), originUserProfile.getProfileImagePath(), originUserProfile.getProperties());
        if (properties != null) {
            final String nickname = properties.remove(ServerProtocol.NICK_NAME_KEY);
            if(nickname != null)
                userProfile.nickname = nickname;

            final String thumbnailPath = properties.remove(ServerProtocol.PROFILE_THUMBNAIL_IMAGE_KEY);
            if(thumbnailPath != null)
                userProfile.thumbnailImagePath = thumbnailPath;

            final String profilePath = properties.remove(ServerProtocol.PROFILE_IMAGE_KEY);
            if(profilePath != null)
                 userProfile.profileImagePath = profilePath;

            if(!properties.isEmpty())
                userProfile.properties.putAll(properties);
        }
        return userProfile;
    }

    /**
     * 사용자 객체를 캐시에 저장한다.
     */
    public void saveUserToCache() {
        SharedPreferencesCache cache = Session.getAppCache();
        if(cache == null)
            return;

        Bundle bundle = new Bundle();

        bundle.putLong(CACHE_USER_ID, id);
        bundle.putString(CACHE_NICKNAME, nickname);
        bundle.putString(CACHE_THUMBNAIL_PATH, thumbnailImagePath);
        bundle.putString(CACHE_PROFILE_PATH, profileImagePath);

        if(!properties.isEmpty()){
            for(String key : properties.keySet()){
                bundle.putString(CACHE_USER_PREFIX + key, properties.get(key));
            }
        }
        cache.save(bundle);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(id);
        dest.writeString(nickname);
        dest.writeString(thumbnailImagePath);
        dest.writeString(profileImagePath);
        dest.writeMap(properties);
    }

    public UserProfile(Parcel in) {
        id = in.readLong();
        nickname = in.readString();
        thumbnailImagePath = in.readString();
        profileImagePath = in.readString();
        in.readMap(properties, getClass().getClassLoader());
    }

    public static final Parcelable.Creator<UserProfile> CREATOR = new Parcelable.Creator<UserProfile>() {
        public UserProfile createFromParcel(Parcel in) {
            return new UserProfile(in);
        }

        public UserProfile[] newArray(int size) {
            return new UserProfile[size];
        }
    };

} public class Logger {
    public static enum LogLevel {
        Verbose, Debug, Info, Warn, Error, Release
    }


    private static final String TAG = "kakao-android-sdk";
	private volatile static Logger instance;

	private LogLevel logLevel;

	private Logger() {
		super();
		this.logLevel = LogLevel.Debug;
	}

	public static Logger getInstance() {
		if (instance == null) {
			synchronized (Logger.class) {
				if (instance == null) instance = new Logger();
			}
		}

		return instance;
	}

	public void setLogLevel(LogLevel logLevel) {
		this.logLevel = logLevel;
	}

    public boolean isLoggable(LogLevel logLevel){
        return this.logLevel.compareTo(logLevel) <= 0;
    }

	public void v(String msg) {
		v(TAG, msg);
	}

	public void d(String msg) {
		d(TAG, msg);
	}

	public void i(String msg) {
		i(TAG, msg);
	}

	public void w(String msg) {
		w(TAG, msg);
	}

	public void e(String msg) {
		e(TAG, msg);
	}
	
	void v(String tag, String msg) {
		switch (logLevel) {
		case Verbose:
			Log.v(tag, msg);
			break;
		}
	}

	public void d(String tag, String msg) {
		switch (logLevel) {
		case Verbose:
		case Debug:
			Log.d(tag, msg);
			break;
		}
	}

	void i(String tag, String msg) {
		switch (logLevel) {
		case Verbose:
		case Debug:
		case Info:
			Log.i(tag, msg);
			break;
		}
	}

	public void w(String tag, String msg) {
		switch (logLevel) {
		case Verbose:
		case Debug:
		case Info:
		case Warn:
			Log.w(tag, msg);
			break;
		}
	}

	void e(String tag, String msg) {
		switch (logLevel) {
		case Verbose:
		case Debug:
		case Info:
		case Warn:
		case Error:
			Log.e(tag, msg);
			break;
		}
	}

	public void v(Throwable tr) {
		v(TAG, tr);
	}
	
	public void d(Throwable tr) {
		d(TAG, tr);
	}
	
	public void i(Throwable tr) {
		i(TAG, tr);
	}
	
	public void w(Throwable tr) {
		w(TAG, tr);
	}
	
	public void e(Throwable tr) {
		e(TAG, tr);
	}
	
	void v(String tag, Throwable tr) {
		switch (logLevel) {
		case Verbose:
			Log.v(tag, tr.getLocalizedMessage(), tr);
			break;
		}
	}
	
	void d(String tag, Throwable tr) {
		switch (logLevel) {
		case Verbose:
		case Debug:
			Log.d(tag, tr.getLocalizedMessage(), tr);
			break;
		}
	}
	
	public void i(String tag, Throwable tr) {
		switch (logLevel) {
		case Verbose:
		case Debug:
		case Info:
			Log.i(tag, tr.getLocalizedMessage(), tr);
			break;
		}
	}
	
	public void w(String tag, Throwable tr) {
		switch (logLevel) {
		case Verbose:
		case Debug:
		case Info:
		case Warn:
			Log.w(tag, tr.getLocalizedMessage(), tr);
			break;
		}
	}
	
	void e(String tag, Throwable tr) {
		switch (logLevel) {
		case Verbose:
		case Debug:
		case Info:
		case Warn:
		case Error:
			Log.e(tag, tr.getLocalizedMessage(), tr);
			break;
		}
	}
} public class ProfileLayout extends FrameLayout {
    private boolean editable = false;
    private MeResponseCallback meResponseCallback;

    private String profileImageURL;
    private String nickname;
    private String userId;
    private ImageView profile;
    private EditText nicknameText;
    private TextView userIdText;

    public ProfileLayout(Context context) {
        super(context);
    }

    public ProfileLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ProfileLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * 사용자 정보가 수정 가능한지를 설정한다.
     * @param editable 수정가능하면 true, 수정 불가능하면 false를 설정한다.
     */
    public void setEditable(boolean editable) {
        this.editable = editable;
    }

    /**
     * 사용자정보 요청 결과에 따른 callback을 설정한다.
     * @param callback 사용자정보 요청 결과에 따른 callback
     */
    public void setMeResponseCallback(final MeResponseCallback callback){
        this.meResponseCallback = callback;
    }
    /**
     * param으로 온 UserProfile에 대해 view를 update한다.
     * @param userProfile 화면에 반영할 사용자 정보
     */
    public void setUserProfile(final UserProfile userProfile) {
        setProfileURL(userProfile.getProfileImagePath());
        setNickname(userProfile.getNickname());
        setUserId(String.valueOf(userProfile.getId()));
    }

    /**
     * 프로필 이미지에 대해 view를 update한다.
     * @param profileImageURL 화면에 반영할 프로필 이미지
     */
    public void setProfileURL(final String profileImageURL) {
        this.profileImageURL = profileImageURL;
        if (profile != null && profileImageURL != null) {
            new DownloadImageTask(profile).execute(profileImageURL);
        }
    }

    /**
     * 별명 view를 update한다.
     * @param nickname 화면에 반영할 별명
     */
    public void setNickname(final String nickname) {
        this.nickname = nickname;
        if (nicknameText != null && nickname != null)
            nicknameText.setText(nickname);
    }

    /**
     * 사용자 아이디 view를 update한다.
     * @param userId 화면에 반영할 사용자 아이디
     */
    public void setUserId(final String userId) {
        this.userId = userId;
        if (userIdText != null && userId != null)
            userIdText.setText(userId);
    }

    /**
     * 사용자 정보를 layout에 그려준다.
     */
    @Override
    protected void onAttachedToWindow () {
        super.onAttachedToWindow();
        View view = inflate(getContext(), R.layout.kakao_profile_layout, this);

        profile = (ImageView) view.findViewById(R.id.com_kakao_profile_image);
        if (profileImageURL != null)
            setProfileURL(profileImageURL);
        if (!editable) {
            ImageView editableMark = (ImageView) view.findViewById(R.id.profile_edit);
            editableMark.setVisibility(View.INVISIBLE);
        }

        nicknameText = (EditText) view.findViewById(R.id.com_kakao_profile_nickname);
        if (!editable) {
            nicknameText.setEnabled(false);
            nicknameText.setKeyListener(null);
            setBackgroundCompat(nicknameText, null);
            nicknameText.setPadding(0, 0, 0, 0);
            nicknameText.setTextColor(getResources().getColor(R.color.com_kakao_profile_text));
        }
        if (nickname != null)
            nicknameText.setText(nickname);

        userIdText = (TextView) view.findViewById(R.id.com_kakao_profile_userId);
        if (userId != null)
            userIdText.setText(userId);
    }

    @TargetApi(16)
    private void setBackgroundCompat(View v, Drawable drawable) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            v.setBackground(drawable);
        } else {
            v.setBackgroundDrawable(drawable);
        }
    }

    /**
     * 사용자 정보를 요청한다.
     */
    public void requestMe() {
        UserManagement.requestMe(meResponseCallback);
    }

    private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
        ImageView imageView;

        public DownloadImageTask(ImageView imageView) {
            this.imageView = imageView;
        }

        protected Bitmap doInBackground(String... urls) {
            String urldisplay = urls[0];
            Bitmap bitmap = null;
            try {
                InputStream in = new URL(urldisplay).openStream();
                bitmap = BitmapFactory.decodeStream(in);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return bitmap;
        }

        protected void onPostExecute(Bitmap result) {
            imageView.setImageBitmap(result);
        }
    }
}
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Calendar;
import java.util.Locale;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.kakao.APIErrorResult;
import com.kakao.KakaoStoryHttpResponseHandler;
import com.kakao.KakaoStoryPostParamBuilder;
import com.kakao.KakaoStoryPostParamBuilder.PERMISSION;
import com.kakao.KakaoStoryProfile;
import com.kakao.KakaoStoryProfile.BirthdayType;
import com.kakao.KakaoStoryService;
import com.kakao.KakaoStoryUpload;
import com.kakao.LogoutResponseCallback;
import com.kakao.UserManagement;
import com.kakao.UserProfile;
import com.kakao.helper.Logger;
import com.kakao.sample.kakaostory.R.drawable;
import com.kakao.sample.kakaostory.R.id;
import com.kakao.widget.ProfileLayout;
/**
 * 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.sample.kakaostory;



/**
 * 카카오스토리 API인 프로필, 포스팅(이미지 업로드)를 테스트 한다.
 * 유효한 세션이 있다는 검증을 {@link KakaoStoryLoginActivity}로 부터 받고 보여지는 로그인 된 페이지이다.
 */
public class KakaoStoryMainActivity extends Activity {
    private final String storyPostText = "This Cafe is really awesome!";
    private final String execParam = "place=1111";
    private ImageView background;
    private ProfileLayout profileLayout;
    private TextView birthdayView;
    private String imageURL;
    private UserProfile userProfile;

    /**
     * @param savedInstanceState 기존 session 정보가 저장된 객체
     */
    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initializeView();
    }

    protected void onResume() {
        super.onResume();
        // 로그인 하면서 caching되어 있는 profile를 그린다.
        userProfile = UserProfile.loadFromCache();
        if (userProfile != null) {
            profileLayout.setUserProfile(userProfile);
        }
    }

    private void redirectLoginActivity() {
        final Intent intent = new Intent(this, KakaoStoryLoginActivity.class);
        startActivity(intent);
        finish();
    }

    private void onClickProfile() {
        KakaoStoryService.requestProfile(new MyKakaoStoryHttpResponseHandler<KakaoStoryProfile>() {