app/src/main/java/com/zfdang/zsmth_android/models/Mail.java
public class Mail implements Parcelable {
  public String url;
  public String title;
  public String author;
  public String date;
  public boolean isNew;

  public boolean isCategory;
  public String category;

  // the following two fields are for refer posts
  public String fromBoard;
  public String referIndex;

  public Mail(String categoryName) {
    isCategory = true;
    category = categoryName;
  }

  public Mail() {
    isCategory = false;
  }

  public String getFrom() {
    String result = author;
    if (fromBoard != null && fromBoard.length() > 0) {
      result += String.format(" @ [%s]", fromBoard);
    }
    return result;
  }

  public String getMailIDFromURL() {
    if (url != null && url.length() > 0) {
      // '/nForum/mail/inbox/192.json'
      Pattern pattern = Pattern.compile("(\\d+)", Pattern.DOTALL);
      Matcher matcher = pattern.matcher(url);
      if (matcher.find()) {
        String mailid = matcher.group(0);
        return mailid;
      }
    }
    return "";
  }

  public boolean isRefferedPost() {
    return this.fromBoard != null && this.fromBoard.length() > 0;
  }

  @Override public String toString() {
    return "Mail{"
        + "author='"
        + author
        + '\''
        + ", url='"
        + url
        + '\''
        + ", title='"
        + title
        + '\''
        + ", date='"
        + date
        + '\''
        + ", isNew="
        + isNew
        + ", isCategory="
        + isCategory
        + ", category='"
        + category
        + '\''
        + ", fromBoard='"
        + fromBoard
        + '\''
        + ", referIndex="
        + referIndex
        + '}';
  }

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

  @Override public void writeToParcel(Parcel dest, int flags) {
    dest.writeString(this.url);
    dest.writeString(this.title);
    dest.writeString(this.author);
    dest.writeString(this.date);
    dest.writeByte(isNew ? (byte) 1 : (byte) 0);
    dest.writeByte(isCategory ? (byte) 1 : (byte) 0);
    dest.writeString(this.category);
    dest.writeString(this.fromBoard);
    dest.writeString(this.referIndex);
  }

  protected Mail(Parcel in) {
    this.url = in.readString();
    this.title = in.readString();
    this.author = in.readString();
    this.date = in.readString();
    this.isNew = in.readByte() != 0;
    this.isCategory = in.readByte() != 0;
    this.category = in.readString();
    this.fromBoard = in.readString();
    this.referIndex = in.readString();
  }

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

    @Override public Mail[] newArray(int size) {
      return new Mail[size];
    }
  };
}
app/src/main/java/com/zfdang/zsmth_android/models/BoardSection.java
public class BoardSection {
  public String sectionURL;
  public String sectionName;
  public String parentName;

  public String getSectionPath() {
    if (parentName == null || parentName.length() == 0) {
      return sectionName;
    } else {
      return String.format("%s | %s", parentName, sectionName);
    }
  }

  @Override
  public String toString() {
    return "BoardSection{" +
            "sectionURL='" + sectionURL + '\'' +
            ", sectionName='" + sectionName + '\'' +
            ", parentName='" + parentName + '\'' +
            '}';
  }
}
app/src/main/java/com/zfdang/zsmth_android/models/Topic.java
public class Topic implements Parcelable {

  static private final int POST_PER_PAGE = 10;

  // 分隔符，只有一个category的名称
  public boolean isCategory;
  private String category;

  // 正常的主题
  private String boardEngName;
  private String boardChsName;

  private String topicID;
  private String title;
  private String author;
  private String publishDate;
  private String replier;
  private String replyDate;

  // 是否是置顶的主题
  public boolean isSticky;

  // status of topic
  private String score = null;
  private String likes = null;
  private String replyCounts = null;
  private boolean hasAttach;

  private int totalPageNo = 0;
  private int totalPostNo = 0;

  // this is actually a category, used in guidance fragment to seperate hot topics
  public Topic(String category) {
    isCategory = true;
    this.category = category;
  }

  public Topic() {
    isCategory = false;
    this.boardChsName = "";
    this.boardEngName = "";
    this.author = "";
  }

  public void setTotalPostNoFromString(String totalPostNoString) {
    try {
      this.totalPostNo = Integer.parseInt(totalPostNoString);
      if (this.totalPostNo % Topic.POST_PER_PAGE == 0) {
        this.totalPageNo = this.totalPostNo / Topic.POST_PER_PAGE;
      } else {
        this.totalPageNo = this.totalPostNo / Topic.POST_PER_PAGE + 1;
      }
    } catch (Exception e) {
      Log.d("Topic", Log.getStackTraceString(e));
    }
  }

  public int getTotalPageNo() {
    return totalPageNo;
  }

  public String getTotalPostNoAsStr() {
    if (this.totalPostNo == 0) return "";

    String result = String.format("%d", this.totalPostNo);
    return result;
  }

  public String getTitle() {
    return title;
  }

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

  public String getAuthor() {
    return author;
  }

  public void setAuthor(String author) {
    this.author = author;
  }

  public String getBoardEngName() {
    return boardEngName;
  }

  public String getTopicURL() {
    return boardEngName;
  }

  public void setBoardEngName(String boardEngName) {
    this.boardEngName = boardEngName;
  }

  public String getBoardChsName() {
    return boardChsName;
  }

  public void setBoardChsName(String boardChsName) {
    this.boardChsName = boardChsName;
  }

  public String getBoardName() {
    if (boardChsName != null && boardChsName.length() > 0) {
      return boardChsName + " [" + boardEngName + "]";
    } else {
      return boardEngName;
    }
  }

  public String getTopicID() {
    return topicID;
  }

  public void setTopicID(String topicID) {
    this.topicID = topicID;
  }

  public String getCategory() {
    return category;
  }

  public void setCategory(String category) {
    this.category = category;
  }

  public String getPublishDate() {
    return publishDate;
  }

  public void setPublishDate(String publishDate) {
    this.publishDate = publishDate;
  }

  public String getReplier() {
    return replier;
  }

  public void setReplier(String replier) {
    this.replier = replier;
  }

  public String getReplyDate() {
    return replyDate;
  }

  public void setReplyDate(String replyDate) {
    this.replyDate = replyDate;
  }

  public boolean hasAttach() {
    return hasAttach;
  }

  public void setHasAttach(boolean hasAttach) {
    this.hasAttach = hasAttach;
  }

  public String getLikes() {
    return likes;
  }

  public void setLikes(String likes) {
    this.likes = likes;
  }

  public String getReplyCounts() {
    return replyCounts;
  }

  public void setReplyCounts(String replyCounts) {
    this.replyCounts = replyCounts;
  }

  public String getScore() {
    return score;
  }

  public void setScore(String score) {
    this.score = score;
  }

  public String getStatusSummary() {
    String result = String.format("回复: %-4s", replyCounts);
    if (likes != null && likes.length() > 0) {
      result += String.format("    Likes: %-4s    评分: %-4s", likes, score);
    }
    return result;
  }

  @Override public String toString() {
    if (isCategory) {
      return "Category " + this.category;
    } else {
      if (isSticky) {
        return String.format("置顶: (%s) %s by %s, %s @ %s", this.topicID, this.title, this.author, this.publishDate, this.boardEngName);
      } else {
        return String.format("(%s) %s by %s, %s @ %s", this.topicID, this.title, this.author, this.publishDate, this.boardEngName);
      }
    }
  }

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

  @Override public void writeToParcel(Parcel dest, int flags) {
    dest.writeByte(isCategory ? (byte) 1 : (byte) 0);
    dest.writeString(this.category);
    dest.writeString(this.boardEngName);
    dest.writeString(this.boardChsName);
    dest.writeString(this.topicID);
    dest.writeString(this.title);
    dest.writeString(this.author);
    dest.writeString(this.publishDate);
    dest.writeString(this.replier);
    dest.writeString(this.replyDate);
    dest.writeByte(isSticky ? (byte) 1 : (byte) 0);
    dest.writeString(this.score);
    dest.writeString(this.likes);
    dest.writeString(this.replyCounts);
    dest.writeByte(hasAttach ? (byte) 1 : (byte) 0);
    dest.writeInt(this.totalPageNo);
    dest.writeInt(this.totalPostNo);
  }

