io.github.hidroh.materialistic.data.HackerNewsItem.java Source code

Java tutorial

Introduction

Here is the source code for io.github.hidroh.materialistic.data.HackerNewsItem.java

Source

/*
 * Copyright (c) 2016 Ha Duy Trung
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.github.hidroh.materialistic.data;

import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Parcel;
import android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.ContextCompat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.view.View;

import io.github.hidroh.materialistic.AppUtils;
import io.github.hidroh.materialistic.Navigable;
import io.github.hidroh.materialistic.R;
import io.github.hidroh.materialistic.annotation.Synthetic;

class HackerNewsItem implements Item {
    private static final String AUTHOR_SEPARATOR = " - ";

    // The item's unique id. Required.
    @Keep
    private long id;
    // true if the item is deleted.
    @Keep
    private boolean deleted;
    // The type of item. One of "job", "story", "comment", "poll", or "pollopt".
    @Keep
    private String type;
    // The username of the item's author.
    @Keep
    private String by;
    // Creation date of the item, in Unix Time.
    @Keep
    private long time;
    // The comment, Ask HN, or poll text. HTML.
    @Keep
    private String text;
    // true if the item is dead.
    @Keep
    private boolean dead;
    // The item's parent. For comments, either another comment or the relevant story. For pollopts, the relevant poll.
    @Keep
    private long parent;
    // The ids of the item's comments, in ranked display order.
    @Keep
    private long[] kids;
    // The URL of the story.
    @Keep
    private String url;
    // The story's score, or the votes for a pollopt.
    @Keep
    private int score;
    // The title of the story or poll.
    @Keep
    private String title;
    // A list of related pollopts, in display order.
    @SuppressWarnings("unused")
    @Keep
    private long[] parts;
    // In the case of stories or polls, the total comment count.
    @Keep
    private int descendants = -1;

    // view state
    private boolean favorite;
    private boolean viewed;
    private int localRevision = -1;
    @VisibleForTesting
    int level = 0;
    private boolean collapsed;
    private boolean contentExpanded;
    int rank;
    private int lastKidCount = -1;
    private boolean hasNewDescendants = false;
    private boolean voted;
    private boolean pendingVoted;
    private long next, previous;

    // non parcelable fields
    private HackerNewsItem[] kidItems;
    private HackerNewsItem parentItem;
    private Spannable displayedTime;
    private Spannable displayedAuthor;
    private CharSequence displayedText;
    private int defaultColor;

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

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

    HackerNewsItem(long id) {
        this.id = id;
    }

    private HackerNewsItem(long id, int level) {
        this(id);
        this.level = level;
    }

    @Synthetic
    HackerNewsItem(Parcel source) {
        id = source.readLong();
        title = source.readString();
        time = source.readLong();
        by = source.readString();
        kids = source.createLongArray();
        url = source.readString();
        text = source.readString();
        type = source.readString();
        favorite = source.readInt() != 0;
        descendants = source.readInt();
        score = source.readInt();
        favorite = source.readInt() == 1;
        viewed = source.readInt() == 1;
        localRevision = source.readInt();
        level = source.readInt();
        dead = source.readInt() == 1;
        deleted = source.readInt() == 1;
        collapsed = source.readInt() == 1;
        contentExpanded = source.readInt() == 1;
        rank = source.readInt();
        lastKidCount = source.readInt();
        hasNewDescendants = source.readInt() == 1;
        parent = source.readLong();
        voted = source.readInt() == 1;
        pendingVoted = source.readInt() == 1;
        next = source.readLong();
        previous = source.readLong();
    }

    @Override
    public void populate(Item info) {
        title = info.getTitle();
        time = info.getTime();
        by = info.getBy();
        kids = info.getKids();
        url = info.getRawUrl();
        text = info.getText();
        displayedText = info.getDisplayedText(); // pre-load, but not part of Parcelable
        type = info.getRawType();
        descendants = info.getDescendants();
        hasNewDescendants = lastKidCount >= 0 && descendants > lastKidCount;
        lastKidCount = descendants;
        parent = Long.parseLong(info.getParent());
        deleted = info.isDeleted();
        dead = info.isDead();
        score = info.getScore();
        viewed = info.isViewed();
        favorite = info.isFavorite();
        localRevision = 1;
    }

    @Override
    public String getRawType() {
        return type;
    }

    @Override
    public String getRawUrl() {
        return url;
    }

    @Override
    public long[] getKids() {
        return kids;
    }

    @Override
    public String getBy() {
        return by;
    }

    @Override
    public long getTime() {
        return time;
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(id);
        dest.writeString(title);
        dest.writeLong(time);
        dest.writeString(by);
        dest.writeLongArray(kids);
        dest.writeString(url);
        dest.writeString(text);
        dest.writeString(type);
        dest.writeInt(favorite ? 1 : 0);
        dest.writeInt(descendants);
        dest.writeInt(score);
        dest.writeInt(favorite ? 1 : 0);
        dest.writeInt(viewed ? 1 : 0);
        dest.writeInt(localRevision);
        dest.writeInt(level);
        dest.writeInt(dead ? 1 : 0);
        dest.writeInt(deleted ? 1 : 0);
        dest.writeInt(collapsed ? 1 : 0);
        dest.writeInt(contentExpanded ? 1 : 0);
        dest.writeInt(rank);
        dest.writeInt(lastKidCount);
        dest.writeInt(hasNewDescendants ? 1 : 0);
        dest.writeLong(parent);
        dest.writeInt(voted ? 1 : 0);
        dest.writeInt(pendingVoted ? 1 : 0);
        dest.writeLong(next);
        dest.writeLong(previous);
    }

    @Override
    public String getId() {
        return String.valueOf(id);
    }

    @Override
    public long getLongId() {
        return id;
    }

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

    @Override
    public String getDisplayedTitle() {
        switch (getType()) {
        case COMMENT_TYPE:
            return text;
        case JOB_TYPE:
        case STORY_TYPE:
        case POLL_TYPE: // TODO poll need to display options
        default:
            return title;
        }
    }

    @NonNull
    @Override
    public String getType() {
        return !TextUtils.isEmpty(type) ? type : STORY_TYPE;
    }

    @Override
    public Spannable getDisplayedAuthor(Context context, boolean linkify, int color) {
        if (displayedAuthor == null) {
            if (TextUtils.isEmpty(by)) {
                displayedAuthor = new SpannableString("");
            } else {
                defaultColor = ContextCompat.getColor(context, AppUtils.getThemedResId(context,
                        linkify ? android.R.attr.textColorLink : android.R.attr.textColorSecondary));
                displayedAuthor = createAuthorSpannable(linkify);
            }
        }
        if (displayedAuthor.length() == 0) {
            return displayedAuthor;
        }
        displayedAuthor.setSpan(new ForegroundColorSpan(color != 0 ? color : defaultColor),
                AUTHOR_SEPARATOR.length(), displayedAuthor.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        return displayedAuthor;
    }

    @Override
    public Spannable getDisplayedTime(Context context) {
        if (displayedTime == null) {
            SpannableStringBuilder builder = new SpannableStringBuilder(
                    dead ? context.getString(R.string.dead_prefix) + " " : "");
            SpannableString timeSpannable = new SpannableString(AppUtils.getAbbreviatedTimeSpan(time * 1000));
            if (deleted) {
                timeSpannable.setSpan(new StrikethroughSpan(), 0, timeSpannable.length(),
                        Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            }
            builder.append(timeSpannable);
            displayedTime = builder;
        }
        return displayedTime;
    }

    @Override
    public int getKidCount() {
        if (descendants > 0) {
            return descendants;
        }

        return kids != null ? kids.length : 0;
    }

    @Override
    public int getLastKidCount() {
        return lastKidCount;
    }

    @Override
    public void setLastKidCount(int lastKidCount) {
        this.lastKidCount = lastKidCount;
    }

    @Override
    public boolean hasNewKids() {
        return hasNewDescendants;
    }

    @Override
    public String getUrl() {
        switch (getType()) {
        case JOB_TYPE:
        case POLL_TYPE:
        case COMMENT_TYPE:
            return getItemUrl(getId());
        default:
            return TextUtils.isEmpty(url) ? getItemUrl(getId()) : url;
        }
    }

    @NonNull
    private SpannableString createAuthorSpannable(boolean authorLink) {
        SpannableString bySpannable = new SpannableString(AUTHOR_SEPARATOR + by);
        if (!authorLink) {
            return bySpannable;
        }
        bySpannable.setSpan(new StyleSpan(Typeface.BOLD), AUTHOR_SEPARATOR.length(), bySpannable.length(),
                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        ClickableSpan clickableSpan = new ClickableSpan() {
            @Override
            public void onClick(View view) {
                view.getContext()
                        .startActivity(new Intent(Intent.ACTION_VIEW).setData(AppUtils.createUserUri(getBy())));
            }

            @Override
            public void updateDrawState(TextPaint ds) {
                super.updateDrawState(ds);
                ds.setUnderlineText(false);
            }
        };
        bySpannable.setSpan(clickableSpan, AUTHOR_SEPARATOR.length(), bySpannable.length(),
                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        return bySpannable;
    }

    private String getItemUrl(String itemId) {
        return String.format(HackerNewsClient.WEB_ITEM_PATH, itemId);
    }

    @Override
    public String getSource() {
        return TextUtils.isEmpty(getUrl()) ? null : Uri.parse(getUrl()).getHost();
    }

    @Override
    public HackerNewsItem[] getKidItems() {
        if (kids == null || kids.length == 0) {
            return new HackerNewsItem[0];
        }

        if (kidItems == null) {
            kidItems = new HackerNewsItem[kids.length];
            for (int i = 0; i < kids.length; i++) {
                HackerNewsItem item = new HackerNewsItem(kids[i], level + 1);
                item.rank = i + 1;
                if (i > 0) {
                    item.previous = kids[i - 1];
                }
                if (i < kids.length - 1) {
                    item.next = kids[i + 1];
                }
                kidItems[i] = item;
            }
        }

        return kidItems;
    }

    @Override
    public String getText() {
        return text;
    }

    @Override
    public CharSequence getDisplayedText() {
        if (displayedText == null) {
            displayedText = AppUtils.fromHtml(text);
        }
        return displayedText;
    }

    @Override
    public boolean isStoryType() {
        switch (getType()) {
        case STORY_TYPE:
        case POLL_TYPE:
        case JOB_TYPE:
            return true;
        case COMMENT_TYPE:
        default:
            return false;
        }
    }

    @Override
    public boolean isFavorite() {
        return favorite;
    }

    @Override
    public void setFavorite(boolean favorite) {
        this.favorite = favorite;
    }

    @Override
    public int getLocalRevision() {
        return localRevision;
    }

    @Override
    public void setLocalRevision(int localRevision) {
        this.localRevision = localRevision;
    }

    @Override
    public int getDescendants() {
        return descendants;
    }

    @Override
    public boolean isViewed() {
        return viewed;
    }

    @Override
    public void setIsViewed(boolean isViewed) {
        viewed = isViewed;
    }

    @Override
    public int getLevel() {
        return level;
    }

    @Override
    public String getParent() {
        return String.valueOf(parent);
    }

    @Override
    public Item getParentItem() {
        if (parent == 0) {
            return null;
        }
        if (parentItem == null) {
            parentItem = new HackerNewsItem(parent);
        }
        return parentItem;
    }

    @Override
    public boolean isDeleted() {
        return deleted;
    }

    @Override
    public boolean isDead() {
        return dead;
    }

    @Override
    public int getScore() {
        return score;
    }

    @Override
    public void incrementScore() {
        score++;
        voted = true;
        pendingVoted = true;
    }

    @Override
    public boolean isVoted() {
        return voted;
    }

    @Override
    public boolean isPendingVoted() {
        return pendingVoted;
    }

    @Override
    public void clearPendingVoted() {
        pendingVoted = false;
    }

    @Override
    public boolean isCollapsed() {
        return collapsed;
    }

    @Override
    public void setCollapsed(boolean collapsed) {
        this.collapsed = collapsed;
    }

    @Override
    public int getRank() {
        return rank;
    }

    @Override
    public boolean isContentExpanded() {
        return contentExpanded;
    }

    @Override
    public void setContentExpanded(boolean expanded) {
        contentExpanded = expanded;
    }

    @Override
    public long getNeighbour(int direction) {
        switch (direction) {
        case Navigable.DIRECTION_UP:
            return previous;
        case Navigable.DIRECTION_DOWN:
            return next;
        case Navigable.DIRECTION_LEFT:
            return level > 1 ? parent : 0L;
        case Navigable.DIRECTION_RIGHT:
            return kids != null && kids.length > 0 ? kids[0] : 0L;
        default:
            return 0L;
        }
    }

    @Override
    public int hashCode() {
        return (int) id;
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof HackerNewsItem && id == ((HackerNewsItem) o).id;
    }

    void preload() {
        getDisplayedText(); // pre-load HTML
        getKidItems(); // pre-construct kids
    }
}