/**
* Note represents a single WordPress.com notification
*/
package kr.kdev.dg1s.biowiki.models;
import android.os.Bundle;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import kr.kdev.dg1s.biowiki.util.AppLog;
import kr.kdev.dg1s.biowiki.util.DateTimeUtils;
import kr.kdev.dg1s.biowiki.util.HtmlUtils;
import kr.kdev.dg1s.biowiki.util.JSONUtil;
public class Note {
public static final String NOTE_COMMENT_LIKE_TYPE = "comment_like";
public static final String NOTE_LIKE_TYPE = "like";
private static final String NOTE_UNKNOWN_TYPE = "unknown";
private static final String NOTE_COMMENT_TYPE = "comment";
private static final String NOTE_MATCHER_TYPE = "automattcher";
// Notes have different types of "templates" for displaying differently
// this is not a canonical list but covers all the types currently in use
private static final String SINGLE_LINE_LIST_TEMPLATE = "single-line-list";
private static final String MULTI_LINE_LIST_TEMPLATE = "multi-line-list";
private static final String BIG_BADGE_TEMPLATE = "big-badge";
// JSON action keys
private static final String ACTION_KEY_REPLY = "replyto-comment";
private static final String ACTION_KEY_APPROVE = "approve-comment";
private static final String ACTION_KEY_UNAPPROVE = "unapprove-comment";
private static final String ACTION_KEY_SPAM = "spam-comment";
// TODO: add other types
private static final Map<String, String> pnType2type = new Hashtable<String, String>() {{
put("c", "comment");
}};
private final JSONObject mNoteJSON;
private Map<String, JSONObject> mActions;
private SpannableStringBuilder mComment = new SpannableStringBuilder();
private boolean mPlaceholder = false;
private int mBlogId;
private int mPostId;
private long mCommentId;
private long mCommentParentId;
private transient String mCommentPreview;
private transient String mSubject;
private transient String mIconUrl;
private transient String mTimestamp;
private transient String mSnippet;
/**
* Create a note using JSON from REST API
*/
public Note(JSONObject noteJSON) {
mNoteJSON = noteJSON;
preloadContent();
}
/**
* Create a placeholder note from a Push Notification payload
*/
public Note(Bundle extras) {
JSONObject tmpNoteJSON = new JSONObject();
String type = extras.getString("type");
String finalType = NOTE_UNKNOWN_TYPE;
if (type != null && pnType2type.containsKey(type)) {
finalType = pnType2type.get(type);
}
JSONObject subject = new JSONObject();
JSONObject body = new JSONObject();
JSONObject html = new JSONObject();
JSONArray items = new JSONArray();
try {
// subject
if (finalType.equals(NOTE_COMMENT_TYPE)) {
subject.put("text", extras.get("title"));
} else {
subject.put("text", extras.get("msg"));
}
subject.put("icon", extras.get("icon"));
subject.put("noticon", extras.get("noticon"));
html.put("html", extras.get("msg"));
items.put(html);
body.put("items", items);
// fake timestamp to put it in top of the list
String timestamp = extras.getString("note_timestamp");
if (timestamp == null || timestamp.equals("")) {
timestamp = "" + (System.currentTimeMillis() / 1000);
}
tmpNoteJSON.put("timestamp", timestamp);
// root
tmpNoteJSON.put("id", extras.get("note_id"));
tmpNoteJSON.put("subject", subject);
tmpNoteJSON.put("body", body);
tmpNoteJSON.put("type", finalType);
tmpNoteJSON.put("unread", "1");
} catch (JSONException e) {
AppLog.e(AppLog.T.NOTIFS, "Failed to put key in noteJSON", e);
}
mNoteJSON = tmpNoteJSON;
}
public boolean isPlaceholder() {
return mPlaceholder;
}
public void setPlaceholder(boolean placeholder) {
this.mPlaceholder = placeholder;
}
public JSONObject toJSONObject() {
return mNoteJSON;
}
public String getId() {
return queryJSON("id", "0");
}
public String getType() {
return queryJSON("type", NOTE_UNKNOWN_TYPE);
}
private Boolean isType(String type) {
return getType().equals(type);
}
public Boolean isCommentType() {
return isType(NOTE_COMMENT_TYPE);
}
public Boolean isCommentLikeType() {
return isType(NOTE_COMMENT_LIKE_TYPE);
}
public Boolean isAutomattcherType() {
return isType(NOTE_MATCHER_TYPE);
}
public String getSubject() {
if (mSubject == null) {
String text = queryJSON("subject.text", "").trim();
if (text.equals("")) {
text = queryJSON("subject.html", "");
}
mSubject = Html.fromHtml(text).toString();
}
return mSubject;
}
public String getIconURL() {
if (mIconUrl == null)
mIconUrl = queryJSON("subject.icon", "");
return mIconUrl;
}
/**
* Removes HTML and cleans up newlines and whitespace
*/
public String getCommentPreview() {
if (mCommentPreview == null)
mCommentPreview = getCommentBody().toString().replaceAll("\uFFFC", "").replace("\n", " ").replaceAll("[\\s]{2,}", " ").trim();
return mCommentPreview;
}
/**
* Gets the comment's text with getCommentText() and sends it through HTML.fromHTML
*/
Spanned getCommentBody() {
return mComment;
}
/**
* For a comment note the text is in the body object's last item. It currently
* is only provided in HTML format.
*/
String getCommentText() {
return queryJSON("body.items[last].html", "");
}
/**
* The inverse of isRead
*/
public Boolean isUnread() {
return !isRead();
}
/**
* A note can have an "unread" of 0 or more ("likes" can have unread of 2+) to indicate the
* quantity of likes that are "unread" within the single note. So for a note to be "read" it
* should have "0"
*/
Boolean isRead() {
return getUnreadCount().equals("0");
}
/**
* For some reason the unread count is a string in the JSON API but is truly represented
* by an Integer. We can handle a simple string.
*/
public String getUnreadCount() {
return queryJSON("unread", "0");
}
/**
*
*/
public void setUnreadCount(String count) {
try {
mNoteJSON.putOpt("unread", count);
} catch (JSONException e) {
AppLog.e(AppLog.T.NOTIFS, "Failed to set unread property", e);
}
}
public Reply buildReply(String content) {
JSONObject replyAction = getActions().get(ACTION_KEY_REPLY);
String restPath = JSONUtil.queryJSON(replyAction, "params.rest_path", "");
AppLog.d(AppLog.T.NOTIFS, String.format("Search actions %s", restPath));
return new Reply(this, String.format("%s/replies/new", restPath), content);
}
/**
* Get the timestamp provided by the API for the note - cached for performance
*/
public String getTimestamp() {
if (mTimestamp == null)
mTimestamp = queryJSON("timestamp", "");
return mTimestamp;
}
/*
* returns a string representing the timespan based on the note's timestamp - used for display
* in the notification list (ex: "3d")
*/
public String getTimeSpan() {
try {
return DateTimeUtils.timestampToTimeSpan(Long.valueOf(getTimestamp()));
} catch (NumberFormatException e) {
AppLog.e(AppLog.T.NOTIFS, "failed to convert timestamp to long", e);
return "";
}
}
String getTemplate() {
return queryJSON("body.template", "");
}
public Boolean isMultiLineListTemplate() {
return getTemplate().equals(MULTI_LINE_LIST_TEMPLATE);
}
public Boolean isSingleLineListTemplate() {
return getTemplate().equals(SINGLE_LINE_LIST_TEMPLATE);
}
public Boolean isBigBadgeTemplate() {
return getTemplate().equals(BIG_BADGE_TEMPLATE);
}
Map<String, JSONObject> getActions() {
if (mActions == null) {
try {
JSONArray actions = queryJSON("body.actions", new JSONArray());
mActions = new HashMap<String, JSONObject>(actions.length());
for (int i = 0; i < actions.length(); i++) {
JSONObject action = actions.getJSONObject(i);
String actionType = JSONUtil.queryJSON(action, "type", "");
if (!actionType.equals("")) {
mActions.put(actionType, action);
}
}
} catch (JSONException e) {
AppLog.e(AppLog.T.NOTIFS, "Could not find actions", e);
mActions = new HashMap<String, JSONObject>();
}
}
return mActions;
}
/*
* returns the "meta" section of the note's JSON (not guaranteed to exist)
*/
private JSONObject getJSONMeta() {
return JSONUtil.getJSONChild(this.toJSONObject(), "meta");
}
/*
* returns the value of the passed name in the meta section of the JSON
*/
public int getMetaValueAsInt(String name, int defaultValue) {
JSONObject jsonMeta = getJSONMeta();
if (jsonMeta == null)
return defaultValue;
return jsonMeta.optInt(name, defaultValue);
}
/*
* returns the actions allowed on this note, assumes it's a comment notification
*/
public EnumSet<EnabledActions> getEnabledActions() {
EnumSet<EnabledActions> actions = EnumSet.noneOf(EnabledActions.class);
Map<String, JSONObject> jsonActions = getActions();
if (jsonActions == null || jsonActions.size() == 0)
return actions;
if (jsonActions.containsKey(ACTION_KEY_REPLY))
actions.add(EnabledActions.ACTION_REPLY);
if (jsonActions.containsKey(ACTION_KEY_APPROVE))
actions.add(EnabledActions.ACTION_APPROVE);
if (jsonActions.containsKey(ACTION_KEY_UNAPPROVE))
actions.add(EnabledActions.ACTION_UNAPPROVE);
if (jsonActions.containsKey(ACTION_KEY_SPAM))
actions.add(EnabledActions.ACTION_SPAM);
return actions;
}
/**
* pre-loads commonly-accessed fields - avoids performance hit of loading these
* fields inside an adapter's getView()
*/
void preloadContent() {
if (isCommentType()) {
// pre-load the comment HTML for being displayed. Cleans up emoticons.
mComment = HtmlUtils.fromHtml(getCommentText());
// pre-load the preview text
getCommentPreview();
}
// pre-load the subject and avatar url
getSubject();
getIconURL();
// pre-load site/post/comment IDs
preloadMetaIds();
}
/*
* nbradbury - preload the blog, post, & comment IDs from the meta section
* ids={"site":61509427,"self":993925505,"post":161,"comment":178,"comment_parent":0}
*/
private void preloadMetaIds() {
JSONObject jsonMeta = getJSONMeta();
if (jsonMeta == null)
return;
JSONObject jsonIDs = jsonMeta.optJSONObject("ids");
if (jsonIDs == null)
return;
mBlogId = jsonIDs.optInt("site");
mPostId = jsonIDs.optInt("post");
mCommentId = jsonIDs.optLong("comment");
mCommentParentId = jsonIDs.optLong("comment_parent");
}
public int getBlogId() {
return mBlogId;
}
public int getPostId() {
return mPostId;
}
public long getCommentId() {
return mCommentId;
}
public long getCommentParentId() {
return mCommentParentId;
}
/*
* plain-text snippet returned by the server - currently shown only for comments
*/
String getSnippet() {
if (mSnippet == null) {
mSnippet = queryJSON("snippet", "");
}
return mSnippet;
}
public boolean hasSnippet() {
return !TextUtils.isEmpty(getSnippet());
}
/**
* Rudimentary system for pulling an item out of a JSON object hierarchy
*/
public <U> U queryJSON(String query, U defaultObject) {
return JSONUtil.queryJSON(this.toJSONObject(), query, defaultObject);
}
public static enum EnabledActions {
ACTION_REPLY,
ACTION_APPROVE,
ACTION_UNAPPROVE,
ACTION_SPAM
}
public static class TimeStampComparator implements Comparator<Note> {
@Override
public int compare(Note a, Note b) {
return b.getTimestamp().compareTo(a.getTimestamp());
}
}
/**
* Represents a user replying to a note.
*/
public static class Reply {
private final Note mNote;
private final String mContent;
private final String mRestPath;
Reply(Note note, String restPath, String content) {
mNote = note;
mRestPath = restPath;
mContent = content;
}
public String getContent() {
return mContent;
}
public String getRestPath() {
return mRestPath;
}
}
/*
* returns subject as spanned html with user names and quoted strings highlighted
*/
/*private transient Spanned mFormattedSubject;
private static final String TAG_START = "</font><font color='#222222'>";
private static final String TAG_END = "</font><font color='#666666'>";
public Spanned getFormattedSubject() {
if (mFormattedSubject == null) {
StringBuilder sb = new StringBuilder(getSubject());
// highlight user names - note we skip the first item in replies because it's the
// actual header text (ex: "In reply to your comment") rather than a user name
if (isMultiLineListTemplate() || isSingleLineListTemplate()) {
JSONArray items = queryJSON("body.items", new JSONArray());
int startIndex = (isCommentType() ? 1 : 0);
if (items.length() > startIndex) {
for (int i = startIndex; i < items.length(); i++) {
try {
String name = JSONUtil.getString((JSONObject) items.get(i), "header_text");
if (!TextUtils.isEmpty(name)) {
// note that we only replace the first instance to avoid false matches
int index = sb.indexOf(name);
if (index > -1) {
sb.replace(index, index + name.length(), TAG_START + name + TAG_END);
}
}
} catch (JSONException e) {
// nop
}
}
}
}
// highlight quoted strings
int startQuote = sb.indexOf("\"");
while (startQuote > -1) {
int endQuote = sb.indexOf("\"", startQuote + 1);
if (endQuote == -1)
break;
String quoted = sb.substring(startQuote, endQuote + 1);
sb.replace(startQuote, endQuote + 1, TAG_START + quoted + TAG_END);
startQuote = sb.indexOf("\"", endQuote + TAG_START.length() + TAG_END.length());
}
mFormattedSubject = Html.fromHtml(sb.toString());
}
return mFormattedSubject;
}*/
}