  protected Topic(Parcel in) {
    this.isCategory = in.readByte() != 0;
    this.category = in.readString();
    this.boardEngName = in.readString();
    this.boardChsName = in.readString();
    this.topicID = in.readString();
    this.title = in.readString();
    this.author = in.readString();
    this.publishDate = in.readString();
    this.replier = in.readString();
    this.replyDate = in.readString();
    this.isSticky = in.readByte() != 0;
    this.score = in.readString();
    this.likes = in.readString();
    this.replyCounts = in.readString();
    this.hasAttach = in.readByte() != 0;
    this.totalPageNo = in.readInt();
    this.totalPostNo = in.readInt();
  }

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

    @Override public Topic[] newArray(int size) {
      return new Topic[size];
    }
  };
}
app/src/main/java/com/zfdang/zsmth_android/models/BoardListContent.java
public class BoardListContent {

  // used by FavoriteBoardFragment
  public static final List<Board> FAVORITE_BOARDS = new ArrayList<>();

  // used by AllBoardFragment
  public static final List<Board> ALL_BOARDS = new ArrayList<>();

  public static void addFavoriteItem(Board item) {
    FAVORITE_BOARDS.add(item);
  }

  public static void addAllBoardItem(Board item) {
    ALL_BOARDS.add(item);
  }

  public static class ChineseComparator implements Comparator<Board> {
    RuleBasedCollator collator = (RuleBasedCollator) Collator.getInstance(Locale.CHINA);

    public int compare(Board b1, Board b2) {
      return collator.compare(b1.getBoardChsName(), b2.getBoardChsName());
    }
  }

  public static void sortAllBoardItem() {
    // sort boards by chinese name
    Collections.sort(ALL_BOARDS, new BoardListContent.ChineseComparator());
  }

  public static void clearFavorites() {
    FAVORITE_BOARDS.clear();
  }

  public static void clearAllBoards() {
    ALL_BOARDS.clear();
  }
}
app/src/main/java/com/zfdang/zsmth_android/helpers/StringUtils.java
public class StringUtils {
  private static SimpleDateFormat dateformat = null;

  public static String getFormattedString(Date date) {
    if (dateformat == null) dateformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    if (date != null) return dateformat.format(date);
    return "";
  }

  public static String subStringBetween(String line, String str1, String str2) {
    if (line == null || line.length() == 0) {
      return "";
    }

    int idx1 = line.indexOf(str1);
    int idx2 = line.lastIndexOf(str2);
    if (idx1 != -1 && idx2 != -1) {
      return line.substring(idx1 + str1.length(), idx2);
    }
    {
      return "";
    }
  }

  // [团购]3.28-4.03 花的传说饰品团购(18) ==> 18
  public static String getReplyCountInParentheses(String content) {
    Pattern hp = Pattern.compile("\\((\\d+)\\)$", Pattern.DOTALL);
    Matcher hm = hp.matcher(content);
    if (hm.find()) {
      String count = hm.group(1);
      return count;
    }

    return "";
  }

  // /nForum/board/ADAgent_TG ==> ADAgent_TG
  // /nForum/article/RealEstate/5017593 ==> 5017593
  public static String getLastStringSegment(String content) {
    if (content == null || content.length() == 0) {
      return "";
    }
    String[] segments = content.split("/");
    if (segments.length > 0) {
      return segments[segments.length - 1];
    }
    return "";
  }

  public static String lookupIPLocation(String content) {
    Pattern myipPattern = Pattern.compile("FROM[: ]*(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.)[\\d\\*]+");
    Matcher myipMatcher = myipPattern.matcher(content);
    while (myipMatcher.find()) {
      String ipl = myipMatcher.group(1);
      if (ipl.length() > 5) {
        ipl = "$1\\*(" + SMTHApplication.geoDB.getLocation(ipl + "1") + ")";
      } else {
        ipl = "$1\\*";
      }
      content = myipMatcher.replaceAll(ipl);
    }
    return content;
  }

  public static String lookupIPLocationInProfile(String content) {
    Pattern myipPattern = Pattern.compile("(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.)[\\d\\*]+");
    Matcher myipMatcher = myipPattern.matcher(content);
    while (myipMatcher.find()) {
      String ipl = myipMatcher.group(1);
      if (ipl.length() > 5) {
        ipl = "$1\\*(" + SMTHApplication.geoDB.getLocation(ipl + "1") + ")";
      } else {
        ipl = "$1\\*";
      }
      content = myipMatcher.replaceAll(ipl);
    }
    return content;
  }

  public static boolean isEmptyString(String content) {
    // non-empty if it's long enough
    if (content.length() > 20) {
      return false;
    }

    String text = Html.fromHtml(content).toString();
    for (int i = 0; i < text.length(); i++) {
      int value = Character.codePointAt(text, i);
      // http://www.utf8-chartable.de/unicode-utf8-table.pl?utf8=dec
      // 如果value不是控制符(0~31)，value不是SPACE(32)，不是"NO-BREAK SPACE"(160), 则认为是非空的
      if (value > 32 && value != 160) {
        return false;
      }
    }
    return true;
  }

  // ellipsize in the mid
  public static String getEllipsizedMidString(String content, int maxLen) {
    if (maxLen < 5) {
      maxLen = 5;
    }

    int length = content.length();
    if (length <= maxLen) {
      return content;
    }

    int pos1 = (maxLen - 3) / 2;
    int pos2 = length - (maxLen - 3 - pos1);

    String result = content.substring(0, pos1) + "..." + content.substring(pos2, length);

    Log.d("Ellipsize", "getEllipsizedMidString: " + content + "==>" + result);
    return result;
  }
}
app/src/main/java/com/zfdang/zsmth_android/models/MailListContent.java
public class MailListContent {

  public static int totalPages = 1;
  public static int totalMails = 0;
  public static int MailNumberPerPage = 20;

  public static void setTotalMails(int totalMails) {
    MailListContent.totalMails = totalMails;

    if (MailListContent.totalMails % MailNumberPerPage == 0) {
      totalPages = MailListContent.totalMails / MailNumberPerPage;
    } else {
      totalPages = MailListContent.totalMails / MailNumberPerPage + 1;
    }
  }

  public static final List<Mail> MAILS = new ArrayList<Mail>();

  public static void clear() {
    MAILS.clear();
    totalMails = 0;
    totalPages = 1;
  }

  public static void addItem(Mail item) {
    MAILS.add(item);
  }
}
app/src/main/java/com/zfdang/SMTHApplication.java
public class SMTHApplication extends Application {
  // http://blog.csdn.net/lieren666/article/details/7598288
  // Android Application的作用
  private static Context context;
  public static String App_Title_Prefix = "zSMTH - ";

  public static final String FROM_BOARD = "From_Board";
  public static final String FROM_BOARD_HOT = "FROM_HOTTOPICS";
  public static final String FROM_BOARD_BOARD = "FROM_BOARDTOPICS";
  public static final String ATTACHMENT_URLS = "ATTACHMENT_URLS";
  public static final String ATTACHMENT_CURRENT_POS = "ATTACHMENT_CURRENT_POS";
  public static final String QUERY_USER_INFO = "QUERY_USER_ID";
  public static final String BOARD_OBJECT = "BOARD_OBJECT";
  public static final String TOPIC_OBJECT = "TOPIC_OBJECT";

  public static final String MAIL_OBJECT = "MAIL_OBJECT";

  // MaintainUserStatusService to UserStatusReceiver, to onNewIntent
  public static final String SERVICE_NOTIFICATION_MESSAGE = "SERVICE_NOTIFICATION_MESSAGE";

  public static final String USER_SERVICE_RECEIVER = "USER_SERVICE_RECEIVER";

