/* * 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 } }