public class ChaoliApplication extends Application {
    private static Context appContext;
    @Override
    public void onCreate() {
        super.onCreate();
        // train classifier on app start
        CodeProcessor.init(this);
        ChaoliApplication.appContext = getApplicationContext();
        if (NightModeHelper.isDay()){
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
        }else {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
        }
        Stetho.initializeWithDefaults(this);
    }

    public static Context getAppContext() {
        return appContext;
    }

    /**
     * get the app-wide shared preference.
     * @return app-wide shared preference
     */
    public static SharedPreferences getSp() {
        return appContext.getSharedPreferences(Constants.APP_NAME, MODE_PRIVATE);
    }
} public class ConversationLayoutSelector extends LayoutSelector<Conversation> {
    @Override
    int getLayout(int type) {
        switch (type) {
            case 0:
                return R.layout.conversation_view;
            case FOOTER_VIEW:
                return R.layout.loading_item_for_conversation;
        }
        throw new IllegalArgumentException("wrong type");
    }

    @Override
    int getType(Conversation item) {
        if (item.getReplies() == -1) return FOOTER_VIEW;
        return 0;
    }
} public abstract class LayoutSelector<T> {
    abstract int getLayout(int type);
    abstract int getType(T item);
    public static final int FOOTER_VIEW = -1;
} public class Me {
    private static User me = new User();

    public static void clear(){
        me = new User();
    }

    public static boolean isEmpty(){
        return me.isEmpty();
    }

    public static int getMyUserId(){
        return me.getUserId();
    }

    public static String getMyUsername(){
        return me.getUsername();
    }

    public static String getMyAvatarSuffix(){
        return me.getAvatarSuffix();
    }

    @SuppressWarnings("unused")
    public static String getMyAvatarURL(){
        return Constants.avatarURL + "avatar_" + getMyUserId() + "." + getMyAvatarSuffix();
    }

    public static String getMyStatus(){
        return me.getStatus();
    }

    public static String getMySignature(){
        return me.getPreferences() != null && me.getPreferences().getSignature() != null ? me.getPreferences().getSignature() : "";
    }

    public static Boolean getMyPrivateAdd(){
        return me.getPreferences().getPrivateAdd();
    }

    public static Boolean getMyStarOnReply(){
        return me.getPreferences().getStarOnReply();
    }

    public static Boolean getMyStarPrivate(){
        return me.getPreferences().getStarPrivate();
    }

    public static Boolean getMyHideOnline(){
        return me.getPreferences().getHideOnline() != null ? me.getPreferences().getHideOnline() : false;
    }

    public static User.Preferences getPreferences() {
        return me.getPreferences();
    }

    public static String getUsername(){
        return me.getUsername();
    }

    public static void setUsername(String username){
        me.setUsername(username);
    }

    @SuppressWarnings("unused")
    public static void setPreferences(User.Preferences preferences){
        me.setPreferences(preferences);
    }


    public static int getUserId() {
        return getMyUserId();
    }

    public static void setUserId(int userId) {
        me.setUserId(userId);
    }

    public static String getStatus() {
        return me.getStatus();
    }

    public static void setStatus(String status) {
        me.setStatus(status);
    }

    public static String getAvatarSuffix() {
        return me.getAvatarSuffix();
    }

    public static void setAvatarSuffix(String avatarSuffix) {
        me.setAvatarSuffix(avatarSuffix);
    }

    public static void setProfile(Context context, User user) {
        user.setUsername(me.getUsername());
        user.setUserId(me.getUserId());
        me = user;
        me.setEmpty(false);
        if(user.getAvatarSuffix() == null){
            user.setAvatarSuffix(Constants.NONE);
        }
        SharedPreferences sharedPreferences = context.getSharedPreferences("userInfo", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString(getUsername(), new Gson().toJson(user));
        editor.apply();
    }

    private static void setInstanceFromJSONStr(Context context, String jsonStr){
        User user2 = new Gson().fromJson(jsonStr, User.class);
        user2.setUserId(me.getUserId());
        user2.setUsername(me.getUsername());
        me = user2;
        me.setEmpty(false);
        if(getAvatarSuffix() == null){
            me.setAvatarSuffix(Constants.NONE);
        }
        SharedPreferences sharedPreferences = context.getSharedPreferences("userInfo", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString(getMyUsername(), new Gson().toJson(me));
        editor.apply();
    }
    public static void setInstanceFromSharedPreference(Context context, String username) {
        SharedPreferences sharedPreferences = context.getSharedPreferences("userInfo", Context.MODE_PRIVATE);
        if(sharedPreferences.contains(username)) {
            String info = sharedPreferences.getString(username, "bing mei you");
            setInstanceFromJSONStr(context, info);
        }
    }

} public enum Channel
{
	caff(R.string.channel_caff, 1, 0xFFA0A0A0),
	ad(R.string.channel_ad, 3, 0xFF999999),
	maths(R.string.channel_maths, 4, 0xFF673AB7),
	physics(R.string.channel_physics, 5, 0xFFFF5722),
	chem(R.string.channel_chem, 6, 0xFFF44336),
	biology(R.string.channel_biology, 7, 0xFF4CAF50),
	tech(R.string.channel_tech, 8, 0xFF2196F3),
	test(R.string.channel_test, 9, 0xFF607D8B),
	admin(R.string.channel_admin, 24, 0xFFEAEAEA),
	court(R.string.channel_court, 25, 0xFFE040D0),
	recycled(R.string.channel_recycled, 27, 0xFFA6B3E0),
	announ(R.string.channel_announ, 28, 0xFF999999),
	others(R.string.channel_others, 30, 0xFF3F5185),
	socsci(R.string.channel_socsci, 34, 0xFFE04000),
	lang(R.string.channel_lang, 40, 0xFF9030C0);

	private int name;
	private int channelId;
	private int color;

	Channel(int name, int channelId, int color)
	{
		this.name = name;
		this.channelId = channelId;
		this.color = color;
	}

	public int getChannelId()
	{
		return channelId;
	}

	public int getColor()
	{
		return color;
	}

	public String toString()
	{
		return ChaoliApplication.getAppContext().getString(this.name);
	}

	@Deprecated
	public String toString(Context context)
	{
		return context.getString(this.name);
	}


	public static Channel getChannel(int channelId)
	{
		for (Channel c : Channel.values())
			if (c.getChannelId() == channelId) return c;
		return null;
	}

	public static Channel getChannel(String channelName)
	{
		for (Channel c : Channel.values())
			if (c.toString().equals(channelName)) return c;
		return null;
	}
}
 @SuppressWarnings({"unused", "WeakerAccess"})
public class Constants
{
	public static final String APP_NAME = "chaoli";		// for shared preference
	public static final String APP_DIR_NAME = "ChaoLi";	// for directory where save attachments
	public static final int paddingLeft = 16;
	public static final int paddingTop = 16;
	public static final int paddingRight = 16;
	public static final int paddingBottom = 16;
	public static final int getNotificationInterval = 15;

	public static final int MAX_IMAGE_WIDTH = (int) (Resources.getSystem().getDisplayMetrics().widthPixels * 0.8);

	public static final String BASE_BASE_URL = "https://chaoli.club/";
	public static final String BASE_URL = BASE_BASE_URL + "index.php/";
	public static final String GET_TOKEN_URL = BASE_URL + "user/login.json";
	// index.php/member/name.json/我是大缺弦
	public static final String GET_USER_ID_URL = BASE_URL + "member/name.json/";
	public static final String LOGIN_URL = BASE_URL + "user/login";
	public static final String HOMEPAGE_URL = BASE_URL;
	public static final String LOGOUT_PRE_URL = BASE_URL + "user/logout?token=";
	public static final String GET_CAPTCHA_URL = BASE_URL + "mscaptcha";
	public static final String SIGN_UP_URL = BASE_URL + "user/join?invite=";
	public static final String GET_ALL_NOTIFICATIONS_URL = BASE_URL + "settings/notifications.json";
	// activity.json/32/2
	public static final String GET_ACTIVITIES_URL = BASE_URL + "member/activity.json/";
	// statistics.ajax/32
	public static final String GET_STATISTICS_URL = BASE_URL + "member/statistics.ajax/";
	// index.json/channelName?searchDetail
	// index.json/all?search=%23%E4%B8%8A%E9%99%90%EF%BC%9A0%20~%20100
	// index.json/chem?search=%23%E7%B2%BE%E5%93%81
	//public static final String conversationListURL = BASE_URL + "/conversations/index.json/";
	public static final String conversationListURL = BASE_URL + "conversations/index.json/";
	public static final String ATTACHMENT_IMAGE_URL = "https://dn-chaoli-upload.qbox.me/";
	// index.json/1430/p2
	public static final String postListURL = BASE_URL + "conversation/index.json/";
	public static final String loginURL = BASE_URL + "user/login";
	public static final String replyURL = BASE_URL + "?p=conversation/reply.ajax/";
	public static final String editURL = BASE_URL + "?p=conversation/editPost.ajax/";
	public static final String cancelEditURL = BASE_URL + "?p=attachment/removeSession/";
	public static final String notifyNewMsgURL = BASE_URL + "settings/notificationCheck/";
	public static final String avatarURL = "https://dn-chaoli-upload.qbox.me/";
	// .ajax/<conversationId>/<floor>&userId=<myId>&token=<token>
	public static final String preQuoteURL = BASE_URL + "?p=conversation/quotePost.json/";
	// .json/<postId>&userId=<myId>&token=<token>
	public static final String quoteURL = BASE_URL + "?p=conversation/reply.ajax/";
	public static final String deleteURL = BASE_URL + "?p=conversation/deletePost.ajax/";
	public static final String restoreURL = BASE_URL + "conversation/restorePost/";

	public static final String GET_QUESTION_URL = "https://chaoli.club/reg-exam/get-q.php?tags=";
	public static final String CONFIRM_ANSWER_URL = "https://chaoli.club/reg-exam/confirm.php";

	public static final String GET_PROFILE_URL = BASE_URL + "settings/general.json";
	public static final String CHECK_NOTIFICATION_URL = BASE_URL + "?p=settings/notificationCheck.ajax";
	public static final String UPDATE_URL = BASE_URL + "?p=conversations/update.ajax/all/";
	public static final String MODIFY_SETTINGS_URL = BASE_URL + "settings/general";

	public static final String GO_TO_POST_URL = BASE_URL + "conversation/post/";

	/* 给主题设置版块 */
	public static final String SET_CHANNEL_URL = BASE_URL + "?p=conversation/save.json/";
	/* 发表主题 */
	public static final String POST_CONVERSATION_URL = BASE_URL + "?p=conversation/start.ajax";
	/* 添加可见用户 */
	public static final String ADD_MEMBER_URL = BASE_URL + "?p=conversation/addMember.ajax/";
	/* 取消可见用户 */
	public static final String REMOVE_MEMBER_URL = BASE_URL + "?p=conversation/removeMember.ajax/";
	/* 获取可见用户列表 */
	public static final String GET_MEMBERS_ALLOWED_URL = BASE_URL + "";
	/* 隐藏主题 */
	public static final String IGNORE_CONVERSATION_URL = BASE_URL + "?p=conversation/ignore.ajax/";
	/* 关注主题 */
	public static final String STAR_CONVERSATION_URL = BASE_URL + "?p=conversation/star.json/";

	public static final String SETTINGS_SP = "settings";
	public static final String INVITING_CODE_SP = "icsp";
	public static final String NIGHT_MODE = "nightMode";
	public static final String CLICK_TWICE_TO_EXIT = "ctte";

	public static final String GLOBAL = "global";
	public static final String FIRST_ENTER_DEMO_MODE = "fedm";

	public static final String conversationSP = "conversationList";
	public static final String conversationSPKey = "listJSON";

	public static final String postSP = "postList";
	public static final String postSPKey = "listJSON";

	public static final String loginSP = "loginReturn";
	public static final String loginSPKey = "listJSON";
	public static final String loginBool = "logged";

	public static final String[] IMAGE_FILE_EXTENSION = {".jpg", ".png", ".gif", ".jpeg"};

	public static final String NONE = "none";

	public static final float MIN_EXPRESSION_ALPHA = 0.4f;

	public static final int POST_PER_PAGE = 20;

	public static final String[] iconStrs = new String[]{ " /:) " ,  " /:D " ,  " /^b^ " ,  " /o.o " ,  " /xx " ,  " /# " ,  " /)) " ,  " /-- " ,  " /TT " ,  " /== " ,
			" /.** " ,  " /:( " ,  " /vv " ,  " /$$ " ,  " /?? " ,  " /:/ " ,  " /xo " ,  " /o0 " ,  " />< " ,  " /love " ,
			" /... " ,  " /XD " ,  " /ii " ,  " /^^ " ,  " /<< " ,  " />. " ,  " /-_- " ,  " /0o0 " ,  " /zz " ,  " /O!O " ,
			" /## " ,  " /:O " ,  " /< " ,  " /heart " ,  " /break " ,  " /rose " ,  " /gift " ,  " /bow " ,  " /moon " ,  " /sun " ,
			" /coin " ,  " /bulb " ,  " /tea " ,  " /cake " ,  " /music " ,  " /rock " ,  " /v " ,  " /good " ,  " /bad " ,  " /ok " ,
			" /asnowwolf-smile " ,  " /asnowwolf-laugh " ,  " /asnowwolf-upset " ,  " /asnowwolf-tear " ,
			" /asnowwolf-worry " ,  " /asnowwolf-shock " ,  " /asnowwolf-amuse " };
	public static final String[] icons = new String[]{"1", "2", "3", "4",
			"5", "6", "7", "8", "9",
			"10", "11", "12", "13", "14",
			"15", "16", "17", "18", "19",
			"20", "21", "22", "23", "24",
			"25", "26", "27", "28", "29",
			"30", "31", "32", "33", "34",
			"35", "36", "37", "38", "39",
			"40", "41", "42", "43", "44",
			"45", "46", "47", "48", "49",
			"50", "asonwwolf_smile", "asonwwolf_laugh", "asonwwolf_upset", "asonwwolf_tear",
			"asonwwolf_worry", "asonwwolf_shock", "asonwwolf_amuse"};
}
 public class Conversation extends BaseObservable implements DiffItem, Comparable<Conversation>, Parcelable {
	private static final String TAG = "Conversation";
	private int conversationId;
	private int channelId;
	private String title;
	private String firstPost;
	private String link;
	private String startMemberId;
	private String lastPostMemberId;
	private String startMember;
	@SerializedName("startMemberAvatarFormat")
	private String startMemberAvatarSuffix;
	private String startTime;
	@SerializedName("lastPostMemberAvatarFormat")
	private String lastPostMemberAvatarSuffix;
	private String lastPostMember;
	private String lastPostTime;
	private String unread;
	private int replies;
	private transient PropertyChangeRegistry propertyChangeRegistry = new PropertyChangeRegistry();


	@Override
	public boolean areContentsTheSame(DiffItem anotherItem) {
		Conversation newConversation = (Conversation) anotherItem;
		return !(this.getFirstPost() == null && newConversation.getFirstPost() != null)
				&& !(this.getFirstPost() != null && newConversation.getFirstPost() == null)
				&& ((this.getFirstPost() == null && newConversation.getFirstPost() == null) || this.getFirstPost().equals(newConversation.getFirstPost()))
				&& this.getReplies() == newConversation.getReplies();
	}

	@Override
	public boolean areItemsTheSame(DiffItem anotherItem) {
		Conversation newConversation = (Conversation) anotherItem;
		return this.getConversationId() == newConversation.getConversationId();
	}

	/**
	 * 对于主题帖来说，最新的排在最前，不同于单个主题帖中的楼层，最先发表的排在最前
	 *
	 * @param o 另一个主题帖
	 * @return 比较结果
	 */
	@Override
	public int compareTo(Conversation o) {
		return o.getLastPostTime().compareTo(getLastPostTime());
	}

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

	@Override
	public void writeToParcel(Parcel dest, int flags) {
		dest.writeInt(this.conversationId);
		dest.writeInt(this.channelId);
		dest.writeString(this.title);
		dest.writeString(this.firstPost);
		dest.writeString(this.link);
		dest.writeString(this.startMemberId);
		dest.writeString(this.lastPostMemberId);
		dest.writeString(this.startMember);
		dest.writeString(this.startMemberAvatarSuffix);
		dest.writeString(this.lastPostMemberAvatarSuffix);
		dest.writeString(this.lastPostMember);
		dest.writeString(this.lastPostTime);
		dest.writeInt(this.replies);
		dest.writeString(this.startTime);
		dest.writeString(this.unread);
	}

	public Conversation() {
		replies = -1;
	}

	protected Conversation(Parcel in) {
		this.conversationId = in.readInt();
		this.channelId = in.readInt();
		this.title = in.readString();
		this.firstPost = in.readString();
		this.link = in.readString();
		this.startMemberId = in.readString();
		this.lastPostMemberId = in.readString();
		this.startMember = in.readString();
		this.startMemberAvatarSuffix = in.readString();
		this.lastPostMemberAvatarSuffix = in.readString();
		this.lastPostMember = in.readString();
		this.lastPostTime = in.readString();
		this.replies = in.readInt();
		this.startTime = in.readString();
		this.unread = in.readString();
	}

	public static final Creator<Conversation> CREATOR = new Creator<Conversation>() {
		@Override
		public Conversation createFromParcel(Parcel source) {
			return new Conversation(source);
		}

		@Override
		public Conversation[] newArray(int size) {
			return new Conversation[size];
		}
	};

	@Bindable
	public int getConversationId() {
		return conversationId;
	}

	public void setConversationId(int conversationId) {
		this.conversationId = conversationId;
		notifyChange(BR.conversationId);
	}

	@Bindable
	public int getChannelId() {
		return channelId;
	}

	public void setChannelId(int channelId) {
		this.channelId = channelId;
		notifyChange(BR.channelId);
	}

	@Bindable
	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
		notifyChange(BR.title);
	}

	@Bindable
	public String getFirstPost() {
		return firstPost;
	}

	public void setFirstPost(String firstPost) {
		this.firstPost = firstPost;
		notifyChange(BR.firstPost);
	}

	@Bindable
	public String getLink() {
		return link;
	}

	public void setLink(String link) {
		this.link = link;
		notifyChange(BR.link);
	}

	@Bindable
	public String getStartMemberId() {
		return startMemberId;
	}

	public void setStartMemberId(String startMemberId) {
		this.startMemberId = startMemberId;
		notifyChange(BR.startMemberId);
	}

	@Bindable
	public String getLastPostMemberId() {
		return lastPostMemberId;
	}

	public void setLastPostMemberId(String lastPostMemberId) {
		this.lastPostMemberId = lastPostMemberId;
		notifyChange(BR.lastPostMemberId);
	}

	@Bindable
	public String getStartMember() {
		return startMember;
	}

	public void setStartMember(String startMember) {
		this.startMember = startMember;
		notifyChange(BR.startMember);
	}

	@Bindable
	public String getStartMemberAvatarSuffix() {
		return startMemberAvatarSuffix;
	}

	public void setStartMemberAvatarSuffix(String startMemberAvatarSuffix) {
		this.startMemberAvatarSuffix = startMemberAvatarSuffix;
		notifyChange(BR.startMemberAvatarSuffix);
	}

	@Bindable
	public String getStartTime() {
		return startTime;
	}

	public void setStartTime(String startTime) {
		this.startTime = startTime;
		notifyChange(BR.startTime);
	}

	@Bindable
	public String getLastPostMemberAvatarSuffix() {
		return lastPostMemberAvatarSuffix;
	}

	public void setLastPostMemberAvatarSuffix(String lastPostMemberAvatarSuffix) {
		this.lastPostMemberAvatarSuffix = lastPostMemberAvatarSuffix;
		notifyChange(BR.lastPostMemberAvatarSuffix);
	}

	@Bindable
	public String getLastPostMember() {
		return lastPostMember;
	}

	public void setLastPostMember(String lastPostMember) {
		this.lastPostMember = lastPostMember;
		notifyChange(BR.lastPostMember);
	}

	@Bindable
	public String getLastPostTime() {
		return lastPostTime;
	}

	public void setLastPostTime(String lastPostTime) {
		this.lastPostTime = lastPostTime;
		notifyChange(BR.lastPostTime);
	}

	@Bindable
	public String getUnread() {
		return unread;
	}

	public void setUnread(String unread) {
		this.unread = unread;
		notifyChange(BR.unread);
	}

	@Bindable
	public int getReplies() {
		return replies;
	}

	public void setReplies(int replies) {
		this.replies = replies;
		notifyChange(BR.replies);
	}

	private void notifyChange(int propertyId) {
		if (propertyChangeRegistry == null) {
			propertyChangeRegistry = new PropertyChangeRegistry();
		}
		propertyChangeRegistry.notifyChange(this, propertyId);
	}

	@Override
	public void addOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
		if (propertyChangeRegistry == null) {
			propertyChangeRegistry = new PropertyChangeRegistry();
		}
		propertyChangeRegistry.add(callback);

	}

	@Override
	public void removeOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
		if (propertyChangeRegistry != null) {
			propertyChangeRegistry.remove(callback);
		}
	}
}
 public class ConversationListResult {
    public List<Conversation> getResults() {
        return results;
    }

    public void setResults(List<Conversation> results) {
        this.results = results;
    }

    List<Conversation> results;
} public class NotificationList {
    public int count;
    public List<Notification> results;
} public class MyOkHttp {
    private final static String TAG = "MyOkHttp";
    private static OkHttpClient okHttpClient;
    private static CookiesManager mCookiesManager;
    private static Context mContext = ChaoliApplication.getAppContext();

    public static void cancel(Object tag) {
        for (Call call : okHttpClient.dispatcher().queuedCalls()) {
            if (tag.equals(call.request().tag())) call.cancel();
        }
        for (Call call : okHttpClient.dispatcher().runningCalls()) {
            if (tag.equals(call.request().tag())) call.cancel();
        }
    }

    private static class MyInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Response response = chain.proceed(chain.request());
            Log.w("Retrofit@Response", response.body().string());
            return response;
        }
    }

    public synchronized static OkHttpClient getClient(){
        if (okHttpClient == null) {
            OkHttpClient.Builder builder = new OkHttpClient.Builder();
            /**
             * 解决5.0以下系统默认不支持TLS协议导致网络访问失败的问题。
             */
            try {
                // Create a trust manager that does not validate certificate chains
                final X509TrustManager trustAllCert =
                        new X509TrustManager() {
                            @Override
                            public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
                            }

                            @Override
                            public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
                            }

                            @Override
                            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                                return new java.security.cert.X509Certificate[]{};
                            }
                        };
                // Install the all-trusting trust manager
                final SSLSocketFactory sslSocketFactory = new SSLSocketFactoryCompat(trustAllCert);
                builder.sslSocketFactory(sslSocketFactory, trustAllCert);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            mCookiesManager = new CookiesManager();
            okHttpClient = builder
                    .cookieJar(mCookiesManager)
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .readTimeout(30, TimeUnit.SECONDS)
                    .addNetworkInterceptor(new StethoInterceptor())
                    //.addInterceptor(new MyInterceptor())
                    //.connectTimeout(5, TimeUnit.SECONDS)
                    //.readTimeout(5, TimeUnit.SECONDS)
                    .build();
        }
        return okHttpClient;
    }

    public synchronized static void clearCookie(){
        if (mCookiesManager != null) {
            mCookiesManager.clear();
        }
    }

    public static class MyOkHttpClient {
        private MultipartBody.Builder builder;
        //private FormBody.Builder formBuilder;
        private RequestBody requestBody;
        private Request.Builder requestBuilder;
        private Request request;

        public MyOkHttpClient get(String url){
            requestBuilder = new Request.Builder().get();
            requestBuilder = requestBuilder.url(url);
            return this;
        }
        public MyOkHttpClient post(String url){
            requestBody = builder.build();
            requestBuilder = new Request.Builder().post(requestBody);
            requestBuilder = requestBuilder.url(url);
            return this;
        }
        public MyOkHttpClient add(String key, String value) {
            if (builder == null) builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
            builder.addFormDataPart(key, value);
            //if (formBuilder == null) formBuilder = new FormBody.Builder();
            //formBuilder.add(key, value);
            return this;
        }

        public MyOkHttpClient add(String key, String type, File file) {
            if (builder == null) builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
            if (file != null) builder.addFormDataPart(key, file.getName(), RequestBody.create(MediaType.parse(type), file));
            return this;
        }
        /*public MyOkHttpClient url(String url) {
            requestBuilder = requestBuilder.url(url);
            return this;
        }*/

        public void enqueue(final Callback callback) {
            request = requestBuilder.build();
            Call call = getClient().newCall(request);
            call.enqueue(new okhttp3.Callback() {
                @Override
                public void onFailure(final Call call, final IOException e) {
                    new Handler(ChaoliApplication.getAppContext().getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            callback.onFailure(call, e);
                        }
                    });
                }

                @Override
                public void onResponse(final Call call, final Response response) throws IOException {
                    final String responseStr = response.body().string();
                    new Handler(ChaoliApplication.getAppContext().getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                callback.onResponse(call, response, responseStr);
                            } catch (IOException e) {
                                onFailure(call, e);
                            }
                        }
                    });
                    response.body().close();
                }
            });
        }
        @Deprecated
        public void enqueue(final Context context, final Callback callback) {
            enqueue(callback);
        }

        public void enqueue(final Callback1 callback) {
            request = requestBuilder.build();
            Call call = getClient().newCall(request);
            call.enqueue(new okhttp3.Callback() {
                @Override
                public void onFailure(final Call call, final IOException e) {
                            callback.onFailure(call, e);
                }

                @Override
                public void onResponse(final Call call, final Response response) throws IOException {
                                callback.onResponse(call, response);
                }
            });
        }

        @Deprecated
        public void enqueue(final Context context, final Callback1 callback) {
            enqueue(callback);
        }
    }
    public static abstract class Callback {
        public abstract void onFailure(Call call, IOException e);
        public abstract void onResponse(Call call, Response response, String responseStr) throws IOException;
    }

    public static abstract class Callback1 {
        public abstract void onFailure(Call call, IOException e);
        public abstract void onResponse(Call call, Response response) throws IOException;
    }

    /**
     * https://segmentfault.com/a/1190000004345545
     */
    private static class CookiesManager implements CookieJar {
        private final PersistentCookieStore cookieStore = new PersistentCookieStore(mContext);

        @Override
        public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
            if (cookies != null && cookies.size() > 0) {
                for (Cookie item : cookies) {
                    cookieStore.add(url, item);
                }
            }
        }

        @Override
        public List<Cookie> loadForRequest(HttpUrl url) {
            return cookieStore.get(url);
        }

        public void clear(){
            cookieStore.removeAll();
        }
    }
} public class MyRetrofit {
    private final static String TAG = "MyRetrofit";

    private static Retrofit retrofit;
    private static ChaoliService service;

    public synchronized static ChaoliService getService(){
        if (service == null) {
            Gson gson = new GsonBuilder().registerTypeAdapter(Integer.class, new IntegerTypeAdapter()).create();
            retrofit = new Retrofit.Builder()
                    .baseUrl(Constants.BASE_BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                    .client(MyOkHttp.getClient())
                    .build();
            service = retrofit.create(ChaoliService.class);
        }
        return service;
    }

    private static class IntegerTypeAdapter extends TypeAdapter<Integer> {
        @Override
        public void write(JsonWriter writer, Integer value) throws IOException {
            if (value == null) {
                writer.nullValue();
                return;
            }
            writer.value(value);
        }

        @Override
        public Integer read(JsonReader reader) throws IOException {
            if(reader.peek() == JsonToken.NULL){
                reader.nextNull();
                return null;
            }
            String stringValue = reader.nextString();
            try{
                int value = Integer.valueOf(stringValue);
                return value;
            }catch(NumberFormatException e){
                return null;
            }
        }
    }
} public class AccountUtils {
    @SuppressWarnings("unused")
    private static final String TAG = "AccountUtils";

    public static void getProfile(final GetProfileObserver observer){
        MyRetrofit.getService().getProfile()
                .enqueue(new Callback<User>() {
                    @Override
                    public void onResponse(Call<User> call, Response<User> response) {
                        Me.setProfile(ChaoliApplication.getAppContext(), response.body());
                        observer.onGetProfileSuccess();
                    }

                    @Override
                    public void onFailure(Call<User> call, Throwable t) {
                        observer.onGetProfileFailure();
                    }
                });
    }

    public static void modifySettings(File avatar, String language, Boolean privateAdd, Boolean starOnReply, Boolean starPrivate, Boolean hideOnline,
                                      final String signature, String userStatus, final ModifySettingsObserver observer){
        MyOkHttp.MyOkHttpClient myOkHttpClient = new MyOkHttp.MyOkHttpClient()
                .add("token", LoginUtils.getToken())
                .add("language", language)
                .add("userStatus", userStatus)
                .add("signature", signature);
        myOkHttpClient.add("avatar", "image/*", avatar);
        if(hideOnline) myOkHttpClient.add("hideOnline", hideOnline.toString());
        if(starPrivate) myOkHttpClient.add("starPrivate", starPrivate.toString());
        if(starOnReply) myOkHttpClient.add("starOnReply", starOnReply.toString());
        if(privateAdd) myOkHttpClient.add("privateAdd", privateAdd.toString());
        myOkHttpClient.add("save", "保存更改");
        myOkHttpClient
                .post(Constants.MODIFY_SETTINGS_URL)
                .enqueue(new MyOkHttp.Callback() {
                    @Override
                    public void onFailure(okhttp3.Call call, IOException e) {
                        observer.onModifySettingsFailure(-3);
                    }

                    @Override
                    public void onResponse(okhttp3.Call call, okhttp3.Response response, String responseStr) throws IOException {
                        observer.onModifySettingsSuccess();
                    }
                });
    }


    /* 生成类似 1,2,3, 格式的字符串 */
    @SuppressWarnings("unused")
    private static String intJoin(int[] aArr, String sSep) {
        StringBuilder sbStr = new StringBuilder();
        for (int i:
                aArr) {
            sbStr.append(i);
            sbStr.append(sSep);
        }
        return sbStr.toString();
    }

    public interface ModifySettingsObserver{
        void onModifySettingsSuccess();
        void onModifySettingsFailure(int statusCode);
    }

    public interface MessageObserver {
        void onCheckNotificationSuccess(NotificationList notificationList);
        void onCheckNotificationFailure(int statusCode);
    }

    public interface GetProfileObserver{
        void onGetProfileSuccess();
        void onGetProfileFailure();
    }
} public class LoginUtils {
    @SuppressWarnings("unused")
    private static final String TAG = "LoginUtils";
    private static final String LOGIN_SP_NAME = "username_and_password";
    private static final String IS_LOGGED_IN = "is_logged_in";
    private static final String SP_USERNAME_KEY = "sUsername";
    private static final String SP_PASSWORD_KEY = "sPassword";
    public static final int FAILED_AT_OPEN_LOGIN_PAGE = 0;
    public static final int FAILED_AT_GET_TOKEN_ON_LOGIN_PAGE = 1;
    public static final int FAILED_AT_LOGIN = 2;
    public static final int WRONG_USERNAME_OR_PASSWORD = 3;
    public static final int FAILED_AT_OPEN_HOMEPAGE = 4;
    public static final int COOKIE_EXPIRED = 5;
    public static final int EMPTY_UN_OR_PW = 6;
    public static final int ERROR_LOGIN_STATUS = 7; // TODO: 17-1-2 handle this error

    private static void setToken(String token) {
        LoginUtils.sToken = token;
    }

    public static String getToken() {
        return sToken;
    }

    private static String sUsername;
    private static String sPassword;
    private static String sToken;
    private static boolean sIsLoggedIn;

    private static SharedPreferences sSharedPreferences;

    public static void begin_login(final String username, final String password, final LoginObserver loginObserver){
        sSharedPreferences = ChaoliApplication.getAppContext().getSharedPreferences(LOGIN_SP_NAME, Context.MODE_PRIVATE);

        Log.d("login", username + ", " + password);

        sUsername = username;
        sPassword = password;
        pre_login(loginObserver);
    }

    public static void begin_login(LoginObserver loginObserver){

        sSharedPreferences = ChaoliApplication.getAppContext().getSharedPreferences(LOGIN_SP_NAME, Context.MODE_PRIVATE);

        sUsername = sSharedPreferences.getString(SP_USERNAME_KEY, "");
        sPassword = sSharedPreferences.getString(SP_PASSWORD_KEY, "");

        if("".equals(sUsername) || "".equals(sPassword)){
            loginObserver.onLoginFailure(EMPTY_UN_OR_PW);
            return;
        }

        Log.d("login", sUsername + ", " + sPassword);

        begin_login(sUsername, sPassword, loginObserver);
    }

    private static void pre_login(final LoginObserver loginObserver){//获取登录页面的token
        MyRetrofit.getService().getUserIdAndToken()
                .enqueue(new retrofit2.Callback<UserIdAndTokenResult>() {
                    @Override
                    public void onResponse(retrofit2.Call<UserIdAndTokenResult> call, retrofit2.Response<UserIdAndTokenResult> response) {
                        if (response.body() == null) {
                            onFailure(call, new RuntimeException("poor network"));
                            return;
                        }

                        if (response.body().getUserId() == 0) {
                            setToken(response.body().getToken());
                            login(loginObserver);
                        } else {
                            saveUserInfo(response.body().getUserId(), sUsername, response.body().getToken());
                            loginObserver.onLoginSuccess(Me.getMyUserId(), getToken());
                        }
                    }

                    @Override
                    public void onFailure(retrofit2.Call<UserIdAndTokenResult> call, Throwable t) {
                        loginObserver.onLoginFailure(FAILED_AT_GET_TOKEN_ON_LOGIN_PAGE);
                    }
                });
    }

    private static void login(final LoginObserver loginObserver){ //发送请求登录
        MyRetrofit.getService()
                .login(sUsername, sPassword, getToken(), "user/login.json", "登录")
                .enqueue(new retrofit2.Callback<UserIdAndTokenResult>() {
                    @Override
                    public void onResponse(retrofit2.Call<UserIdAndTokenResult> call, retrofit2.Response<UserIdAndTokenResult> response) {
                        saveUserInfo(response.body().getUserId(), sUsername, response.body().getToken());

                        saveUsernameAndPasswordToSp(sUsername, sPassword);

                        loginObserver.onLoginSuccess(Me.getMyUserId(), getToken());
                    }

                    @Override
                    public void onFailure(retrofit2.Call<UserIdAndTokenResult> call, Throwable t) {
                        loginObserver.onLoginFailure(WRONG_USERNAME_OR_PASSWORD);
                        t.printStackTrace();
                    }
                });
    }

    public static void logout(final LogoutObserver logoutObserver){
        String logoutURL = Constants.LOGOUT_PRE_URL + getToken();
        clear(ChaoliApplication.getAppContext());
        Me.clear();
        new MyOkHttp.MyOkHttpClient()
                .get(logoutURL)
                .enqueue(new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {
                        logoutObserver.onLogoutFailure(0);
                    }

                    @Override
                    public void onResponse(Call call, Response response, String responseStr) throws IOException {
                        logoutObserver.onLogoutSuccess();
                    }
                });
    }

    public static void clear(Context context){
        MyOkHttp.clearCookie();
        sIsLoggedIn = false;
        sSharedPreferences = context.getSharedPreferences(LOGIN_SP_NAME, Context.MODE_PRIVATE);
        if(sSharedPreferences != null) {
            SharedPreferences.Editor editor = sSharedPreferences.edit();
            editor.remove(IS_LOGGED_IN);
            editor.remove(SP_USERNAME_KEY);
            editor.remove(SP_PASSWORD_KEY);
            editor.apply();
        }
    }

    public static boolean isLoggedIn() {
        return sIsLoggedIn;
    }

    public static boolean hasSavedData() {
        sSharedPreferences = ChaoliApplication.getAppContext().getSharedPreferences(LOGIN_SP_NAME, Context.MODE_PRIVATE);
        return !sSharedPreferences.getString(SP_USERNAME_KEY, "").equals("") && !sSharedPreferences.getString(SP_PASSWORD_KEY, "").equals("");
    }

    public static String getSavedUsername() {
        sSharedPreferences = ChaoliApplication.getAppContext().getSharedPreferences(LOGIN_SP_NAME, Context.MODE_PRIVATE);
        return sSharedPreferences.getString(SP_USERNAME_KEY, "");
    }

    public static void saveUsernameAndPasswordToSp(String username, String password) {
        sSharedPreferences = ChaoliApplication.getAppContext().getSharedPreferences(LOGIN_SP_NAME, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sSharedPreferences.edit();
        editor.putString(SP_USERNAME_KEY, username);
        // TODO: 16-3-11 1915 Encrypt saved sPassword
        editor.putString(SP_PASSWORD_KEY, password);
        editor.apply();
    }

    private static void saveUserInfo(int userId, String username, String token) {
        Me.setUserId(userId);
        Me.setUsername(username);
        setToken(token);
        sIsLoggedIn = true;
    }
    public interface LoginObserver
    {
        void onLoginSuccess(int userId, String token);
        void onLoginFailure(int statusCode);
    }

    public interface LogoutObserver
    {
        void onLogoutSuccess();
        void onLogoutFailure(int statusCode);
    }
} public class MyUtils {
    @SuppressWarnings("unused")
    private static final String TAG = "MyUtils";

    private static final String quoteRegex = "\\[quote((.|\n)*?)\\[/quote]";
    private static final String codeRegex = "\\[code]((.|\n)*?)\\[/code]";
    private static final String imgRegex = "\\[img](.*?)\\[/img]";
    private static final String attRegex = "\\[attachment:(.*?)]";

    /**
     * 针对可能有其他帖子被顶到最上方，导致下一页的主题帖与这一页的主题帖有重合的现象
     * @param A 已有的主题帖列表
     * @param B 下一页主题帖列表
     * @return 合成后的新列表的长度
     */
    public static <T extends Comparable<T>> int expandUnique(List<T> A, List<T> B) {
        return expandUnique(A, B, true);
    }

    /**
     * a > b 表示 a 排在 b 后面
     */
    public static <T extends Comparable<T>> int expandUnique(List<T> A, List<T> B, Boolean addBehind) {
        return expandUnique(A, B, addBehind, false);
    }

    public static <T extends Comparable<T>> int expandUnique(List<T> A, List<T> B, Boolean addBehind, Boolean reversed) {
        int lenA = A.size();
        if (lenA == 0) A.addAll(B);
        else {
            if (addBehind) {
                int i;
                for (i = 0; i < B.size(); i++)
                    if ((!reversed && B.get(i).compareTo(A.get(A.size() - 1)) > 0) || (reversed && B.get(i).compareTo(A.get(A.size() - 1)) < 0))
                        break;
                A.addAll(B.subList(i, B.size()));
            } else {
                int i;
                for (i = 0; i < B.size(); i++)
                    if ((!reversed && B.get(i).compareTo(A.get(0)) >= 0) || (reversed && B.get(i).compareTo(A.get(0)) <= 0))
                        break;
                A.addAll(0, B.subList(0, i));
            }
        }
        return A.size();
    }

    public static <T> List reverse(List<T> list) {
        List<T> reversed = new ArrayList<>();
        for (T item : list) {
            reversed.add(0, item);
        }
        return reversed;
    }

    public static String formatSignature(String str) {
        return str.replace("[code]", "").replace("[/code]", "");
    }

    public static void downloadAttachment(Context context, Post.Attachment attachment) {
        try {
            String attUrl = MyUtils.getAttachmentFileUrl(attachment);
            try {
                DownloadManager.Request request = new DownloadManager.Request(Uri.parse(attUrl));
                request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, attachment.getFilename());
                request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); // to notify when download is complete
                request.allowScanningByMediaScanner();// if you want to be available from media players
                DownloadManager manager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE);
                manager.enqueue(request);
            } catch (SecurityException e) {
                // in the case of user rejects the permission request
                e.printStackTrace();
                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(attUrl));
                context.startActivity(intent);
            }
        } catch (UnsupportedEncodingException e) {
            // say bye-bye to your poor phone
            Toast.makeText(context, "say bye-bye to your poor phone", Toast.LENGTH_SHORT).show();
        }
    }

    public static void setToolbarOffset(CoordinatorLayout coordinatorLayout, AppBarLayout appBarLayout, int offset) {
        CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
        AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) layoutParams.getBehavior();
        if (behavior != null) {
            behavior.setTopAndBottomOffset(offset);
            behavior.onNestedPreScroll(coordinatorLayout, appBarLayout, null, 0, 1, new int[2]);
        }
    }

    public static String getAttachmentImageUrl(Post.Attachment attachment) {
        return Constants.ATTACHMENT_IMAGE_URL + attachment.getAttachmentId() + attachment.getSecret();
    }

    @SuppressWarnings("WeakerAccess")
    public static String getAttachmentFileUrl(Post.Attachment attachment) throws UnsupportedEncodingException{
        return "https://chaoli.club/index.php/attachment/" + attachment.getAttachmentId() + "_" + URLEncoder.encode(attachment.getFilename(), "UTF-8");
    }

    /**
     * 去除引用
     * @param content API获得的帖子内容
     * @return 去除引用之后的内容，显示给用户或用于在发帖时避免多重引用
     */
    public static String removeQuote(String content) {
        return content.replaceAll(quoteRegex, "");
    }

    private static String replaceMany(String content) {
        return content.replaceAll(codeRegex, ChaoliApplication.getAppContext().getString(R.string.see_codes_in_original_post))
                .replaceAll(imgRegex, ChaoliApplication.getAppContext().getString(R.string.see_img_in_original_post))
                .replaceAll(attRegex, ChaoliApplication.getAppContext().getString(R.string.see_att_in_original_post));
    }

    public static String formatQuote(String quote) {
        return removeQuote(replaceMany(quote));
    }

}
import android.databinding.ObservableArrayList;
import android.databinding.ObservableBoolean;
import android.databinding.ObservableField;
import android.databinding.ObservableInt;
import android.util.Log;
import android.view.View;
import com.daquexian.chaoli.forum.ChaoliApplication;
import com.daquexian.chaoli.forum.R;
import com.daquexian.chaoli.forum.binding.ConversationLayoutSelector;
import com.daquexian.chaoli.forum.binding.LayoutSelector;
import com.daquexian.chaoli.forum.data.Me;
import com.daquexian.chaoli.forum.meta.Channel;
import com.daquexian.chaoli.forum.meta.Constants;
import com.daquexian.chaoli.forum.model.Conversation;
import com.daquexian.chaoli.forum.model.ConversationListResult;
import com.daquexian.chaoli.forum.model.NotificationList;
import com.daquexian.chaoli.forum.network.MyOkHttp;
import com.daquexian.chaoli.forum.network.MyRetrofit;
import com.daquexian.chaoli.forum.utils.AccountUtils;
import com.daquexian.chaoli.forum.utils.LoginUtils;
import com.daquexian.chaoli.forum.utils.MyUtils;
import java.util.List;
import java.util.concurrent.TimeUnit;
import rx.Observable;
import rx.Observer;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import rx.subscriptions.CompositeSubscription;
    /**
     * 去掉刷新时的圆圈
     */
    private void removeCircle() {
        isRefreshing.set(false);
        isRefreshing.notifyChange();
    }

    /**
     * 显示刷新时的圆圈
     */
    private void showCircle() {
        isRefreshing.set(true);
        isRefreshing.notifyChange();
    }

    public void onClickAvatar(View view) {
        if (isLoggedIn.get()) {
            goToHomepage.notifyChange();
        } else {
            goToLogin.notifyChange();
        }
    }

    public void onClickPostFab(View view) {
        goToPost.notifyChange();
    }

    public void onClickConversation(Conversation conversation) {
        Log.d(TAG, "onClickConversation: ");
        clickedConversation = conversation;
        goToConversation.notifyChange();
    }

    public void tryToLoadFromBottom() {
        if (canAutoLoad) {
            canAutoLoad = false;
            loadMore();
        }
    }

    public void startUp() {
        Log.d(TAG, "startUp() called");
        isRefreshing.set(true);

        if (LoginUtils.hasSavedData()) {
            isLoggedIn.set(true);   // isLoggedIn is only to determine whether there are available username, signature etc to show

            Me.setInstanceFromSharedPreference(ChaoliApplication.getAppContext(), LoginUtils.getSavedUsername());
            if (!Me.isEmpty()) {
                myUsername.set(LoginUtils.getSavedUsername());
                myAvatarSuffix.set(Me.getMyAvatarSuffix());
                mySignature.set(Me.getMySignature());
            } else {
                myUsername.set(getString(R.string.loading));
                mySignature.set(getString(R.string.loading));
            }
        }

        LoginUtils.begin_login(new LoginUtils.LoginObserver() {
            @Override
            public void onLoginSuccess(int userId, String token) {
                failed.set(false);
                isLoggedIn.set(true);
                loginComplete.set(true);

                Log.d(TAG, "onLoginSuccess: success");
                getProfile();

                if (channel.equals("") || channel.equals("all")) {
                    toFirstLoadConversation.notifyChange();   // update conversations
                }
                intervalSend();
            }

            @Override
            public void onLoginFailure(int statusCode) {
                if (statusCode == LoginUtils.EMPTY_UN_OR_PW) {
                    if (channel.equals("") || channel.equals("all")) {
                        toFirstLoadConversation.notifyChange();   // update conversations
                    }
                    failed.set(false);
                    isLoggedIn.set(false);
                    loginComplete.set(true);
                } else {
                    /**
                     * show error screen here
                     */
                    failed.set(true);
                    isRefreshing.set(false);
                    // no isLoggedIn.set(false) here, show name, signature in drawer
                    loginComplete.set(true);
                    toastContent = getString(R.string.network_err);
                    showToast.notifyChange();
                }
                Log.d(TAG, "onLoginFailure: " + statusCode);
            }
        });
    }

    public void getProfile() {
        Log.d(TAG, "getProfile() called, username = " + myUsername.get() + ", " + Me.getMyUsername());
        AccountUtils.getProfile(new AccountUtils.GetProfileObserver() {
            @Override
            public void onGetProfileSuccess() {
                myUserId.set(Me.getMyUserId());
                myUsername.set(Me.getMyUsername());
                myAvatarSuffix.set(Me.getMyAvatarSuffix());
                mySignature.set(Me.getMySignature());
            }

            @Override
            public void onGetProfileFailure() {

            }
        });
    }


    private void intervalSend(){
        subscription.add(Observable.interval(Constants.getNotificationInterval,