  public static final String COMPOSE_POST_CONTEXT = "Compose_Post_Context";

  public static final String NOTIFICATION_NEW_MAIL = "你有新邮件!";
  public static final String NOTIFICATION_NEW_AT = "你有新@!";
  public static final String NOTIFICATION_NEW_REPLY = "你有新回复!";
  public static final String NOTIFICATION_NEW_LIKE = "你有新Like!";

  // IP database
  public static GEODatabase geoDB;

  public static boolean isValidUser() {
    return activeUser != null && !activeUser.getId().equals("guest");
  }

  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
  }

  // current logined user
  public static UserStatus activeUser;

  public void onCreate() {
    super.onCreate();
    SMTHApplication.context = getApplicationContext();

    // init IP lookup database
    geoDB = new GEODatabase(this);

    // init shareSDK
    MobSDK.init(this);

    // init umeng SDK
    UMConfigure.init(this, "56e8c05567e58e0a9e0011cc", "UMENG_CHANNEL", UMConfigure.DEVICE_TYPE_PHONE, null);

    // init Fresco
    // Set<RequestListener> requestListeners = new HashSet<>();
    // requestListeners.add(new RequestLoggingListener());
    OkHttpClient httpClient = SMTHHelper.getInstance().mHttpClient;
    ImagePipelineConfig config = OkHttpImagePipelineConfigFactory.newBuilder(context, httpClient)
        //                .setRequestListeners(requestListeners)
                        .setDownsampleEnabled(false)
        .build();
    Fresco.initialize(context, config);
    // FLog.setMinimumLoggingLevel(FLog.VERBOSE);

    boolean bNightMode = Settings.getInstance().isNightMode();
    if (bNightMode) {
      AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
    } else {
      AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
    }
  }

  public static Context getAppContext() {
    return SMTHApplication.context;
  }
}
app/src/main/java/com/zfdang/zsmth_android/helpers/MakeList.java
public class MakeList {
  public static <E> List<E> makeList(Iterable<E> iter) {
    List<E> list = new ArrayList<E>();
    for (E item : iter) {
      list.add(item);
    }
    return list;
  }
}
app/src/main/java/com/zfdang/zsmth_android/models/Post.java
public class Post {
  private static final String ATTACHMENT_MARK = "###ZSMTH_ATTACHMENT###";
  public static int ACTION_DEFAULT = 0;
  public static int ACTION_FIRST_POST_IN_SUBJECT = 1;
  public static int ACTION_PREVIOUS_POST_IN_SUBJECT = 2;
  public static int ACTION_NEXT_POST_IN_SUBJECT = 3;
  private String postID;
  private String title;
  private String author;
  private String nickName;
  private Date date;
  private String position;

  private List<Like> likes;
  private List<Attachment> attachFiles;

  private String htmlContent; // likes are not included
  private String htmlContentAndLikes; // contents and likes are merged together
  private List<ContentSegment> mSegments;  // parsed from htmlCompleteContent

  public Post() {
    date = new Date();
  }

  public String getPostID() {
    return postID;
  }

  public void setPostID(String postID) {
    this.postID = postID;
  }

  public String getTitle() {
    return this.title;
  }

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

  public String getAuthor() {
    if (nickName == null || nickName.length() == 0) {
      return this.author;
    } else {
      return String.format("%s(%s)", this.author, this.nickName);
    }
  }

  public void setAuthor(String author) {
    this.author = author;
  }

  public String getRawAuthor() {
    return this.author;
  }

  public void setNickName(String nickName) {
    final int MAX_NICKNAME_LENGTH = 12;
    if (nickName.length() > MAX_NICKNAME_LENGTH) {
      nickName = nickName.substring(0, MAX_NICKNAME_LENGTH) + "..";
    }
    this.nickName = nickName;
  }

  public void setDate(Date date) {
    this.date = date;
  }

  public String getFormatedDate() {
    return StringUtils.getFormattedString(this.date);
  }

  public String getPosition() {
    return position;
  }

  public void setPosition(String position) {
    this.position = position;
  }

  public void parsePostContent(Element content, boolean isPost) {
    Element pureContent = null;
    if (isPost) {
      // 1. parse likes node first
      // <div class="likes">
      Elements likeNodes = content.select("div.likes");
      if (likeNodes.size() == 1) {
        Element likeNode = likeNodes.first();
        ParseLikeElementInPostContent(likeNode);
        likeNode.remove();
      }

      // 2. remove like button
      // <button class="button add_like"
      Elements likeButtons = content.select("button.add_like");
      if(likeButtons.size() == 1) {
        Element likeButton = likeButtons.get(0);
        likeButton.remove();
      }

      // on Oct.19, SMTH add topic id in front of author
      // <font style="display: none">1026567117</font>发信人: areshuang (壹剑客), 信区: SecondDigi
      // remove this meaningless block
      for (Element font : content.select("font[style='display: none']")) {
        font.remove();
      }

      // 4. take td content as the pure post content
      pureContent = Jsoup.parse(content.html());
    } else {
      // from email, content not enclosed by <p> tag
      pureContent = content;
    }

    // 2. parse post pure content, result in htmlContent
    parsePostPureContent(pureContent);

    // 3. merge contents and likes, result in htmlContentAndLikes
    mergePureContentAndLikes();

    // 4. parse contents into segments
    parseContentToSegments();
  }

    // parse like list in post content
    public void ParseLikeElementInPostContent(Element likeNode) {
      if(likes == null) {
          likes = new ArrayList<>();
      }
      likes.clear();

        // <div class="like_name">有36位用户评价了这篇文章：</div>
        Elements nodes = likeNode.select("div.like_name");
        if (nodes.size() == 1) {
            Element node = nodes.first();
        }

        // <li><span class="like_score_0">[&nbsp;&nbsp;]</span><span class="like_user">fly891198061:</span>
        // <span class="like_msg">无法忍受，我不会变节，先斗智，不行就自杀！来个痛快的~！</span>
        // <span class="like_time">(2016-03-27 15:04)</span></li>
        nodes = likeNode.select("li");
        for (Element n : nodes) {
          Elements spans = n.select("span");
          if(spans.size() == 4) {
            Like like = new Like(spans.get(0).text(), spans.get(1).text(), spans.get(2).text(), spans.get(3).text());
            likes.add(like);
          }
        }
    }

  // parse post pure content, then merge them to htmlCompleteContent, then split it into htmlSegments
  public void parsePostPureContent(Element content) {
    // find all attachment from node
    // <a target="_blank" href="http://att.newsmth.net/nForum/att/AutoWorld/1939790539/4070982">
    // <img border="0" title="单击此查看原图" src="http://att.newsmth.net/nForum/att/AutoWorld/1939790539/4070982/large" class="resizeable">
    // </a>
    // special image attachment: webp. newsmth does not support webp now, so it's not displayed as img
    // <a href="http://att.newsmth.net/nForum/att/MMJoke/1634780953/211" target="_blank">1.webp</a>

    // new image URL after 2017-06-28
    // <a target="_blank" href="//att.newsmth.net/nForum/att/Picture/1726713/228774">
    // <img border="0" title="单击此查看原图" src="//att.newsmth.net/nForum/att/Picture/1726713/228774/large" class="resizeable" /></a>

    // new image url after 2020-06-10
    // <a target="_blank" href="//static.mysmth.net/nForum/att/FamilyLife/1763462541/17096">
    // <img border="0" title="单击此查看原图" src="//static.mysmth.net/nForum/att/FamilyLife/1763462541/17096/large" class="resizeable" /></a>

    // other attachment
    // 		<a href="//static.mysmth.net/nForum/att/Test/943486/245" target="_blank">《三国演义》_(果麦经典)_罗贯中.epub</a>

    Elements as = content.select("a[href]");
    for (Element a : as) {
      // process each a|href
      Elements imgs = a.select("img[src]");
      if (imgs.size() == 1) {
        // find one image attachment
        String origImageSrc = a.attr("href");

        Element img = imgs.first();
        String resizedImageSrc = img.attr("src");

        Attachment attach = new Attachment(origImageSrc, resizedImageSrc);
        this.addAttachFile(attach);

        // replace a[href] with MARK
        // we will split the string with MARK, so make sure no two MAKR will stay together
        a.html(ATTACHMENT_MARK + " ");
      } else if (imgs.size() == 0) {
        // does not find any image element, handle the special webp
        String attachName = a.text();
        if (attachName != null && attachName.endsWith(".webp")) {
          // this is a webp attachment, show it as image
          String origImageSrc = a.attr("href");
          Attachment attach = new Attachment(origImageSrc, origImageSrc);
          this.addAttachFile(attach);

          a.html(ATTACHMENT_MARK + " ");
        } else {
          // other attachment, add link for downloading
          String downloadURL = a.attr("href");
          downloadURL = SMTHHelper.preprocessSMTHImageURL(downloadURL);
          if (downloadURL.contains("/nForum/att/")){
            a.append("<br>" + downloadURL);
          }
        }
      }
    }

    // process pure post content
    // it's important to know that not all HTML tags are supported by Html.fromHtml, see the supported list
    // https://commonsware.com/blog/Android/2010/05/26/html-tags-supported-by-textview.html
    // http://stackoverflow.com/questions/18295881/android-textview-html-font-size-tag
    String formattedEscapedPlainText = TextUtils.htmlEncode(Html.fromHtml(content.html()).toString());
    this.htmlContent = this.parsePostPureContentFormat(formattedEscapedPlainText);
  }

  public void mergePureContentAndLikes(){
      htmlContentAndLikes = this.htmlContent;

      if (likes != null && likes.size() > 0) {
        StringBuilder wordList = new StringBuilder();
        wordList.append("有" + likes.size() + "位用户评价了这篇文章:");
        wordList.append("<br/>");
        wordList.append("<small>");
        for (Like like : likes) {
          wordList.append("[<font face='monospace'>" + like.score + "</font>]");
          wordList.append(" " + like.msg);
          wordList.append(" ( " + like.user);
          wordList.append(" @ " + like.time);
          wordList.append(" )<br/>");
        }
        wordList.append("</small>");
        htmlContentAndLikes += new String(wordList);
    }
  }

  // split complete content with ATTACHMENT_MARK
  private void parseContentToSegments() {
    if (mSegments == null) {
      mSegments = new ArrayList<>();
    }
    mSegments.clear();

    if (attachFiles == null || attachFiles.size() == 0) {
      // no attachment, add all content as one segment
      mSegments.add(new ContentSegment(ContentSegment.SEGMENT_TEXT, htmlContentAndLikes));
    } else {
      // when there are attachments here, separate them one by one
      String[] segments = htmlContentAndLikes.split(ATTACHMENT_MARK);

      // add segments and attachments together
      int attachIndex = 0;
      for (String segment : segments) {
        //                Log.d("Splited Result:", String.format("{%s}", segment));
        // add segment to results if it's not empty,
        // MARK are seperated by several <br />, we should skip these seperated text
        if (!StringUtils.isEmptyString(segment) || attachIndex == 0) {
          // since we expect there will always be a textview before imageview
          // even the first text segment is empty, we still add it
          mSegments.add(new ContentSegment(ContentSegment.SEGMENT_TEXT, segment));
        }

        // add next image attachment to results
        if (attachFiles != null && attachIndex < attachFiles.size()) {
          Attachment attach = attachFiles.get(attachIndex);
          String imageURL = null;
          if (Settings.getInstance().isLoadOriginalImage()) {
            imageURL = attach.getOriginalImageSource();
          } else {
            imageURL = attach.getResizedImageSource();
          }
          ContentSegment img = new ContentSegment(ContentSegment.SEGMENT_IMAGE, imageURL);
          img.setImgIndex(attachIndex);
          mSegments.add(img);
        }

        attachIndex++;
      }
    }

    //        Log.d("ContentSegment", String.format("Total segments here: %d", mSegments.size()));
    //        for (ContentSegment content : mSegments) {
    //            if (content.getType() == ContentSegment.SEGMENT_IMAGE) {
    //                Log.d("ContentSegment", String.format("Image %s, index = %d", content.getUrl(), content.getImgIndex()));
    //            } else if (content.getType() == ContentSegment.SEGMENT_TEXT) {
    //                Log.d("ContentSegment", String.format("Text, {%s}", content.getSpanned().toString()));
    //            }
    //        }
  }

  /*
  the expected input is formatted plain text, no html tag is expected
  line break by \n, but not <br>
  */
  private String parsePostPureContentFormat(String content) {
    // &nbsp; is converted as code=160, but not a whitespace (ascii=32)
    // http://stackoverflow.com/questions/4728625/why-trim-is-not-working
    content = content.replace(String.valueOf((char) 160), " ");

    // it's important to know that not all HTML tags are supported by Html.fromHtml, see the supported list
    // https://commonsware.com/blog/Android/2010/05/26/html-tags-supported-by-textview.html
    String[] lines = content.split("\n");

    // find signature start line
    int signatureStartLine = -1;
    for (int i = lines.length - 1; i >= 0; i--) {
      String line = lines[i];
      if (line.startsWith("--") && line.length() <= 3) {
        // find the first "--" from the last to the first
        signatureStartLine = i;
        break;
      }
    }

    // process content line by line
    StringBuilder sb = new StringBuilder();
    int linebreak = 0;
    int signatureMode = 0;
    for (int i = 0; i < lines.length; i++) {
      String line = lines[i];

      if ((line.startsWith("发信人:") || line.startsWith("寄信人:")) && i <= 3) {
        // find nickname for author here, skip the line
        // 发信人: schower (schower), 信区: WorkLife
        String nickName = StringUtils.subStringBetween(line, "(", ")");
        if (nickName != null && nickName.length() > 0) {
          this.setNickName(nickName);
        }
        continue;
      } else if (line.startsWith("标  题:") && i <= 3) {
        // skip this line
        continue;
      } else if (line.startsWith("发信站:") && i <= 3) {
        // find post date here, skip the line
        // <br /> 发信站: 水木社区 (Fri Mar 25 11:52:04 2016), 站内
        line = StringUtils.subStringBetween(line, "(", ")");
        SimpleDateFormat simpleFormat = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy", Locale.US);
        try {
          Date localdate = simpleFormat.parse(line);
          this.setDate(localdate);
          continue;
        } catch (ParseException e1) {
          e1.printStackTrace();
        }
      }

      // handle ATTACH_MARK
      if (line.contains(ATTACHMENT_MARK)) {
        sb.append(line);
        continue;
      }

      // handle quoted content
      if (line.startsWith(":")) {
        line = "<font color=#299999>" + line + "</font>";
        sb.append(line).append("<br />");
        continue;
      }

      if (line.trim().length() == 0) {
        linebreak++;
        if (linebreak >= 2) {
          // continuous linebreak, skip extra linebreak
          continue;
        } else {
          sb.append(line).append("<br />");
          continue;
        }
      } else {
        // reset counter
        linebreak = 0;
      }

      // handle signature
      // we have to make sure "--" is the last one, it might appear in post content body
      if (i == signatureStartLine) {
        // entering signature mode
        signatureMode = 1;
        sb.append(line).append("<br />");
        continue;
      }

      // ※ 修改:·wpd419 于 Mar 29 09:43:17 2016 修改本文·[FROM: 111.203.75.*]
      // ※ 来源:·水木社区 http://www.newsmth.net·[FROM: 111.203.75.*]
      if (line.contains("※ 来源:·")) {
        // jump out of signature mode
        signatureMode = 0;
        line = line.replace("·", "")
            .replace("http://www.mysmth.net", "")
            .replace("http://www.newsmth.net", "")
            .replace("http://m.mysmth.net", "")
            .replace("http://m.newsmth.net", "")
            .replace("http://mysmth.net", "")
            .replace("http://newsmth.net", "")
            .replace("mysmth.net", "")
            .replace("newsmth.net", "")
            .replace("m.mysmth.net", "")
            .replace("m.newsmth.net", "")
            .replace("官方应用", "")
            .replace("客户端", "");

        line = "<font color=#727272>" + StringUtils.lookupIPLocation(line) + "</font>";
        sb.append(line).append("<br />");
        continue;
      } else if (line.contains("※ 修改:·")) {
        // jump out of signature mode
        signatureMode = 0;
        line = line.replace("·", "").replace("修改本文", "");
        line = "<font color=#727272>" + StringUtils.lookupIPLocation(line) + "</font>";
        sb.append(line).append("<br />");
        continue;
      }

      // after handle last part of post content, if it's still in signature mode, add signature
      if (signatureMode == 1) {
        line = "<small><font color=#727272>" + line + "</font></small>";
        sb.append(line).append("<br />");
        continue;
      }

      // for other normal line, add it directly
      sb.append(line).append("<br />");
    }

    return sb.toString().trim();
  }

  public List<ContentSegment> getContentSegments() {
    return mSegments;
  }

  // used by copy post content menu, or quoted content while replying
  public String getRawContent() {
    return Html.fromHtml(this.htmlContent.replace(ATTACHMENT_MARK, "")).toString();
  }

  // this method will be called when post content can't be fetched properly
  // error message is returned in this case
  public void setRawContent(String rawContent) {
    this.htmlContent = rawContent;
    this.htmlContentAndLikes = rawContent;
    parseContentToSegments();
  }

  public void addAttachFile(Attachment attach) {
    if (attachFiles == null) {
      attachFiles = new ArrayList<>();
    }
    if (attach != null) {
      attachFiles.add(attach);
    }
  }

  public List<Attachment> getAttachFiles() {
    return attachFiles;
  }

  @Override public String toString() {
    return "Post{"
        + "postID='"
        + postID
        + '\''
        + ", title='"
        + title
        + '\''
        + ", author='"
        + author
        + '\''
        + ", nickName='"
        + nickName
        + '\''
        + ", date="
        + date
        + ", position='"
        + position
        + '\''
        + '}';
  }
}
app/src/main/java/com/zfdang/zsmth_android/Settings.java
public class Settings {

  private static final String USERNAME_KEY = "username";
  private String mUsername;

  public String getUsername() {
    return mUsername;
  }

  public void setUsername(String mUsername) {
    if (this.mUsername == null || !this.mUsername.equals(mUsername)) {
      this.mUsername = mUsername;
      mEditor.putString(USERNAME_KEY, this.mUsername);
      mEditor.commit();
    }
  }

  private static final String PASSWORD_KEY = "password";
  private String mPassword;

  public String getPassword() {
    return mPassword;
  }

  public void setPassword(String mPassword) {
    if (this.mPassword == null || !this.mPassword.equals(mPassword)) {
      this.mPassword = mPassword;
      mEditor.putString(PASSWORD_KEY, this.mPassword);
      mEditor.commit();
    }
  }

  // auto login
  private static final String AUTO_LOGIN = "auto_login";
  private boolean bAutoLogin;

  public boolean isAutoLogin() {
    return bAutoLogin;
  }

  public void setAutoLogin(boolean mAutoLogin) {
    if (this.bAutoLogin != mAutoLogin) {
      this.bAutoLogin = mAutoLogin;
      mEditor.putBoolean(AUTO_LOGIN, this.bAutoLogin);
      mEditor.commit();
    }
  }


  // last login success
  private static final String LAST_LOGIN_SUCCESS = "last_login_success";
  private boolean bLastLoginSuccess;

  public boolean isLastLoginSuccess() {
    return bLastLoginSuccess;
  }

  public void setLastLoginSuccess(boolean bLastLoginSuccess) {
    if (this.bLastLoginSuccess != bLastLoginSuccess) {
      this.bLastLoginSuccess = bLastLoginSuccess;
      mEditor.putBoolean(LAST_LOGIN_SUCCESS, this.bLastLoginSuccess);
      mEditor.commit();
    }
  }

  // after user init login action, set online = true;
  // after user init logout action, set online = false;
  // this value will impact autoLogin behaviour of service
  private static final String USER_ONLINE = "user_online";
  private boolean bUserOnline;

  public boolean isUserOnline() {
    return bUserOnline;
  }

  public void setUserOnline(boolean bUserOnline) {
    if (this.bUserOnline != bUserOnline) {
      this.bUserOnline = bUserOnline;
      mEditor.putBoolean(USER_ONLINE, this.bUserOnline);
      mEditor.commit();
    }
  }


  // use device signature or not
  private static final String USE_DEVICE_SIGNATURE = "use_device_signature";
  private boolean bUseSignature;

  public boolean bUseSignature() {
    return bUseSignature;
  }

  public void setUseSignature(boolean bUseSignature) {
    if (this.bUseSignature != bUseSignature) {
      this.bUseSignature = bUseSignature;
      mEditor.putBoolean(USE_DEVICE_SIGNATURE, this.bUseSignature);
      mEditor.commit();
    }
  }

  // customized device signature
  private static final String DEVICE_SIGNATURE = "device_signature";
  private String mSignature;

  public String getSignature() {
    if (mSignature != null && mSignature.length() > 0) {
      return mSignature;
    } else {
      return "Android";
    }
  }

  public void setSignature(String signature) {
    if (this.mSignature == null || !this.mSignature.equals(signature)) {
      this.mSignature = signature;
      mEditor.putString(DEVICE_SIGNATURE, this.mSignature);
      mEditor.commit();
    }
  }

  // show sticky topics or not
  private static final String SHOW_STICKY_TOPIC = "show_sticky_topic";
  private boolean mShowSticky;

  public boolean isShowSticky() {
    return mShowSticky;
  }

  public void setShowSticky(boolean mShowSticky) {
    if (this.mShowSticky != mShowSticky) {
      this.mShowSticky = mShowSticky;
      mEditor.putBoolean(SHOW_STICKY_TOPIC, this.mShowSticky);
      mEditor.commit();
    }
  }

  public void toggleShowSticky() {
    this.mShowSticky = !this.mShowSticky;
    mEditor.putBoolean(SHOW_STICKY_TOPIC, this.mShowSticky);
    mEditor.commit();
  }

  // save forward target
  private static final String FORWARD_TAEGET = "forward_target";
  private String mTarget;

  public String getTarget() {
    return mTarget;
  }

  public void setTarget(String target) {
    if (this.mTarget == null || !this.mTarget.equals(target)) {
      this.mTarget = target;
      mEditor.putString(FORWARD_TAEGET, this.mTarget);
      mEditor.commit();
    }
  }

  // load original image in post list, or load resized image
  // FS image viewer will always load original image
  private static final String LOAD_ORIGINAL_IMAGE = "LOAD_ORIGINAL_IMAGE";
  private boolean bLoadOriginalImage;

  public boolean isLoadOriginalImage() {
    return bLoadOriginalImage;
  }

  public void setLoadOriginalImage(boolean bLoadOriginalImage) {
    if (this.bLoadOriginalImage != bLoadOriginalImage) {
      this.bLoadOriginalImage = bLoadOriginalImage;
      mEditor.putBoolean(LOAD_ORIGINAL_IMAGE, this.bLoadOriginalImage);
      mEditor.commit();
    }
  }


  // load image from cdn, or from smth website directly
  // https://www.mysmth.net/nForum/#!article/PocketLife/3100239
  // https://static.mysmth.net/nForum/#!article/PocketLife/3100239
  private static final String IMAGE_SOURCE_CDN = "IMAGE_SOURCE_CDN";
  private boolean bImageSourceCDN;

  public boolean isImageSourceCDN() {
    return bImageSourceCDN;
  }

  public void setImageSourceCDN(boolean value) {
    if (this.bImageSourceCDN != value) {
      this.bImageSourceCDN = value;
      mEditor.putBoolean(IMAGE_SOURCE_CDN, this.bImageSourceCDN);
      mEditor.commit();
    }
  }

  // show board master only items in postlist actions
  private static final String BOARD_MASTER_ONLY = "BOARD_MASTER_ONLY";
  private boolean bBoardMasterOnly;

  public boolean isBoardMasterOnly() {
    return bBoardMasterOnly;
  }

  public void setBoardMasterOnly(boolean bBoardMasterOnly) {
    if (this.bBoardMasterOnly != bBoardMasterOnly) {
      this.bBoardMasterOnly = bBoardMasterOnly;
      mEditor.putBoolean(BOARD_MASTER_ONLY, this.bBoardMasterOnly);
      mEditor.commit();
    }
  }

  // night mode
  private static final String NIGHT_MODE = "NIGHT_MODE";
  private boolean bNightMode;

  public boolean isNightMode() {
    return bNightMode;
  }

  public void setNightMode(boolean bNightMode) {
    if (this.bNightMode != bNightMode) {
      this.bNightMode = bNightMode;
      mEditor.putBoolean(NIGHT_MODE, this.bNightMode);
      mEditor.commit();
    }
  }

  private static final String LAST_LAUNCH_VERSION = "LAST_LAUNCH_VERSION";
  private int iLastVersion;

  public boolean isFirstRun() {
    PackageManager pm = SMTHApplication.getAppContext().getPackageManager();
    int currentVersion = 0;
    try {
      PackageInfo pi = pm.getPackageInfo(SMTHApplication.getAppContext().getPackageName(), 0);
      currentVersion = pi.versionCode;
    } catch (PackageManager.NameNotFoundException e) {
      Log.e("Setting", "isFirstRun: " + Log.getStackTraceString(e));
    }

    if (currentVersion == iLastVersion) {
      return false;
    } else {
      this.iLastVersion = currentVersion;
      mEditor.putInt(LAST_LAUNCH_VERSION, this.iLastVersion);
      mEditor.commit();
      return true;
    }
  }

  private static final String NOTIFICATION_MAIL = "NOTIFICATION_MAIL";
  private boolean bNotificationMail;

  public boolean isNotificationMail() {
    return bNotificationMail;
  }

  public void setNotificationMail(boolean bNotificationMail) {
    if (this.bNotificationMail != bNotificationMail) {
      this.bNotificationMail = bNotificationMail;
      mEditor.putBoolean(NOTIFICATION_MAIL, this.bNotificationMail);
      mEditor.commit();
    }
  }

  private static final String NOTIFICATION_AT = "NOTIFICATION_AT";
  private boolean bNotificationAt;

  public boolean isNotificationAt() {
    return bNotificationAt;
  }

  public void setNotificationAt(boolean bNotificationAt) {
    if (this.bNotificationAt != bNotificationAt) {
      this.bNotificationAt = bNotificationAt;
      mEditor.putBoolean(NOTIFICATION_AT, this.bNotificationAt);
      mEditor.commit();
    }
  }

  private static final String NOTIFICATION_LIKE = "NOTIFICATION_LIKE";
  private boolean bNotificationLike;

  public boolean isNotificationLike() {
    return bNotificationLike;
  }

  public void setNotificationLike(boolean bNotificationLike) {
    if (this.bNotificationLike != bNotificationLike) {
      this.bNotificationLike = bNotificationLike;
      mEditor.putBoolean(NOTIFICATION_LIKE, this.bNotificationLike);
      mEditor.commit();
    }
  }

  private static final String NOTIFICATION_REPLY = "NOTIFICATION_REPLY";
  private boolean bNotificationReply;

  public boolean isNotificationReply() {
    return bNotificationReply;
  }

  public void setNotificationReply(boolean bNotificationReply) {
    if (this.bNotificationReply != bNotificationReply) {
      this.bNotificationReply = bNotificationReply;
      mEditor.putBoolean(NOTIFICATION_REPLY, this.bNotificationReply);
      mEditor.commit();
    }
  }

  private static final String LAUNCH_HOTTOPIC_AS_ENTRY = "LAUNCH_HOTTOPIC_AS_ENTRY";
  private boolean bLaunchHotTopic;

  public boolean isLaunchHotTopic() {
    return bLaunchHotTopic;
  }

  public void setLaunchHotTopic(boolean bLaunchHotTopic) {
    if (this.bLaunchHotTopic != bLaunchHotTopic) {
      this.bLaunchHotTopic = bLaunchHotTopic;
      mEditor.putBoolean(LAUNCH_HOTTOPIC_AS_ENTRY, this.bLaunchHotTopic);
      mEditor.commit();
    }
  }

  private static final String SHOW_POST_NAVITATION_BAR = "SHOW_POST_NAVITATION_BAR";
  private boolean bPostNavBar;

  public boolean hasPostNavBar() {
    return bPostNavBar;
  }

  public void setPostNavBar(boolean bPostNavBar) {
    if (this.bPostNavBar != bPostNavBar) {
      this.bPostNavBar = bPostNavBar;
      mEditor.putBoolean(SHOW_POST_NAVITATION_BAR, this.bPostNavBar);
      mEditor.commit();
    }
  }

  private static final String VOLUME_KEY_SCROLL = "VOLUME_KEY_SCROLL";
  private boolean bVolumeKeyScroll;

  public boolean isVolumeKeyScroll() {
    return bVolumeKeyScroll;
  }

  public void setVolumeKeyScroll(boolean bVolumeKeyScroll) {
    if (this.bVolumeKeyScroll != bVolumeKeyScroll) {
      this.bVolumeKeyScroll = bVolumeKeyScroll;
      mEditor.putBoolean(VOLUME_KEY_SCROLL, this.bVolumeKeyScroll);
      mEditor.commit();
    }
  }


  // defined in arrays.xml
  // 0: large font; 1: normal font; 2: small, 3: extra small 4: extra large 5: extremely large
  // defaut: 1 - normal font
  private static final String ZSMTH_FONT_INDEX = "ZSMTH_FONT_INDEX";
  private int iFontIndex;

  public int getFontIndex() {
    return iFontIndex;
  }

  public float getFontSizeFloatValue() {
    if (iFontIndex == 1) {
      return 1.0f;
    } else if (iFontIndex == 2) {
      return 0.85f;
    } else if (iFontIndex == 0) {
      return 1.15f;
    } else if (iFontIndex == 3) {
      return 0.65f;
    } else if (iFontIndex == 4) {
      return 1.35f;
    } else if (iFontIndex == 5) {
      return 1.55f;
    }
    return 1.0f;
  }

  public void setFontIndex(int iFontIndex) {
    System.out.println(iFontIndex);
    if (this.iFontIndex != iFontIndex) {
      this.iFontIndex = iFontIndex;
      mEditor.putInt(ZSMTH_FONT_INDEX, this.iFontIndex);
      mEditor.commit();
    }
  }

  // we will cache contents in compose post activity, so that user can restore contents
  private static final String COMPOSE_POST_CACHE = "COMPOSE_POST_CACHE";
  private String mPostCache;

  public String getPostCache() {
    return mPostCache;
  }

  public void setPostCache(String mPostCache) {
    if (this.mPostCache == null || !this.mPostCache.equals(mPostCache)) {
      this.mPostCache = mPostCache;
      mEditor.putString(COMPOSE_POST_CACHE, this.mPostCache);
      mEditor.commit();
    }
  }

  private final String Preference_Name = "ZSMTH_Config";

  private SharedPreferences mPreference;
  private SharedPreferences.Editor mEditor;

  // Singleton
  private static Settings ourInstance = new Settings();

  public static Settings getInstance() {
    return ourInstance;
  }

  private Settings() {
    initSettings();
  }

  // load all settings from SharedPreference
  private void initSettings() {
    // this
    mPreference = SMTHApplication.getAppContext().getSharedPreferences(Preference_Name, Activity.MODE_PRIVATE);
    mEditor = mPreference.edit();

    // load all values from preference to variables
    mShowSticky = mPreference.getBoolean(SHOW_STICKY_TOPIC, false);
    mUsername = mPreference.getString(USERNAME_KEY, "");
    mPassword = mPreference.getString(PASSWORD_KEY, "");
    bAutoLogin = mPreference.getBoolean(AUTO_LOGIN, true);
    bLastLoginSuccess = mPreference.getBoolean(LAST_LOGIN_SUCCESS, false);
    bUserOnline = mPreference.getBoolean(USER_ONLINE, false);

    bUseSignature = mPreference.getBoolean(USE_DEVICE_SIGNATURE, true);
    mSignature = mPreference.getString(DEVICE_SIGNATURE, "");
    if (mSignature.length() == 0) {
      String marketingName = DeviceMarketingName.getInstance(SMTHApplication.getAppContext()).getDeviceMarketingName(false);
      setSignature(marketingName);
    }

    mTarget = mPreference.getString(FORWARD_TAEGET, "");

    bLoadOriginalImage = mPreference.getBoolean(LOAD_ORIGINAL_IMAGE, false);
    bImageSourceCDN = mPreference.getBoolean(IMAGE_SOURCE_CDN, true);

    bBoardMasterOnly = mPreference.getBoolean(BOARD_MASTER_ONLY, false);

    bNightMode = mPreference.getBoolean(NIGHT_MODE, true);

    iLastVersion = mPreference.getInt(LAST_LAUNCH_VERSION, 0);

    bNotificationMail = mPreference.getBoolean(NOTIFICATION_MAIL, true);
    bNotificationAt = mPreference.getBoolean(NOTIFICATION_AT, true);
    bNotificationLike = mPreference.getBoolean(NOTIFICATION_LIKE, true);
    bNotificationReply = mPreference.getBoolean(NOTIFICATION_REPLY, true);

    bLaunchHotTopic = mPreference.getBoolean(LAUNCH_HOTTOPIC_AS_ENTRY, true);

    bPostNavBar = mPreference.getBoolean(SHOW_POST_NAVITATION_BAR, true);
    bVolumeKeyScroll = mPreference.getBoolean(VOLUME_KEY_SCROLL, true);
    iFontIndex = mPreference.getInt(ZSMTH_FONT_INDEX, 1);

    mPostCache = mPreference.getString(COMPOSE_POST_CACHE, "");
  }
}
app/src/main/java/com/zfdang/zsmth_android/models/Board.java
public class Board implements Externalizable, Parcelable {
  // for Externalizable
  static final long serialVersionUID = 20160322L;

  public enum BoardType {
    BOARD,
    SECTION,
    FOLDER,
    INVALID
  }
  BoardType boardType;

  // 具体的版面
  // http://www.newsmth.net/nForum/#!board/DrivingStudy
  private String boardID;       // this field is not longer used in nForum
  private String boardEngName;  // Android
  private String boardChsName;  // 安卓系统设备

  private String categoryName;  // 电脑技术
  private String moderator;     // 版主

  // 用户在收藏夹里创建的目录
  // folderId = 1, folderName = 次常用版面
  private String folderID;      // 1
  private String folderName;    // 次常用版面

  // 系统的二级目录
  // o.o(true,1,678,59144,'[供求]','SecondHand','二手货交易','[目录]',981,677,0);
  // http://www.newsmth.net/nForum/#!section/Automobile
  private String sectionID;    // SecondHand
  private String sectionName;  // 二手货交易
  public String sectionPath;

  // http://stackoverflow.com/questions/21966784/reading-object-from-file-throws-illegalaccessexception
  // used by readObject
  public Board() {
  }

  public void initAsInvalid(String message) {
    boardType = BoardType.INVALID;
    this.boardEngName = message;
    this.categoryName = "错误提示";
  }


  public void initAsBoard(String boardEngName, String boardChsName, String categoryName, String moderator){
    boardType = BoardType.BOARD;

    this.boardEngName = boardEngName;
    this.boardChsName = boardChsName;
    this.categoryName = categoryName;
    if (moderator.length() > 25) {
      moderator = moderator.substring(0, 21) + "...";
    }
    this.moderator = moderator;
  }


  public void initAsFolder(String folderID, String folderName){
    boardType = BoardType.FOLDER;
    this.folderID = folderID;
    this.folderName = folderName;
    this.categoryName = "自定义目录";
  }

  public void initAsSection(String sectionID, String sectionName){
    boardType = BoardType.SECTION;
    this.sectionID = sectionID;
    this.sectionName = sectionName;
    this.categoryName = "二级目录";
  }

  public boolean isBoard(){
    return boardType == BoardType.BOARD;
  }

  public boolean isFolder() {
    return boardType == BoardType.FOLDER;
  }

  public boolean isSection() {
    return boardType == BoardType.SECTION;
  }

  public boolean isInvalid() {
    return boardType == BoardType.INVALID;
  }

  public String getBoardID() {
    return boardID;
  }

  public String getBoardEngName() {
    return boardEngName;
  }

  public String getBoardChsName() {
    return boardChsName;
  }

  public String getCategoryName() {
    return categoryName;
  }

  public String getModerator() {
    return moderator;
  }

  public String getFolderID() {
    return folderID;
  }

  public String getFolderName() {
    return folderName;
  }

  public String getSectionID() {
    return sectionID;
  }

  public String getSectionName() {
    return sectionName;
  }

  public String getBoardName() {
    if (boardChsName == null || boardChsName.length() == 0) {
      return boardEngName;
    } else {
      return String.format("%s [%s]", boardChsName, boardEngName);
    }
  }

  @Override
  public String toString() {
    return "Board{" +
            "boardType=" + boardType +
            ", boardID='" + boardID + '\'' +
            ", boardEngName='" + boardEngName + '\'' +
            ", boardChsName='" + boardChsName + '\'' +
            ", categoryName='" + categoryName + '\'' +
            ", moderator='" + moderator + '\'' +
            ", folderID='" + folderID + '\'' +
            ", folderName='" + folderName + '\'' +
            ", sectionID='" + sectionID + '\'' +
            ", sectionName='" + sectionName + '\'' +
            ", sectionPath='" + sectionPath + '\'' +
            '}';
  }


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

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeInt(this.boardType == null ? -1 : this.boardType.ordinal());
    dest.writeString(this.boardID);
    dest.writeString(this.boardEngName);
    dest.writeString(this.boardChsName);
    dest.writeString(this.categoryName);
    dest.writeString(this.moderator);
    dest.writeString(this.folderID);
    dest.writeString(this.folderName);
    dest.writeString(this.sectionID);
    dest.writeString(this.sectionName);
    dest.writeString(this.sectionPath);
  }

  protected Board(Parcel in) {
    int tmpBoardType = in.readInt();
    this.boardType = tmpBoardType == -1 ? null : BoardType.values()[tmpBoardType];
    this.boardID = in.readString();
    this.boardEngName = in.readString();
    this.boardChsName = in.readString();
    this.categoryName = in.readString();
    this.moderator = in.readString();
    this.folderID = in.readString();
    this.folderName = in.readString();
    this.sectionID = in.readString();
    this.sectionName = in.readString();
    this.sectionPath = in.readString();
  }

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

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

  @Override
  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    if (in.readBoolean()) {
      boardType = BoardType.values()[in.readShort()];
    }
    if (in.readBoolean()) {
      boardID = in.readUTF();
    }
    if (in.readBoolean()) {
      boardEngName = in.readUTF();
    }
    if (in.readBoolean()) {
      boardChsName = in.readUTF();
    }
    if (in.readBoolean()) {
      categoryName = in.readUTF();
    }
    if (in.readBoolean()) {
      moderator = in.readUTF();
    }
    if (in.readBoolean()) {
      folderID = in.readUTF();
    }
    if (in.readBoolean()) {
      folderName = in.readUTF();
    }
    if (in.readBoolean()) {
      sectionID = in.readUTF();
    }
    if (in.readBoolean()) {
      sectionName = in.readUTF();
    }
    if (in.readBoolean()) {
      sectionPath = in.readUTF();
    }
  }

  @Override
  public void writeExternal(ObjectOutput out) throws IOException {
    if (boardType == null) {
      out.writeBoolean(false);
    } else {
      out.writeBoolean(true);
      out.writeShort((short) boardType.ordinal());
    }
    if (boardID == null) {
      out.writeBoolean(false);
    } else {
      out.writeBoolean(true);
      out.writeUTF(boardID);
    }
    if (boardEngName == null) {
      out.writeBoolean(false);
    } else {
      out.writeBoolean(true);
      out.writeUTF(boardEngName);
    }
    if (boardChsName == null) {
      out.writeBoolean(false);
    } else {
      out.writeBoolean(true);
      out.writeUTF(boardChsName);
    }
    if (categoryName == null) {
      out.writeBoolean(false);
    } else {
      out.writeBoolean(true);
      out.writeUTF(categoryName);
    }
    if (moderator == null) {
      out.writeBoolean(false);
    } else {
      out.writeBoolean(true);
      out.writeUTF(moderator);
    }
    if (folderID == null) {
      out.writeBoolean(false);
    } else {
      out.writeBoolean(true);
      out.writeUTF(folderID);
    }
    if (folderName == null) {
      out.writeBoolean(false);
    } else {
      out.writeBoolean(true);
      out.writeUTF(folderName);
    }
    if (sectionID == null) {
      out.writeBoolean(false);
    } else {
      out.writeBoolean(true);
      out.writeUTF(sectionID);
    }
    if (sectionName == null) {
      out.writeBoolean(false);
    } else {
      out.writeBoolean(true);
      out.writeUTF(sectionName);
    }
    if (sectionPath == null) {
      out.writeBoolean(false);
    } else {
      out.writeBoolean(true);
      out.writeUTF(sectionPath);
    }
  }



}


Question: import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.media.ExifInterface;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.franmontiel.persistentcookiejar.ClearableCookieJar;
import com.franmontiel.persistentcookiejar.PersistentCookieJar;
import com.franmontiel.persistentcookiejar.cache.SetCookieCache;
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor;
import com.zfdang.SMTHApplication;
import com.zfdang.zsmth_android.Settings;
import com.zfdang.zsmth_android.helpers.MakeList;
import com.zfdang.zsmth_android.helpers.StringUtils;
import com.zfdang.zsmth_android.models.Board;
import com.zfdang.zsmth_android.models.BoardListContent;
import com.zfdang.zsmth_android.models.BoardSection;
import com.zfdang.zsmth_android.models.Mail;
import com.zfdang.zsmth_android.models.MailListContent;
import com.zfdang.zsmth_android.models.Post;
import com.zfdang.zsmth_android.models.Topic;
import io.reactivex.Observable;
import io.reactivex.annotations.NonNull;
import io.reactivex.functions.Function;
import io.reactivex.functions.Predicate;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.Cache;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.logging.HttpLoggingInterceptor;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;
    Document doc = Jsoup.parse(response);
    Elements errors = doc.select("table.error");
    if (errors != null && errors.size() > 0) {
      Element error = errors.first();
      return error.text();
    }

    return "未识别的返回";
  }

  /*
  * All Boards related methods
  * Starts here
   */
  public static String getCacheFile(int type, String folder) {
    if (type == BOARD_TYPE_ALL) {
      return ALL_BOARD_CACHE_FILE;
    } else if (type == BOARD_TYPE_FAVORITE) {
      if (folder == null || folder.length() == 0) {
        folder = "ROOT";
      }
      return String.format("%s-%s", FAVORITE_BOARD_CACHE_PREFIX, folder);
    }
    return null;
  }

  public static List<Board> LoadBoardListFromCache(int type, String folder) {
    String filename = getCacheFile(type, folder);
    List<Board> boards = new ArrayList<>();
    try {
      Kryo kryo = new Kryo();
      Input input = new Input(SMTHApplication.getAppContext().openFileInput(filename));
      boards = kryo.readObject(input, ArrayList.class);
      input.close();
      Log.d("LoadBoardListFromCache", String.format("%d boards loaded from cache file %s", boards.size(), filename));
    } catch (Exception e) {
      Log.d("LoadBoardListFromCache", e.toString());
      Log.d("LoadBoardListFromCache", "failed to load boards from cache file " + filename);
    }
    return boards;
  }

  public static void SaveBoardListToCache(List<Board> boards, int type, String folder) {
    String filename = getCacheFile(type, folder);
    try {
      Kryo kryo = new Kryo();
      Output output = new Output(SMTHApplication.getAppContext().openFileOutput(filename, Context.MODE_PRIVATE));
      kryo.writeObject(output, boards);
      output.close();
      Log.d("SaveBoardListToCache", String.format("%d boards saved to cache file %s", boards.size(), filename));
    } catch (Exception e) {
      Log.d("SaveBoardListToCache", e.toString());
      Log.d("SaveBoardListToCache", "failed to save boards to cache file " + filename);
    }
  }

  public static void ClearBoardListCache(int type, String folder) {
    String filename = getCacheFile(type, folder);
    try {
      if (SMTHApplication.getAppContext().deleteFile(filename)) {
        Log.d("ClearBoardListCache", String.format("delete cache file %s successfully", filename));
        return;
      }
    } catch (Exception e) {
      Log.d("ClearBoardListCache", e.toString());
      Log.d("ClearBoardListCache", "Failed to delete cache file " + filename);
    }
  }

  public static List<Board> LoadFavoriteBoardsInFolder(final String path) {
    Iterable<Board> its = SMTHHelper.getInstance().wService.getFavoriteBoardsInFolder(path).flatMap(new Function<ResponseBody, Observable<Board>>() {
      @Override public Observable<Board> apply(@NonNull ResponseBody responseBody) throws Exception {
        try {
          String response = SMTHHelper.DecodeResponseFromWWW(responseBody.bytes());
          // Log.d(TAG, response);
          List<Board> boards = SMTHHelper.ParseFavoriteBoardsFromWWW(response);
          return Observable.fromIterable(boards);
        } catch (Exception e) {
          Log.e(TAG, "Failed to load favorite {" + path + "}");
          Log.e(TAG, Log.getStackTraceString(e));
          return null;
        }
      }
    }).blockingIterable();

    List<Board> results = MakeList.makeList(its);

    SaveBoardListToCache(results, BOARD_TYPE_FAVORITE, path);

    return results;
  }

  public static List<Board> LoadFavoriteBoardsInSection(final String path) {
    Iterable<Board> its = SMTHHelper.getInstance().wService.getFavoriteBoardsInSection(path).flatMap(new Function<ResponseBody, Observable<Board>>() {
      @Override public Observable<Board> apply(@NonNull ResponseBody responseBody) throws Exception {
        try {
          String response = SMTHHelper.DecodeResponseFromWWW(responseBody.bytes());
          // Log.d(TAG, response);
          List<Board> boards = SMTHHelper.ParseFavoriteBoardsFromWWW(response);
          return Observable.fromIterable(boards);
        } catch (Exception e) {
          Log.e(TAG, "Failed to load favorite {" + path + "}");
          Log.e(TAG, Log.getStackTraceString(e));
          return null;
        }
      }
    }).blockingIterable();

    List<Board> results = MakeList.makeList(its);

    SaveBoardListToCache(results, BOARD_TYPE_FAVORITE, path);

    return results;
  }

  // load all boards from WWW, recursively
  // http://stackoverflow.com/questions/31246088/how-to-do-recursive-observable-call-in-rxjava
  public static List<Board> LoadAllBoardsFromWWW() {
    final String[] SectionNames = { "社区管理", "国内院校", "休闲娱乐", "五湖四海", "游戏运动", "社会信息", "知性感性", "文化人文", "学术科学", "电脑技术", "终止版面" };
    final String[] SectionURLs = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A" };
Answer:     final List<BoardSection> sections = new ArrayList<>();