aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/models
diff options
context:
space:
mode:
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/models')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Account.java111
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/AccountHelper.java51
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/AccountModel.java248
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Blog.java576
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/BlogIdentifier.java52
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/BlogPairId.java25
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Capability.java21
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/CategoryModel.java65
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/CategoryNode.java110
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Comment.java244
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/CommentList.java107
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/CommentStatus.java83
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/FeatureSet.java38
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/FilterCriteria.java5
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/MediaUploadState.java15
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Note.java590
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java115
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java24
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Person.java174
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Post.java505
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/PostLocation.java93
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/PostStatus.java70
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/PostsListPost.java183
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/PostsListPostList.java60
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderBlog.java169
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderBlogList.java87
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderComment.java138
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderCommentList.java115
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java718
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderPostDiscoverData.java187
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderPostList.java90
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendBlogList.java54
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendedBlog.java79
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java214
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderTagList.java69
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java45
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderUrlList.java36
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderUser.java121
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderUserIdList.java14
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/ReaderUserList.java44
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Role.java102
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/SiteSettingsModel.java418
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Suggestion.java71
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Tag.java51
-rw-r--r--WordPress/src/main/java/org/wordpress/android/models/Theme.java183
45 files changed, 6570 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Account.java b/WordPress/src/main/java/org/wordpress/android/models/Account.java
new file mode 100644
index 000000000..80b414b65
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Account.java
@@ -0,0 +1,111 @@
+package org.wordpress.android.models;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.AccountTable;
+import org.wordpress.android.datasets.ReaderUserTable;
+import org.wordpress.android.ui.prefs.PrefsEvents;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * Class for managing logged in user informations.
+ */
+public class Account extends AccountModel {
+ public void fetchAccountDetails() {
+ if (!hasAccessToken()) {
+ AppLog.e(T.API, "User is not logged in with WordPress.com, ignoring the fetch account details request");
+ return;
+ }
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null) {
+ updateFromRestResponse(jsonObject);
+ save();
+
+ ReaderUserTable.addOrUpdateUser(ReaderUser.fromJson(jsonObject));
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_1().get("me", listener, errorListener);
+ }
+
+ public void fetchAccountSettings() {
+ if (!hasAccessToken()) {
+ AppLog.e(T.API, "User is not logged in with WordPress.com, ignoring the fetch account settings request");
+ return;
+ }
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null) {
+ updateAccountSettingsFromRestResponse(jsonObject);
+ save();
+ EventBus.getDefault().post(new PrefsEvents.AccountSettingsFetchSuccess());
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ EventBus.getDefault().post(new PrefsEvents.AccountSettingsFetchError(volleyError));
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_1().get("me/settings", listener, errorListener);
+ }
+
+ public void postAccountSettings(Map<String, String> params) {
+ if (!hasAccessToken()) {
+ AppLog.e(T.API, "User is not logged in with WordPress.com, ignoring the post account settings request");
+ return;
+ }
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null) {
+ updateAccountSettingsFromRestResponse(jsonObject);
+ save();
+ EventBus.getDefault().post(new PrefsEvents.AccountSettingsPostSuccess());
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.API, volleyError);
+ EventBus.getDefault().post(new PrefsEvents.AccountSettingsPostError(volleyError));
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_1().post("me/settings", params, null, listener, errorListener);
+ }
+
+ public void signout() {
+ init();
+ save();
+ }
+
+ public void save() {
+ AccountTable.save(this);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/AccountHelper.java b/WordPress/src/main/java/org/wordpress/android/models/AccountHelper.java
new file mode 100644
index 000000000..f4593931c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/AccountHelper.java
@@ -0,0 +1,51 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.AccountTable;
+
+/**
+ * The app supports only one WordPress.com account at the moment, so we might use getDefaultAccount() everywhere we
+ * need the account data.
+ */
+public class AccountHelper {
+ private static Account sAccount;
+ private final static Object mLock = new Object();
+
+ public static Account getDefaultAccount() {
+ if (sAccount == null) {
+ // Singleton pattern in concurrent env.
+ synchronized(mLock) {
+ if (sAccount == null) {
+ sAccount = AccountTable.getDefaultAccount();
+ if (sAccount == null) {
+ sAccount = new Account();
+ }
+ }
+ }
+ }
+ return sAccount;
+ }
+
+ public static boolean isSignedIn() {
+ return getDefaultAccount().hasAccessToken() || (WordPress.wpDB.getNumVisibleBlogs() != 0);
+ }
+
+ public static boolean isSignedInWordPressDotCom() {
+ return getDefaultAccount().hasAccessToken();
+ }
+
+ public static boolean isJetPackUser() {
+ return WordPress.wpDB.hasAnyJetpackBlogs();
+ }
+
+ public static String getCurrentUsernameForBlog(Blog blog) {
+ if (!TextUtils.isEmpty(getDefaultAccount().getUserName())) {
+ return getDefaultAccount().getUserName();
+ } else if (blog != null) {
+ return blog.getUsername();
+ }
+ return "";
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/AccountModel.java b/WordPress/src/main/java/org/wordpress/android/models/AccountModel.java
new file mode 100644
index 000000000..93f3400ef
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/AccountModel.java
@@ -0,0 +1,248 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.Date;
+
+public class AccountModel {
+ // WordPress.com only - data fetched from the REST API endpoint
+ private String mUserName;
+ private long mUserId;
+ private String mDisplayName;
+ private String mProfileUrl;
+ private String mAvatarUrl;
+ private long mPrimaryBlogId;
+ private int mSiteCount;
+ private int mVisibleSiteCount;
+ private String mAccessToken;
+ private String mEmail;
+ private String mFirstName;
+ private String mLastName;
+ private String mAboutMe;
+ private Date mDateCreated;
+ private String mNewEmail;
+ private boolean mPendingEmailChange;
+ private String mWebAddress;
+
+ public AccountModel() {
+ init();
+ }
+
+ public void init() {
+ mUserName = "";
+ mUserId = 0;
+ mDisplayName = "";
+ mProfileUrl = "";
+ mAvatarUrl = "";
+ mPrimaryBlogId = 0;
+ mSiteCount = 0;
+ mVisibleSiteCount = 0;
+ mAccessToken = "";
+ mEmail = "";
+ mFirstName = "";
+ mLastName = "";
+ mAboutMe = "";
+ mDateCreated = new Date();
+ mNewEmail = "";
+ mPendingEmailChange = false;
+ mWebAddress = "";
+ }
+
+ public void updateFromRestResponse(JSONObject json) {
+ mUserId = json.optLong("ID");
+ mUserName = json.optString("username");
+ mDisplayName = json.optString("display_name");
+ mProfileUrl = json.optString("profile_URL");
+ mAvatarUrl = json.optString("avatar_URL");
+ mPrimaryBlogId = json.optLong("primary_blog");
+ mSiteCount = json.optInt("site_count");
+ mVisibleSiteCount = json.optInt("visible_site_count");
+ mEmail = json.optString("email");
+
+ Date date = DateTimeUtils.dateFromIso8601(json.optString("date"));
+ if (date != null) {
+ mDateCreated = date;
+ } else {
+ AppLog.e(AppLog.T.API, "Date could not be found from Account JSON response");
+ }
+ }
+
+ public void updateAccountSettingsFromRestResponse(JSONObject json) {
+ if (json.has(RestParam.FIRST_NAME.getDescription())) mFirstName = json.optString(RestParam.FIRST_NAME.getDescription());
+ if (json.has(RestParam.LAST_NAME.getDescription())) mLastName = json.optString(RestParam.LAST_NAME.getDescription());
+ if (json.has(RestParam.DISPLAY_NAME.getDescription())) mDisplayName = json.optString(RestParam.DISPLAY_NAME.getDescription());
+ if (json.has(RestParam.ABOUT_ME.getDescription())) mAboutMe = json.optString(RestParam.ABOUT_ME.getDescription());
+ if (json.has(RestParam.EMAIL.getDescription())) mEmail = json.optString(RestParam.EMAIL.getDescription());
+ if (json.has(RestParam.NEW_EMAIL.getDescription())) mNewEmail = json.optString(RestParam.NEW_EMAIL.getDescription());
+ if (json.has(RestParam.EMAIL_CHANGE_PENDING.getDescription())) mPendingEmailChange = json.optBoolean(RestParam.EMAIL_CHANGE_PENDING.getDescription());
+ if (json.has(RestParam.PRIMARY_BLOG.getDescription())) mPrimaryBlogId = json.optLong(RestParam.PRIMARY_BLOG.getDescription());
+ if (json.has(RestParam.WEB_ADDRESS.getDescription())) mWebAddress = json.optString(RestParam.WEB_ADDRESS.getDescription());
+ }
+
+ public long getUserId() {
+ return mUserId;
+ }
+
+ public void setUserId(long userId) {
+ mUserId = userId;
+ }
+
+ public void setPrimaryBlogId(long primaryBlogId) {
+ mPrimaryBlogId = primaryBlogId;
+ }
+
+ public long getPrimaryBlogId() {
+ return mPrimaryBlogId;
+ }
+
+ public String getUserName() {
+ return StringUtils.notNullStr(mUserName);
+ }
+
+ public void setUserName(String userName) {
+ mUserName = userName;
+ }
+
+ public String getAccessToken() {
+ return mAccessToken;
+ }
+
+ public void setAccessToken(String accessToken) {
+ mAccessToken = accessToken;
+ }
+
+ boolean hasAccessToken() {
+ return !TextUtils.isEmpty(getAccessToken());
+ }
+
+ public String getDisplayName() {
+ return StringUtils.notNullStr(mDisplayName);
+ }
+
+ public void setDisplayName(String displayName) {
+ mDisplayName = displayName;
+ }
+
+ public String getProfileUrl() {
+ return StringUtils.notNullStr(mProfileUrl);
+ }
+
+ public void setProfileUrl(String profileUrl) {
+ mProfileUrl = profileUrl;
+ }
+
+ public String getAvatarUrl() {
+ return StringUtils.notNullStr(mAvatarUrl);
+ }
+
+ public void setAvatarUrl(String avatarUrl) {
+ mAvatarUrl = avatarUrl;
+ }
+
+ public int getSiteCount() {
+ return mSiteCount;
+ }
+
+ public void setSiteCount(int siteCount) {
+ mSiteCount = siteCount;
+ }
+
+ public int getVisibleSiteCount() {
+ return mVisibleSiteCount;
+ }
+
+ public void setVisibleSiteCount(int visibleSiteCount) {
+ mVisibleSiteCount = visibleSiteCount;
+ }
+
+ public void setEmail(String email) {
+ mEmail = email;
+ }
+
+ public String getEmail() {
+ return StringUtils.notNullStr(mEmail);
+ }
+
+ public String getFirstName() {
+ return StringUtils.notNullStr(mFirstName);
+ }
+
+ public void setFirstName(String firstName) {
+ mFirstName = firstName;
+ }
+
+ public String getLastName() {
+ return StringUtils.notNullStr(mLastName);
+ }
+
+ public void setLastName(String lastName) {
+ mLastName = lastName;
+ }
+
+ public String getAboutMe() {
+ return StringUtils.notNullStr(mAboutMe);
+ }
+
+ public void setAboutMe(String aboutMe) {
+ mAboutMe = aboutMe;
+ }
+
+ public Date getDateCreated() {
+ return mDateCreated;
+ }
+
+ public void setDateCreated(Date date) {
+ mDateCreated = date;
+ }
+
+ public String getNewEmail() {
+ return StringUtils.notNullStr(mNewEmail);
+ }
+
+ public void setNewEmail(String newEmail) {
+ mNewEmail = newEmail;
+ }
+
+ public boolean getPendingEmailChange() {
+ return mPendingEmailChange;
+ }
+
+ public void setPendingEmailChange(boolean pendingEmailChange) {
+ mPendingEmailChange = pendingEmailChange;
+ }
+
+ public String getWebAddress() {
+ return mWebAddress;
+ }
+
+ public void setWebAddress(String webAddress) {
+ mWebAddress = webAddress;
+ }
+
+ public enum RestParam {
+ FIRST_NAME("first_name"),
+ LAST_NAME("last_name"),
+ DISPLAY_NAME("display_name"),
+ ABOUT_ME("description"),
+ EMAIL("user_email"),
+ NEW_EMAIL("new_user_email"),
+ EMAIL_CHANGE_PENDING("user_email_change_pending"),
+ PRIMARY_BLOG("primary_site_ID"),
+ WEB_ADDRESS("user_URL");
+
+ private String description;
+
+ RestParam(String description) {
+ this.description = description;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Blog.java b/WordPress/src/main/java/org/wordpress/android/models/Blog.java
new file mode 100644
index 000000000..31daba99d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Blog.java
@@ -0,0 +1,576 @@
+//Manages data for blog settings
+
+package org.wordpress.android.models;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.StringUtils;
+
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+
+public class Blog {
+ private int localTableBlogId;
+ private String url;
+ private String homeURL;
+ private String blogName;
+ private String username;
+ private String password;
+ private String imagePlacement;
+ private boolean featuredImageCapable;
+ private boolean fullSizeImage;
+ private boolean scaledImage;
+ private int scaledImageWidth;
+ private String maxImageWidth;
+ private int maxImageWidthId;
+ private int remoteBlogId;
+ private String dotcom_username;
+ private String dotcom_password;
+ private String api_key;
+ private String api_blogid;
+ private boolean dotcomFlag;
+ private String wpVersion;
+ private String httpuser = "";
+ private String httppassword = "";
+ private String postFormats;
+ private String blogOptions = "{}";
+ private String capabilities;
+ private boolean isAdmin;
+ private boolean isHidden;
+ private long planID;
+ private String planShortName;
+
+ public Blog() {
+ }
+
+ public Blog(int localTableBlogId, String url, String homeURL, String blogName, String username, String password, String imagePlacement, boolean featuredImageCapable, boolean fullSizeImage, boolean scaledImage, int scaledImageWidth, String maxImageWidth, int maxImageWidthId, int remoteBlogId, String dotcom_username, String dotcom_password, String api_key, String api_blogid, boolean dotcomFlag, String wpVersion, String httpuser, String httppassword, String postFormats, String blogOptions, String capabilities, boolean isAdmin, boolean isHidden) {
+ this.localTableBlogId = localTableBlogId;
+ this.url = url;
+ this.homeURL = homeURL;
+ this.blogName = blogName;
+ this.username = username;
+ this.password = password;
+ this.imagePlacement = imagePlacement;
+ this.featuredImageCapable = featuredImageCapable;
+ this.fullSizeImage = fullSizeImage;
+ this.scaledImage = scaledImage;
+ this.scaledImageWidth = scaledImageWidth;
+ this.maxImageWidth = maxImageWidth;
+ this.maxImageWidthId = maxImageWidthId;
+ this.remoteBlogId = remoteBlogId;
+ this.dotcom_username = dotcom_username;
+ this.dotcom_password = dotcom_password;
+ this.api_key = api_key;
+ this.api_blogid = api_blogid;
+ this.dotcomFlag = dotcomFlag;
+ this.wpVersion = wpVersion;
+ this.httpuser = httpuser;
+ this.httppassword = httppassword;
+ this.postFormats = postFormats;
+ this.blogOptions = blogOptions;
+ this.capabilities = capabilities;
+ this.isAdmin = isAdmin;
+ this.isHidden = isHidden;
+ }
+
+ public Blog(String url, String username, String password) {
+ this.url = url;
+ this.username = username;
+ this.password = password;
+ this.localTableBlogId = -1;
+ }
+
+ public int getLocalTableBlogId() {
+ return localTableBlogId;
+ }
+
+ public void setLocalTableBlogId(int id) {
+ this.localTableBlogId = id;
+ }
+
+ public String getNameOrHostUrl() {
+ return (getBlogName() == null || getBlogName().isEmpty()) ? getUri().getHost() : getBlogName();
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public URI getUri() {
+ try {
+ String url = getUrl();
+ if (url == null) {
+ AppLog.e(T.UTILS, "Blog url is null");
+ return null;
+ }
+ return new URI(url);
+ } catch (URISyntaxException e) {
+ AppLog.e(T.UTILS, "Blog url is invalid: " + getUrl());
+ return null;
+ }
+ }
+
+ /**
+ * TODO: When we rewrite this in WPStores, make sure we only have one of this function.
+ * This is used to open the site in the browser. getHomeURL() was not used, probably due to a bug where
+ * it returns an empty or invalid url.
+ * @return site url
+ */
+ public @NonNull String getAlternativeHomeUrl() {
+ String siteURL = null;
+ Gson gson = new Gson();
+ Type type = new TypeToken<Map<?, ?>>() { }.getType();
+ Map<?, ?> blogOptions = gson.fromJson(this.getBlogOptions(), type);
+ if (blogOptions != null) {
+ Map<?, ?> homeURLMap = (Map<?, ?>) blogOptions.get("home_url");
+ if (homeURLMap != null) {
+ siteURL = homeURLMap.get("value").toString();
+ }
+ }
+ // Try to guess the URL of the site if blogOptions is null (blog not added to the app)
+ if (siteURL == null) {
+ siteURL = this.getUrl().replace("/xmlrpc.php", "");
+ }
+ return siteURL;
+ }
+
+ public String getHomeURL() {
+ return homeURL;
+ }
+
+ public void setHomeURL(String homeURL) {
+ this.homeURL = homeURL;
+ }
+
+ public String getBlogName() {
+ return blogName;
+ }
+
+ public void setBlogName(String blogName) {
+ this.blogName = blogName;
+ }
+
+ public String getUsername() {
+ return StringUtils.notNullStr(username);
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return StringUtils.notNullStr(password);
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getImagePlacement() {
+ return imagePlacement;
+ }
+
+ public void setImagePlacement(String imagePlacement) {
+ this.imagePlacement = imagePlacement;
+ }
+
+ public boolean isFeaturedImageCapable() {
+ return featuredImageCapable;
+ }
+
+ public void setFeaturedImageCapable(boolean isCapable) {
+ this.featuredImageCapable = isCapable;
+ }
+
+ public boolean bsetFeaturedImageCapable(boolean isCapable) {
+ if (featuredImageCapable == isCapable) {
+ return false;
+ }
+ setFeaturedImageCapable(isCapable);
+ return true;
+ }
+
+ public boolean isFullSizeImage() {
+ return fullSizeImage;
+ }
+
+ public void setFullSizeImage(boolean fullSizeImage) {
+ this.fullSizeImage = fullSizeImage;
+ }
+
+ public String getMaxImageWidth() {
+ return StringUtils.notNullStr(maxImageWidth);
+ }
+
+ public void setMaxImageWidth(String maxImageWidth) {
+ this.maxImageWidth = maxImageWidth;
+ }
+
+ public int getMaxImageWidthId() {
+ return maxImageWidthId;
+ }
+
+ public void setMaxImageWidthId(int maxImageWidthId) {
+ this.maxImageWidthId = maxImageWidthId;
+ }
+
+ public int getRemoteBlogId() {
+ return remoteBlogId;
+ }
+
+ public void setRemoteBlogId(int blogId) {
+ this.remoteBlogId = blogId;
+ }
+
+ public String getDotcom_username() {
+ return dotcom_username;
+ }
+
+ public void setDotcom_username(String dotcomUsername) {
+ dotcom_username = dotcomUsername;
+ }
+
+ public String getDotcom_password() {
+ return dotcom_password;
+ }
+
+ public void setDotcom_password(String dotcomPassword) {
+ dotcom_password = dotcomPassword;
+ }
+
+ public String getApi_key() {
+ return api_key;
+ }
+
+ public void setApi_key(String apiKey) {
+ api_key = apiKey;
+ }
+
+ public String getApi_blogid() {
+ if (api_blogid == null) {
+ JSONObject jsonOptions = getBlogOptionsJSONObject();
+ if (jsonOptions!=null && jsonOptions.has("jetpack_client_id")) {
+ try {
+ String jetpackBlogId = jsonOptions.getJSONObject("jetpack_client_id").getString("value");
+ if (!TextUtils.isEmpty(jetpackBlogId)) {
+ this.setApi_blogid(jetpackBlogId);
+ WordPress.wpDB.saveBlog(this);
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Cannot load jetpack_client_id from options: " + jsonOptions, e);
+ }
+ }
+ }
+ return api_blogid;
+ }
+
+ public void setApi_blogid(String apiBlogid) {
+ api_blogid = apiBlogid;
+ }
+
+ public boolean isDotcomFlag() {
+ return dotcomFlag;
+ }
+
+ public void setDotcomFlag(boolean dotcomFlag) {
+ this.dotcomFlag = dotcomFlag;
+ }
+
+ public String getWpVersion() {
+ return wpVersion;
+ }
+
+ public void setWpVersion(String wpVersion) {
+ this.wpVersion = wpVersion;
+ }
+
+ public boolean bsetWpVersion(String wpVersion) {
+ if (StringUtils.equals(this.wpVersion, wpVersion)) {
+ return false;
+ }
+ setWpVersion(wpVersion);
+ return true;
+ }
+
+ public String getHttpuser() {
+ return httpuser;
+ }
+
+ public void setHttpuser(String httpuser) {
+ this.httpuser = httpuser;
+ }
+
+ public String getHttppassword() {
+ return httppassword;
+ }
+
+ public void setHttppassword(String httppassword) {
+ this.httppassword = httppassword;
+ }
+
+ public boolean isHidden() {
+ return isHidden;
+ }
+
+ public void setHidden(boolean isHidden) {
+ this.isHidden = isHidden;
+ }
+
+ public String getPostFormats() {
+ return postFormats;
+ }
+
+ public void setPostFormats(String postFormats) {
+ this.postFormats = postFormats;
+ }
+
+ public boolean bsetPostFormats(String postFormats) {
+ if (StringUtils.equals(this.postFormats, postFormats)) {
+ return false;
+ }
+ setPostFormats(postFormats);
+ return true;
+ }
+
+ public boolean isScaledImage() {
+ return scaledImage;
+ }
+
+ public void setScaledImage(boolean scaledImage) {
+ this.scaledImage = scaledImage;
+ }
+
+ public int getScaledImageWidth() {
+ return scaledImageWidth;
+ }
+
+ public void setScaledImageWidth(int scaledImageWidth) {
+ this.scaledImageWidth = scaledImageWidth;
+ }
+
+ public String getBlogOptions() {
+ return blogOptions;
+ }
+
+ public JSONObject getBlogOptionsJSONObject() {
+ String optionsString = getBlogOptions();
+ if (TextUtils.isEmpty(optionsString)) {
+ return null;
+ }
+ try {
+ return new JSONObject(optionsString);
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "invalid blogOptions json", e);
+ }
+ return null;
+ }
+
+ public void setBlogOptions(String blogOptions) {
+ this.blogOptions = blogOptions;
+ JSONObject options = getBlogOptionsJSONObject();
+ if (options == null) {
+ this.blogOptions = "{}";
+ options = getBlogOptionsJSONObject();
+ }
+
+ if (options.has("jetpack_client_id")) {
+ try {
+ String jetpackBlogId = options.getJSONObject("jetpack_client_id").getString("value");
+ if (!TextUtils.isEmpty(jetpackBlogId)) {
+ this.setApi_blogid(jetpackBlogId);
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Cannot load jetpack_client_id from options: " + blogOptions, e);
+ }
+ }
+ }
+
+ // TODO: it's ugly to compare json strings, we have to normalize both strings before
+ // comparison or compare JSON objects after parsing
+ public boolean bsetBlogOptions(String blogOptions) {
+ if (StringUtils.equals(this.blogOptions, blogOptions)) {
+ return false;
+ }
+ setBlogOptions(blogOptions);
+ return true;
+ }
+
+ public boolean isAdmin() {
+ return isAdmin;
+ }
+
+ public void setAdmin(boolean isAdmin) {
+ this.isAdmin = isAdmin;
+ }
+
+ public boolean bsetAdmin(boolean isAdmin) {
+ if (this.isAdmin == isAdmin) {
+ return false;
+ }
+ setAdmin(isAdmin);
+ return true;
+ }
+
+ public String getAdminUrl() {
+ String adminUrl = null;
+ JSONObject jsonOptions = getBlogOptionsJSONObject();
+ if (jsonOptions != null) {
+ try {
+ adminUrl = jsonOptions.getJSONObject("admin_url").getString("value");
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Cannot load admin_url from options: " + jsonOptions, e);
+ }
+ }
+
+ // Try to guess the URL of the dashboard if blogOptions is null (blog not added to the app), or WP version is < 3.6
+ if (TextUtils.isEmpty(adminUrl)) {
+ if (this.getUrl().lastIndexOf("/") != -1) {
+ adminUrl = this.getUrl().substring(0, this.getUrl().lastIndexOf("/")) + "/wp-admin";
+ } else {
+ adminUrl = this.getUrl().replace("xmlrpc.php", "wp-admin");
+ }
+ }
+ return adminUrl;
+ }
+
+ public boolean isPrivate() {
+ if (!isDotcomFlag()) {
+ return false; // only wpcom blogs can be marked private.
+ }
+ JSONObject jsonOptions = getBlogOptionsJSONObject();
+ if (jsonOptions != null && jsonOptions.has("blog_public")) {
+ try {
+ String blogPublicValue = jsonOptions.getJSONObject("blog_public").getString("value");
+ if (!TextUtils.isEmpty(blogPublicValue) && "-1".equals(blogPublicValue)) {
+ return true;
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Cannot load blog_public from options: " + jsonOptions, e);
+ }
+ }
+ return false;
+ }
+
+ public boolean isJetpackPowered() {
+ JSONObject jsonOptions = getBlogOptionsJSONObject();
+ if (jsonOptions != null && jsonOptions.has("jetpack_client_id")) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Return the Jetpack plugin version code
+ *
+ * @return The Jetpack version string, null for non Jetpack sites, or wpcom sites
+ */
+ public String getJetpackVersion() {
+ String jetpackVersion = null;
+ JSONObject jsonOptions = getBlogOptionsJSONObject();
+ if (jsonOptions != null && jsonOptions.has("jetpack_version")) {
+ try {
+ jetpackVersion = jsonOptions.getJSONObject("jetpack_version").getString("value");
+ } catch (JSONException e) {
+ AppLog.e(T.UTILS, "Cannot load jetpack_version from options: " + jsonOptions, e);
+ }
+ }
+ return jetpackVersion;
+ }
+
+ public boolean isPhotonCapable() {
+ return ((isDotcomFlag() && !isPrivate()) || (isJetpackPowered() && !hasValidHTTPAuthCredentials()));
+ }
+
+ public boolean hasValidJetpackCredentials() {
+ return !TextUtils.isEmpty(getDotcom_username()) && !TextUtils.isEmpty(getApi_key());
+ }
+
+ public boolean hasValidHTTPAuthCredentials() {
+ return !TextUtils.isEmpty(getHttppassword()) && !TextUtils.isEmpty(getHttpuser());
+ }
+
+ /**
+ * Get the remote Blog ID stored on the wpcom backend.
+ *
+ * In the app db it's stored in blogId for WP.com, and in api_blogId for Jetpack.
+ *
+ * For WP.com sites this function returns the same value of getRemoteBlogId().
+ *
+ * @return WP.com blogId string, potentially null for Jetpack sites
+ */
+ public String getDotComBlogId() {
+ if (isDotcomFlag()) {
+ return String.valueOf(getRemoteBlogId());
+ } else {
+ String remoteID = getApi_blogid();
+ // Self-hosted blogs edge cases.
+ if (TextUtils.isEmpty(remoteID)) {
+ return null;
+ }
+ try {
+ long parsedBlogID = Long.parseLong(remoteID);
+ // remote blogID is always > 1 for Jetpack blogs
+ if (parsedBlogID < 1) {
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ AppLog.e(T.UTILS, "The remote blog ID stored in options isn't valid: " + remoteID);
+ return null;
+ }
+ return remoteID;
+ }
+ }
+
+ public long getPlanID() {
+ return planID;
+ }
+
+ public void setPlanID(long planID) {
+ this.planID = planID;
+ }
+
+ public String getPlanShortName() {
+ return StringUtils.notNullStr(planShortName);
+ }
+
+ public void setPlanShortName(String name) {
+ this.planShortName = StringUtils.notNullStr(name);
+ }
+
+ public String getCapabilities() {
+ return StringUtils.notNullStr(capabilities);
+ }
+
+ public void setCapabilities(String capabilities) {
+ this.capabilities = capabilities;
+ }
+
+ public boolean hasCapability(Capability capability) {
+ // If a capability is missing it means the user don't have it.
+ if (capabilities.isEmpty() || capability == null) {
+ return false;
+ }
+ try {
+ JSONObject jsonObject = new JSONObject(capabilities);
+ return jsonObject.optBoolean(capability.getLabel());
+ } catch (JSONException e) {
+ AppLog.e(T.PEOPLE, "Capabilities is not a valid json: " + capabilities);
+ return false;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/BlogIdentifier.java b/WordPress/src/main/java/org/wordpress/android/models/BlogIdentifier.java
new file mode 100644
index 000000000..755614324
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/BlogIdentifier.java
@@ -0,0 +1,52 @@
+package org.wordpress.android.models;
+
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+/**
+ * A blog is uniquely identified by the combination of xmlRpcUrl and blogId
+ */
+public class BlogIdentifier {
+ private String mXmlRpcUrl;
+ private int mBlogId;
+
+ public BlogIdentifier(String mXmlRpcUrl, int mBlogId) {
+ this.mXmlRpcUrl = mXmlRpcUrl;
+ this.mBlogId = mBlogId;
+ }
+
+ public String getXmlRpcUrl() {
+ return mXmlRpcUrl;
+ }
+
+ public void setXmlRpcUrl(String mXmlRpcUrl) {
+ this.mXmlRpcUrl = mXmlRpcUrl;
+ }
+
+ public int getBlogId() {
+ return mBlogId;
+ }
+
+ public void setBlogId(int mBlogId) {
+ this.mBlogId = mBlogId;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) {
+ return false;
+ }
+ if (other == this) { // same instance
+ return true;
+ }
+ if (!(other instanceof BlogIdentifier)) {
+ return false;
+ }
+ BlogIdentifier o = (BlogIdentifier) other;
+ return mXmlRpcUrl.equals(o.getXmlRpcUrl()) && mBlogId == o.getBlogId();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(3739, 50989).append(mBlogId).append(mXmlRpcUrl).toHashCode();
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/models/BlogPairId.java b/WordPress/src/main/java/org/wordpress/android/models/BlogPairId.java
new file mode 100644
index 000000000..c458fe67b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/BlogPairId.java
@@ -0,0 +1,25 @@
+package org.wordpress.android.models;
+
+import java.io.Serializable;
+
+/**
+ * Simple POJO to store a pair of ids: a remoteBlogId + a genericId.
+ * Could be used to identify a comment (remoteBlogId + commentId) or a post (remoteBlogId + postId)
+ */
+public class BlogPairId implements Serializable {
+ private long mId;
+ private long mRemoteBlogId;
+
+ public BlogPairId(long remoteBlogId, long id) {
+ mRemoteBlogId = remoteBlogId;
+ mId = id;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public long getRemoteBlogId() {
+ return mRemoteBlogId;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Capability.java b/WordPress/src/main/java/org/wordpress/android/models/Capability.java
new file mode 100644
index 000000000..2ae03aeac
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Capability.java
@@ -0,0 +1,21 @@
+package org.wordpress.android.models;
+
+/**
+ * Used to decide what can the current user do in a particular blog
+ * A list of capabilities can be found in: https://codex.wordpress.org/Roles_and_Capabilities#Capabilities
+ */
+public enum Capability {
+ LIST_USERS("list_users"), // Check if user can visit People page
+ PROMOTE_USERS("promote_users"), // Check if user can change another user's role
+ REMOVE_USERS("remove_users"); // Check if user can remove another user
+
+ private final String label;
+
+ Capability(String label) {
+ this.label = label;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/CategoryModel.java b/WordPress/src/main/java/org/wordpress/android/models/CategoryModel.java
new file mode 100644
index 000000000..77b08bfae
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/CategoryModel.java
@@ -0,0 +1,65 @@
+package org.wordpress.android.models;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+/**
+ * Represents WordPress post Category data and handles local database (de)serialization.
+ */
+public class CategoryModel {
+ // Categories table column names
+ public static final String ID_COLUMN_NAME = "ID";
+ public static final String NAME_COLUMN_NAME = "name";
+ public static final String SLUG_COLUMN_NAME = "slug";
+ public static final String DESC_COLUMN_NAME = "description";
+ public static final String PARENT_ID_COLUMN_NAME = "parent";
+ public static final String POST_COUNT_COLUMN_NAME = "post_count";
+
+ public int id;
+ public String name;
+ public String slug;
+ public String description;
+ public int parentId;
+ public int postCount;
+ public boolean isInLocalTable;
+
+ public CategoryModel() {
+ id = -1;
+ name = "";
+ slug = "";
+ description = "";
+ parentId = -1;
+ postCount = 0;
+ isInLocalTable = false;
+ }
+
+ /**
+ * Sets data from a local database {@link Cursor}.
+ */
+ public void deserializeFromDatabase(Cursor cursor) {
+ if (cursor == null) return;
+
+ id = cursor.getInt(cursor.getColumnIndex(ID_COLUMN_NAME));
+ name = cursor.getString(cursor.getColumnIndex(NAME_COLUMN_NAME));
+ slug = cursor.getString(cursor.getColumnIndex(SLUG_COLUMN_NAME));
+ description = cursor.getString(cursor.getColumnIndex(DESC_COLUMN_NAME));
+ parentId = cursor.getInt(cursor.getColumnIndex(PARENT_ID_COLUMN_NAME));
+ postCount = cursor.getInt(cursor.getColumnIndex(POST_COUNT_COLUMN_NAME));
+ isInLocalTable = true;
+ }
+
+ /**
+ * Creates the {@link ContentValues} object to store this category data in a local database.
+ */
+ public ContentValues serializeToDatabase() {
+ ContentValues values = new ContentValues();
+ values.put(ID_COLUMN_NAME, id);
+ values.put(NAME_COLUMN_NAME, name);
+ values.put(SLUG_COLUMN_NAME, slug);
+ values.put(DESC_COLUMN_NAME, description);
+ values.put(PARENT_ID_COLUMN_NAME, parentId);
+ values.put(POST_COUNT_COLUMN_NAME, postCount);
+
+ return values;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/CategoryNode.java b/WordPress/src/main/java/org/wordpress/android/models/CategoryNode.java
new file mode 100644
index 000000000..cd01d8c79
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/CategoryNode.java
@@ -0,0 +1,110 @@
+package org.wordpress.android.models;
+
+import android.util.SparseArray;
+import org.wordpress.android.WordPress;
+
+import java.util.*;
+
+public class CategoryNode {
+ private int categoryId;
+ private String name;
+ private int parentId;
+ private int level;
+ SortedMap<String, CategoryNode> children = new TreeMap<String, CategoryNode>(new Comparator<String>() {
+ @Override
+ public int compare(String s, String s2) {
+ return s.compareToIgnoreCase(s2);
+ }
+ });
+
+ public SortedMap<String, CategoryNode> getChildren() {
+ return children;
+ }
+
+ public void setChildren(SortedMap<String, CategoryNode> children) {
+ this.children = children;
+ }
+
+ public CategoryNode(int categoryId, int parentId, String name) {
+ this.categoryId = categoryId;
+ this.parentId = parentId;
+ this.name = name;
+ }
+
+ public int getCategoryId() {
+ return categoryId;
+ }
+
+ public void setCategoryId(int categoryId) {
+ this.categoryId = categoryId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getParentId() {
+ return parentId;
+ }
+
+ public void setParentId(int parentId) {
+ this.parentId = parentId;
+ }
+
+ public int getLevel() {
+ return level;
+ }
+
+ public static CategoryNode createCategoryTreeFromDB(int blogId) {
+ CategoryNode rootCategory = new CategoryNode(-1, -1, "");
+ if (WordPress.wpDB == null) {
+ return rootCategory;
+ }
+ List<String> stringCategories = WordPress.wpDB.loadCategories(blogId);
+
+ // First pass instantiate CategoryNode objects
+ SparseArray<CategoryNode> categoryMap = new SparseArray<CategoryNode>();
+ CategoryNode currentRootNode;
+ for (String name : stringCategories) {
+ int categoryId = WordPress.wpDB.getCategoryId(blogId, name);
+ int parentId = WordPress.wpDB.getCategoryParentId(blogId, name);
+ CategoryNode node = new CategoryNode(categoryId, parentId, name);
+ categoryMap.put(categoryId, node);
+ }
+
+ // Second pass associate nodes to form a tree
+ for(int i = 0; i < categoryMap.size(); i++){
+ CategoryNode category = categoryMap.valueAt(i);
+ if (category.getParentId() == 0) { // root node
+ currentRootNode = rootCategory;
+ } else {
+ currentRootNode = categoryMap.get(category.getParentId(), rootCategory);
+ }
+ currentRootNode.children.put(category.getName(), categoryMap.get(category.getCategoryId()));
+ }
+ return rootCategory;
+ }
+
+ private static void preOrderTreeTraversal(CategoryNode node, int level, ArrayList<CategoryNode> returnValue) {
+ if (node == null) {
+ return ;
+ }
+ if (node.parentId != -1) {
+ node.level = level;
+ returnValue.add(node);
+ }
+ for (CategoryNode child : node.getChildren().values()) {
+ preOrderTreeTraversal(child, level + 1, returnValue);
+ }
+ }
+
+ public static ArrayList<CategoryNode> getSortedListOfCategoriesFromRoot(CategoryNode node) {
+ ArrayList<CategoryNode> sortedCategories = new ArrayList<CategoryNode>();
+ preOrderTreeTraversal(node, 0, sortedCategories);
+ return sortedCategories;
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Comment.java b/WordPress/src/main/java/org/wordpress/android/models/Comment.java
new file mode 100644
index 000000000..7ec244b53
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Comment.java
@@ -0,0 +1,244 @@
+package org.wordpress.android.models;
+
+import android.content.Context;
+import android.text.Spanned;
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+public class Comment {
+ public long postID;
+ public long commentID;
+
+ private String authorName;
+ private String status;
+ private String comment;
+ private String postTitle;
+ private String authorUrl;
+ private String authorEmail;
+ private String published;
+ private String profileImageUrl;
+
+ public Comment(long postID,
+ long commentID,
+ String authorName,
+ String pubDateGmt,
+ String comment,
+ String status,
+ String postTitle,
+ String authorURL,
+ String authorEmail,
+ String profileImageUrl) {
+ this.postID = postID;
+ this.commentID = commentID;
+ this.authorName = authorName;
+ this.status = status;
+ this.comment = comment;
+ this.postTitle = postTitle;
+ this.authorUrl = authorURL;
+ this.authorEmail = authorEmail;
+ this.profileImageUrl = profileImageUrl;
+ this.published = pubDateGmt;
+ }
+
+ private Comment() {
+ // nop
+ }
+
+ /*
+ * nbradbury 11/14/13 - create a comment from JSON (REST response)
+ * https://developer.wordpress.com/docs/api/1/get/sites/%24site/comments/%24comment_ID/
+ */
+ public static Comment fromJSON(JSONObject json) {
+ if (json == null)
+ return null;
+
+ Comment comment = new Comment();
+ comment.commentID = json.optLong("ID");
+ comment.status = JSONUtils.getString(json, "status");
+ comment.published = JSONUtils.getString(json, "date");
+
+ // note that the content often contains html, and on rare occasions may contain
+ // script blocks that need to be removed (only seen with blogs that use the
+ // sociable plugin)
+ comment.comment = HtmlUtils.stripScript(JSONUtils.getString(json, "content"));
+
+ JSONObject jsonPost = json.optJSONObject("post");
+ if (jsonPost != null) {
+ comment.postID = jsonPost.optLong("ID");
+ // TODO: c.postTitle = ???
+ }
+
+ JSONObject jsonAuthor = json.optJSONObject("author");
+ if (jsonAuthor!=null) {
+ // author names may contain html entities (esp. pingbacks)
+ comment.authorName = JSONUtils.getStringDecoded(jsonAuthor, "name");
+ comment.authorUrl = JSONUtils.getString(jsonAuthor, "URL");
+
+ // email address will be set to "false" when there isn't an email address
+ comment.authorEmail = JSONUtils.getString(jsonAuthor, "email");
+ if (comment.authorEmail.equals("false"))
+ comment.authorEmail = "";
+
+ comment.profileImageUrl = JSONUtils.getString(jsonAuthor, "avatar_URL");
+ }
+
+ return comment;
+ }
+
+ public String getProfileImageUrl() {
+ return StringUtils.notNullStr(profileImageUrl);
+ }
+ public void setProfileImageUrl(String url) {
+ profileImageUrl = StringUtils.notNullStr(url);
+ }
+ public boolean hasProfileImageUrl() {
+ return !TextUtils.isEmpty(profileImageUrl);
+ }
+
+ public CommentStatus getStatusEnum() {
+ return CommentStatus.fromString(status);
+ }
+
+ public String getStatus() {
+ return StringUtils.notNullStr(status);
+ }
+ public void setStatus(String status) {
+ this.status = StringUtils.notNullStr(status);
+ }
+
+ public String getPublished() {
+ return StringUtils.notNullStr(published);
+ }
+ public void setPublished(String pubDate) {
+ published = StringUtils.notNullStr(pubDate);
+ }
+
+ public boolean hasAuthorName() {
+ return !TextUtils.isEmpty(authorName);
+ }
+ public String getAuthorName() {
+ return StringUtils.notNullStr(authorName);
+ }
+ public void setAuthorName(String name) {
+ authorName = StringUtils.notNullStr(name);
+ }
+
+ public boolean hasAuthorEmail() {
+ return !TextUtils.isEmpty(authorEmail);
+ }
+ public String getAuthorEmail() {
+ return StringUtils.notNullStr(authorEmail);
+ }
+ public void setAuthorEmail(String email) {
+ authorEmail = StringUtils.notNullStr(email);
+ }
+
+ public boolean hasAuthorUrl() {
+ return !TextUtils.isEmpty(authorUrl);
+ }
+ public String getAuthorUrl() {
+ return StringUtils.notNullStr(authorUrl);
+ }
+ public void setAuthorUrl(String url) {
+ authorUrl = StringUtils.notNullStr(url);
+ }
+
+ public String getCommentText() {
+ return StringUtils.notNullStr(comment);
+ }
+ public void setCommentText(String text) {
+ comment = StringUtils.notNullStr(text);
+ }
+
+ public boolean hasPostTitle() {
+ return !TextUtils.isEmpty(postTitle);
+ }
+ public String getPostTitle() {
+ return StringUtils.notNullStr(postTitle);
+ }
+ public void setPostTitle(String title) {
+ postTitle = StringUtils.notNullStr(title);
+ }
+
+ /****
+ * the following are transient variables whose sole purpose is to cache commonly-used values
+ * for the comment that speeds up accessing them inside adapters
+ ****/
+
+ /*
+ * converts iso8601 published date to an actual java date
+ */
+ private transient java.util.Date dtPublished;
+ public java.util.Date getDatePublished() {
+ if (dtPublished == null)
+ dtPublished = DateTimeUtils.dateFromIso8601(published);
+ return dtPublished;
+ }
+
+ private transient Spanned unescapedCommentWithDrawables;
+ public void setUnescapedCommentWithDrawables(Spanned spanned){
+ unescapedCommentWithDrawables = spanned;
+ }
+ public Spanned getUnescapedCommentTextWithDrawables() {
+ return unescapedCommentWithDrawables;
+ }
+
+ private transient String unescapedPostTitle;
+ public String getUnescapedPostTitle() {
+ if (unescapedPostTitle == null)
+ unescapedPostTitle = StringUtils.unescapeHTML(getPostTitle().trim());
+ return unescapedPostTitle;
+ }
+
+ /*
+ * returns the avatar url as a photon/gravatar url set to the passed size
+ */
+ private transient String avatarForDisplay;
+ public String getAvatarForDisplay(int avatarSize) {
+ if (avatarForDisplay == null) {
+ if (hasProfileImageUrl()) {
+ avatarForDisplay = GravatarUtils.fixGravatarUrl(profileImageUrl, avatarSize);
+ } else if (hasAuthorEmail()) {
+ avatarForDisplay = GravatarUtils.gravatarFromEmail(authorEmail, avatarSize);
+ } else {
+ avatarForDisplay = "";
+ }
+ }
+ return avatarForDisplay;
+ }
+
+ /*
+ * returns the author + post title as "Author Name on Post Title" - used by comment list
+ */
+ private transient String formattedTitle;
+ public String getFormattedTitle() {
+ if (formattedTitle == null) {
+ Context context = WordPress.getContext();
+ final String author = (hasAuthorName() ? getAuthorName() : context.getString(R.string.anonymous));
+ if (hasPostTitle()) {
+ formattedTitle = author
+ + "<font color=" + HtmlUtils.colorResToHtmlColor(context, R.color.grey_darken_10) + ">"
+ + " " + context.getString(R.string.on) + " "
+ + "</font>"
+ + getUnescapedPostTitle();
+ } else {
+ formattedTitle = author;
+ }
+ }
+ return formattedTitle;
+ }
+
+ public boolean willTrashingPermanentlyDelete(){
+ CommentStatus status = getStatusEnum();
+ return CommentStatus.TRASH.equals(status) || CommentStatus.SPAM.equals(status);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/CommentList.java b/WordPress/src/main/java/org/wordpress/android/models/CommentList.java
new file mode 100644
index 000000000..6942e3f86
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/CommentList.java
@@ -0,0 +1,107 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class CommentList extends ArrayList<Comment> {
+ public int indexOfCommentId(long commentId) {
+ for (int i=0; i < this.size(); i++) {
+ if (commentId==this.get(i).commentID)
+ return i;
+ }
+ return -1;
+ }
+
+ /*
+ * replace comments in this list that match the passed list
+ */
+ public void replaceComments(final CommentList comments) {
+ if (comments == null || comments.size() == 0)
+ return;
+ for (Comment comment: comments) {
+ int index = indexOfCommentId(comment.commentID);
+ if (index > -1)
+ set(index, comment);
+ }
+ }
+
+ /*
+ * delete comments in this list that match the passed list
+ */
+ public void deleteComments(final CommentList comments) {
+ if (comments == null || comments.size() == 0)
+ return;
+ for (Comment comment: comments) {
+ int index = indexOfCommentId(comment.commentID);
+ if (index > -1)
+ remove(index);
+ }
+ }
+
+ /*
+ * returns true if any comments in this list have the passed status
+ */
+ public boolean hasAnyWithStatus(CommentStatus status) {
+ for (Comment comment: this) {
+ if (comment.getStatusEnum().equals(status))
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * returns true if any comments in this list do NOT have the passed status
+ */
+ public boolean hasAnyWithoutStatus(CommentStatus status) {
+ for (Comment comment: this) {
+ if (!comment.getStatusEnum().equals(status))
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * does passed list contain the same comments as this list?
+ */
+ public boolean isSameList(CommentList comments) {
+ if (comments == null || comments.size() != this.size())
+ return false;
+
+ for (final Comment comment: comments) {
+ int index = this.indexOfCommentId(comment.commentID);
+ if (index == -1)
+ return false;
+ final Comment thisComment = this.get(index);
+ if (!thisComment.getStatus().equals(comment.getStatus()))
+ return false;
+ if (!thisComment.getCommentText().equals(comment.getCommentText()))
+ return false;
+ if (!thisComment.getAuthorName().equals(comment.getAuthorName()))
+ return false;
+ if (!thisComment.getAuthorEmail().equals(comment.getAuthorEmail()))
+ return false;
+ if (!thisComment.getAuthorUrl().equals(comment.getAuthorUrl()))
+ return false;
+
+ }
+
+ return true;
+ }
+
+ public static CommentList fromJSONV1_1(JSONObject object) throws JSONException {
+ CommentList commentList = new CommentList();
+ if (object == null) {
+ return null;
+ } else {
+ JSONArray comments = object.getJSONArray("comments");
+ for (int i=0; i < comments.length(); i++){
+ JSONObject commentJson = comments.getJSONObject(i);
+ commentList.add(Comment.fromJSON(commentJson));
+ }
+ return commentList;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/CommentStatus.java b/WordPress/src/main/java/org/wordpress/android/models/CommentStatus.java
new file mode 100644
index 000000000..a855e716f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/CommentStatus.java
@@ -0,0 +1,83 @@
+package org.wordpress.android.models;
+
+import android.support.annotation.StringRes;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+
+public enum CommentStatus implements FilterCriteria {
+ UNKNOWN(R.string.comment_status_all),
+ UNAPPROVED(R.string.comment_status_unapproved),
+ APPROVED(R.string.comment_status_approved),
+ TRASH(R.string.comment_status_trash),
+ SPAM(R.string.comment_status_spam),
+ DELETE(R.string.comment_status_trash);
+
+ private final int mLabelResId;
+
+ CommentStatus(@StringRes int labelResId) {
+ mLabelResId = labelResId;
+ }
+
+ @Override
+ public String getLabel() {
+ return WordPress.getContext().getString(mLabelResId);
+ }
+
+ /*
+ * returns the string representation of the passed status, as used by the XMLRPC API
+ */
+ public static String toString(CommentStatus status) {
+ if (status == null){
+ return "";
+ }
+
+ switch (status) {
+ case UNAPPROVED:
+ return "hold";
+ case APPROVED:
+ return "approve";
+ case SPAM:
+ return "spam";
+ case TRASH:
+ return "trash";
+ default:
+ return "";
+ }
+ }
+
+ /*
+ * returns the string representation of the passed status, as used by the REST API
+ */
+ public static String toRESTString(CommentStatus status) {
+ switch (status) {
+ case UNAPPROVED:
+ return "unapproved";
+ case APPROVED:
+ return "approved";
+ case SPAM:
+ return "spam";
+ case TRASH:
+ return "trash";
+ default:
+ return "all";
+ }
+ }
+
+ /*
+ * returns the status associated with the passed strings - handles both XMLRPC and REST
+ */
+ public static CommentStatus fromString(String value) {
+ if (value == null)
+ return CommentStatus.UNKNOWN;
+ if (value.equals("approve") || value.equals("approved"))
+ return CommentStatus.APPROVED;
+ if (value.equals("hold") || value.equals("unapproved"))
+ return CommentStatus.UNAPPROVED;
+ if (value.equals("spam"))
+ return SPAM;
+ if (value.equals("trash"))
+ return TRASH;
+ return CommentStatus.UNKNOWN;
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/models/FeatureSet.java b/WordPress/src/main/java/org/wordpress/android/models/FeatureSet.java
new file mode 100644
index 000000000..a64445042
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/FeatureSet.java
@@ -0,0 +1,38 @@
+package org.wordpress.android.models;
+
+import java.util.Map;
+
+/**
+ * A Model for parsing the result of wpcom.getFeatures() to retrieve
+ * features for a hosted WordPress.com blog.
+ */
+public class FeatureSet {
+ private int mBlogId;
+
+ private boolean mIsVideopressEnabled = false;
+ // add future features here
+
+ public FeatureSet(int blogId, Map<?,?> map) {
+ setBlogId(blogId);
+
+ if (map.containsKey("videopress_enabled"))
+ setIsVideopressEnabled((Boolean) map.get("videopress_enabled"));
+
+ }
+
+ public boolean isVideopressEnabled() {
+ return mIsVideopressEnabled;
+ }
+
+ public void setIsVideopressEnabled(boolean enabled) {
+ this.mIsVideopressEnabled = enabled;
+ }
+
+ public int getBlogId() {
+ return mBlogId;
+ }
+
+ public void setBlogId(int blogId) {
+ this.mBlogId = blogId;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/FilterCriteria.java b/WordPress/src/main/java/org/wordpress/android/models/FilterCriteria.java
new file mode 100644
index 000000000..db4866752
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/FilterCriteria.java
@@ -0,0 +1,5 @@
+package org.wordpress.android.models;
+
+public interface FilterCriteria {
+ String getLabel();
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/MediaUploadState.java b/WordPress/src/main/java/org/wordpress/android/models/MediaUploadState.java
new file mode 100644
index 000000000..5a40b3801
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/MediaUploadState.java
@@ -0,0 +1,15 @@
+package org.wordpress.android.models;
+
+public enum MediaUploadState {
+ QUEUED,
+ UPLOADING,
+ DELETE,
+ DELETED,
+ FAILED,
+ UPLOADED;
+
+ @Override
+ public String toString() {
+ return this.name().toLowerCase();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Note.java b/WordPress/src/main/java/org/wordpress/android/models/Note.java
new file mode 100644
index 000000000..7dc1a463b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Note.java
@@ -0,0 +1,590 @@
+/**
+ * Note represents a single WordPress.com notification
+ */
+package org.wordpress.android.models;
+
+import android.text.Html;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.simperium.client.BucketSchema;
+import com.simperium.client.Syncable;
+import com.simperium.util.JSONDiff;
+
+import org.apache.commons.lang.time.DateUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.List;
+
+public class Note extends Syncable {
+ private static final String TAG = "NoteModel";
+
+ // Maximum character length for a comment preview
+ static private final int MAX_COMMENT_PREVIEW_LENGTH = 200;
+
+ // Note types
+ public static final String NOTE_FOLLOW_TYPE = "follow";
+ public static final String NOTE_LIKE_TYPE = "like";
+ public static final String NOTE_COMMENT_TYPE = "comment";
+ private static final String NOTE_MATCHER_TYPE = "automattcher";
+ private static final String NOTE_COMMENT_LIKE_TYPE = "comment_like";
+ private static final String NOTE_REBLOG_TYPE = "reblog";
+ private static final String NOTE_UNKNOWN_TYPE = "unknown";
+
+ // 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_SPAM = "spam-comment";
+ private static final String ACTION_KEY_LIKE = "like-comment";
+
+ private JSONObject mActions;
+ private JSONObject mNoteJSON;
+ private final String mKey;
+
+ private final Object mSyncLock = new Object();
+ private String mLocalStatus;
+
+ public enum EnabledActions {
+ ACTION_REPLY,
+ ACTION_APPROVE,
+ ACTION_UNAPPROVE,
+ ACTION_SPAM,
+ ACTION_LIKE
+ }
+
+ public enum NoteTimeGroup {
+ GROUP_TODAY,
+ GROUP_YESTERDAY,
+ GROUP_OLDER_TWO_DAYS,
+ GROUP_OLDER_WEEK,
+ GROUP_OLDER_MONTH
+ }
+
+ /**
+ * Create a note using JSON from Simperium
+ */
+ private Note(String key, JSONObject noteJSON) {
+ mKey = key;
+ mNoteJSON = noteJSON;
+ }
+
+ /**
+ * Simperium method @see Diffable
+ */
+ @Override
+ public JSONObject getDiffableValue() {
+ synchronized (mSyncLock) {
+ return JSONDiff.deepCopy(mNoteJSON);
+ }
+ }
+
+ /**
+ * Simperium method for identifying bucket object @see Diffable
+ */
+ @Override
+ public String getSimperiumKey() {
+ return getId();
+ }
+
+ public String getId() {
+ return mKey;
+ }
+
+ public String getType() {
+ return queryJSON("type", NOTE_UNKNOWN_TYPE);
+ }
+
+ private Boolean isType(String type) {
+ return getType().equals(type);
+ }
+
+ public Boolean isCommentType() {
+ synchronized (mSyncLock) {
+ return (isAutomattcherType() && JSONUtils.queryJSON(mNoteJSON, "meta.ids.comment", -1) != -1) ||
+ isType(NOTE_COMMENT_TYPE);
+ }
+ }
+
+ public Boolean isAutomattcherType() {
+ return isType(NOTE_MATCHER_TYPE);
+ }
+
+ public Boolean isFollowType() {
+ return isType(NOTE_FOLLOW_TYPE);
+ }
+
+ public Boolean isLikeType() {
+ return isType(NOTE_LIKE_TYPE);
+ }
+
+ public Boolean isCommentLikeType() {
+ return isType(NOTE_COMMENT_LIKE_TYPE);
+ }
+
+ public Boolean isReblogType() {
+ return isType(NOTE_REBLOG_TYPE);
+ }
+
+ public Boolean isCommentReplyType() {
+ return isCommentType() && getParentCommentId() > 0;
+ }
+
+ // Returns true if the user has replied to this comment note
+ public Boolean isCommentWithUserReply() {
+ return isCommentType() && !TextUtils.isEmpty(getCommentSubjectNoticon());
+ }
+
+ public Boolean isUserList() {
+ return isLikeType() || isCommentLikeType() || isFollowType() || isReblogType();
+ }
+
+ /*
+ * does user have permission to moderate/reply/spam this comment?
+ */
+ public boolean canModerate() {
+ EnumSet<EnabledActions> enabledActions = getEnabledActions();
+ return enabledActions != null && (enabledActions.contains(EnabledActions.ACTION_APPROVE) || enabledActions.contains(EnabledActions.ACTION_UNAPPROVE));
+ }
+
+ public boolean canMarkAsSpam() {
+ EnumSet<EnabledActions> enabledActions = getEnabledActions();
+ return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_SPAM));
+ }
+
+ public boolean canReply() {
+ EnumSet<EnabledActions> enabledActions = getEnabledActions();
+ return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_REPLY));
+ }
+
+ public boolean canTrash() {
+ return canModerate();
+ }
+
+ public boolean canEdit(int localBlogId) {
+ return (localBlogId > 0 && canModerate());
+ }
+
+ public boolean canLike() {
+ EnumSet<EnabledActions> enabledActions = getEnabledActions();
+ return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_LIKE));
+ }
+
+ private String getLocalStatus() {
+ return StringUtils.notNullStr(mLocalStatus);
+ }
+
+ public void setLocalStatus(String localStatus) {
+ mLocalStatus = localStatus;
+ }
+
+ private JSONObject getSubject() {
+ try {
+ synchronized (mSyncLock) {
+ JSONArray subjectArray = mNoteJSON.getJSONArray("subject");
+ if (subjectArray.length() > 0) {
+ return subjectArray.getJSONObject(0);
+ }
+ }
+ } catch (JSONException e) {
+ return null;
+ }
+
+ return null;
+ }
+
+ private Spannable getFormattedSubject() {
+ return NotificationsUtils.getSpannableContentForRanges(getSubject());
+ }
+
+ public String getTitle() {
+ return queryJSON("title", "");
+ }
+
+ private String getIconURL() {
+ return queryJSON("icon", "");
+ }
+
+ private String getCommentSubject() {
+ synchronized (mSyncLock) {
+ JSONArray subjectArray = mNoteJSON.optJSONArray("subject");
+ if (subjectArray != null) {
+ String commentSubject = JSONUtils.queryJSON(subjectArray, "subject[1].text", "");
+
+ // Trim down the comment preview if the comment text is too large.
+ if (commentSubject != null && commentSubject.length() > MAX_COMMENT_PREVIEW_LENGTH) {
+ commentSubject = commentSubject.substring(0, MAX_COMMENT_PREVIEW_LENGTH - 1);
+ }
+
+ return commentSubject;
+ }
+
+ }
+
+ return "";
+ }
+
+ private String getCommentSubjectNoticon() {
+ JSONArray subjectRanges = queryJSON("subject[0].ranges", new JSONArray());
+ if (subjectRanges != null) {
+ for (int i=0; i < subjectRanges.length(); i++) {
+ try {
+ JSONObject rangeItem = subjectRanges.getJSONObject(i);
+ if (rangeItem.has("type") && rangeItem.optString("type").equals("noticon")) {
+ return rangeItem.optString("value", "");
+ }
+ } catch (JSONException e) {
+ return "";
+ }
+ }
+ }
+
+ return "";
+ }
+
+ public long getCommentReplyId() {
+ return queryJSON("meta.ids.reply_comment", 0);
+ }
+
+ /**
+ * Compare note timestamp to now and return a time grouping
+ */
+ public static NoteTimeGroup getTimeGroupForTimestamp(long timestamp) {
+ Date today = new Date();
+ Date then = new Date(timestamp * 1000);
+
+ if (then.compareTo(DateUtils.addMonths(today, -1)) < 0) {
+ return NoteTimeGroup.GROUP_OLDER_MONTH;
+ } else if (then.compareTo(DateUtils.addWeeks(today, -1)) < 0) {
+ return NoteTimeGroup.GROUP_OLDER_WEEK;
+ } else if (then.compareTo(DateUtils.addDays(today, -2)) < 0
+ || DateUtils.isSameDay(DateUtils.addDays(today, -2), then)) {
+ return NoteTimeGroup.GROUP_OLDER_TWO_DAYS;
+ } else if (DateUtils.isSameDay(DateUtils.addDays(today, -1), then)) {
+ return NoteTimeGroup.GROUP_YESTERDAY;
+ } else {
+ return NoteTimeGroup.GROUP_TODAY;
+ }
+ }
+
+ /**
+ * The inverse of isRead
+ */
+ public Boolean isUnread() {
+ return !isRead();
+ }
+
+ private Boolean isRead() {
+ return queryJSON("read", 0) == 1;
+ }
+
+ public void markAsRead() {
+ try {
+ synchronized (mSyncLock) {
+ mNoteJSON.put("read", 1);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Unable to update note read property", e);
+ return;
+ }
+ save();
+ }
+
+ /**
+ * Get the timestamp provided by the API for the note
+ */
+ public long getTimestamp() {
+ return DateTimeUtils.timestampFromIso8601(queryJSON("timestamp", ""));
+ }
+
+ public JSONArray getBody() {
+ try {
+ synchronized (mSyncLock) {
+ return mNoteJSON.getJSONArray("body");
+ }
+ } catch (JSONException e) {
+ return new JSONArray();
+ }
+ }
+
+ // returns character code for notification font
+ private String getNoticonCharacter() {
+ return queryJSON("noticon", "");
+ }
+
+ private JSONObject getCommentActions() {
+ if (mActions == null) {
+ // Find comment block that matches the root note comment id
+ long commentId = getCommentId();
+ JSONArray bodyArray = getBody();
+ for (int i = 0; i < bodyArray.length(); i++) {
+ try {
+ JSONObject bodyItem = bodyArray.getJSONObject(i);
+ if (bodyItem.has("type") && bodyItem.optString("type").equals("comment")
+ && commentId == JSONUtils.queryJSON(bodyItem, "meta.ids.comment", 0)) {
+ mActions = JSONUtils.queryJSON(bodyItem, "actions", new JSONObject());
+ break;
+ }
+ } catch (JSONException e) {
+ break;
+ }
+ }
+
+ if (mActions == null) {
+ mActions = new JSONObject();
+ }
+ }
+
+ return mActions;
+ }
+
+
+ private void updateJSON(JSONObject json) {
+ synchronized (mSyncLock) {
+ mNoteJSON = json;
+ }
+ }
+
+ /*
+ * returns the actions allowed on this note, assumes it's a comment notification
+ */
+ public EnumSet<EnabledActions> getEnabledActions() {
+ EnumSet<EnabledActions> actions = EnumSet.noneOf(EnabledActions.class);
+ JSONObject jsonActions = getCommentActions();
+ if (jsonActions == null || jsonActions.length() == 0) {
+ return actions;
+ }
+
+ if (jsonActions.has(ACTION_KEY_REPLY)) {
+ actions.add(EnabledActions.ACTION_REPLY);
+ }
+ if (jsonActions.has(ACTION_KEY_APPROVE) && jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) {
+ actions.add(EnabledActions.ACTION_UNAPPROVE);
+ }
+ if (jsonActions.has(ACTION_KEY_APPROVE) && !jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) {
+ actions.add(EnabledActions.ACTION_APPROVE);
+ }
+ if (jsonActions.has(ACTION_KEY_SPAM)) {
+ actions.add(EnabledActions.ACTION_SPAM);
+ }
+ if (jsonActions.has(ACTION_KEY_LIKE)) {
+ actions.add(EnabledActions.ACTION_LIKE);
+ }
+
+ return actions;
+ }
+
+ public int getSiteId() {
+ return queryJSON("meta.ids.site", 0);
+ }
+
+ public int getPostId() {
+ return queryJSON("meta.ids.post", 0);
+ }
+
+ public long getCommentId() {
+ return queryJSON("meta.ids.comment", 0);
+ }
+
+ public long getParentCommentId() {
+ return queryJSON("meta.ids.parent_comment", 0);
+ }
+
+ /**
+ * Rudimentary system for pulling an item out of a JSON object hierarchy
+ */
+ private <U> U queryJSON(String query, U defaultObject) {
+ synchronized (mSyncLock) {
+ if (mNoteJSON == null) return defaultObject;
+ return JSONUtils.queryJSON(mNoteJSON, query, defaultObject);
+ }
+ }
+
+ /**
+ * Constructs a new Comment object based off of data in a Note
+ */
+ public Comment buildComment() {
+ return new Comment(
+ getPostId(),
+ getCommentId(),
+ getCommentAuthorName(),
+ DateTimeUtils.iso8601FromTimestamp(getTimestamp()),
+ getCommentText(),
+ CommentStatus.toString(getCommentStatus()),
+ "", // post title is unavailable in note model
+ getCommentAuthorUrl(),
+ "", // user email is unavailable in note model
+ getIconURL()
+ );
+ }
+
+ public String getCommentAuthorName() {
+ JSONArray bodyArray = getBody();
+
+ for (int i=0; i < bodyArray.length(); i++) {
+ try {
+ JSONObject bodyItem = bodyArray.getJSONObject(i);
+ if (bodyItem.has("type") && bodyItem.optString("type").equals("user")) {
+ return bodyItem.optString("text");
+ }
+ } catch (JSONException e) {
+ return "";
+ }
+ }
+
+ return "";
+ }
+
+ private String getCommentText() {
+ return queryJSON("body[last].text", "");
+ }
+
+ private String getCommentAuthorUrl() {
+ JSONArray bodyArray = getBody();
+
+ for (int i=0; i < bodyArray.length(); i++) {
+ try {
+ JSONObject bodyItem = bodyArray.getJSONObject(i);
+ if (bodyItem.has("type") && bodyItem.optString("type").equals("user")) {
+ return JSONUtils.queryJSON(bodyItem, "meta.links.home", "");
+ }
+ } catch (JSONException e) {
+ return "";
+ }
+ }
+
+ return "";
+ }
+
+ public CommentStatus getCommentStatus() {
+ EnumSet<EnabledActions> enabledActions = getEnabledActions();
+
+ if (enabledActions.contains(EnabledActions.ACTION_UNAPPROVE)) {
+ return CommentStatus.APPROVED;
+ } else if (enabledActions.contains(EnabledActions.ACTION_APPROVE)) {
+ return CommentStatus.UNAPPROVED;
+ }
+
+ return CommentStatus.UNKNOWN;
+ }
+
+ public boolean hasLikedComment() {
+ JSONObject jsonActions = getCommentActions();
+ return !(jsonActions == null || jsonActions.length() == 0) && jsonActions.optBoolean(ACTION_KEY_LIKE);
+ }
+
+ public String getUrl() {
+ return queryJSON("url", "");
+ }
+
+ public JSONArray getHeader() {
+ synchronized (mSyncLock) {
+ return mNoteJSON.optJSONArray("header");
+ }
+ }
+
+ /**
+ * Represents a user replying to a note.
+ */
+ public static class Reply {
+ private final String mContent;
+ private final String mRestPath;
+
+ Reply(String restPath, String content) {
+ mRestPath = restPath;
+ mContent = content;
+ }
+
+ public String getContent() {
+ return mContent;
+ }
+
+ public String getRestPath() {
+ return mRestPath;
+ }
+ }
+
+ public Reply buildReply(String content) {
+ String restPath;
+ if (this.isCommentType()) {
+ restPath = String.format("sites/%d/comments/%d", getSiteId(), getCommentId());
+ } else {
+ restPath = String.format("sites/%d/posts/%d", getSiteId(), getPostId());
+ }
+
+ return new Reply(String.format("%s/replies/new", restPath), content);
+ }
+
+ /**
+ * Simperium Schema
+ */
+ public static class Schema extends BucketSchema<Note> {
+
+ static public final String NAME = "note20";
+ static public final String TIMESTAMP_INDEX = "timestamp";
+ static public final String SUBJECT_INDEX = "subject";
+ static public final String SNIPPET_INDEX = "snippet";
+ static public final String UNREAD_INDEX = "unread";
+ static public final String NOTICON_INDEX = "noticon";
+ static public final String ICON_URL_INDEX = "icon";
+ static public final String IS_UNAPPROVED_INDEX = "unapproved";
+ static public final String COMMENT_SUBJECT_NOTICON = "comment_subject_noticon";
+ static public final String LOCAL_STATUS = "local_status";
+ static public final String TYPE_INDEX = "type";
+
+ private static final Indexer<Note> sNoteIndexer = new Indexer<Note>() {
+
+ @Override
+ public List<Index> index(Note note) {
+ List<Index> indexes = new ArrayList<>();
+ try {
+ indexes.add(new Index(TIMESTAMP_INDEX, note.getTimestamp()));
+ } catch (NumberFormatException e) {
+ // note will not have an indexed timestamp so it will
+ // show up at the end of a query sorting by timestamp
+ android.util.Log.e("WordPress", "Failed to index timestamp", e);
+ }
+
+ indexes.add(new Index(SUBJECT_INDEX, Html.toHtml(note.getFormattedSubject())));
+ indexes.add(new Index(SNIPPET_INDEX, note.getCommentSubject()));
+ indexes.add(new Index(UNREAD_INDEX, note.isUnread()));
+ indexes.add(new Index(NOTICON_INDEX, note.getNoticonCharacter()));
+ indexes.add(new Index(ICON_URL_INDEX, note.getIconURL()));
+ indexes.add(new Index(IS_UNAPPROVED_INDEX, note.getCommentStatus() == CommentStatus.UNAPPROVED));
+ indexes.add(new Index(COMMENT_SUBJECT_NOTICON, note.getCommentSubjectNoticon()));
+ indexes.add(new Index(LOCAL_STATUS, note.getLocalStatus()));
+ indexes.add(new Index(TYPE_INDEX, note.getType()));
+
+ return indexes;
+ }
+
+ };
+
+ public Schema() {
+ addIndex(sNoteIndexer);
+ }
+
+ @Override
+ public String getRemoteName() {
+ return NAME;
+ }
+
+ @Override
+ public Note build(String key, JSONObject properties) {
+ return new Note(key, properties);
+ }
+
+ public void update(Note note, JSONObject properties) {
+ note.updateJSON(properties);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java b/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java
new file mode 100644
index 000000000..688c3821a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java
@@ -0,0 +1,115 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+// Maps to notification settings returned from the /me/notifications/settings endpoint on wp.com
+public class NotificationsSettings {
+
+ public static final String KEY_BLOGS = "blogs";
+ public static final String KEY_OTHER = "other";
+ public static final String KEY_DOTCOM = "wpcom";
+ public static final String KEY_DEVICES = "devices";
+
+ public static final String KEY_DEVICE_ID = "device_id";
+ public static final String KEY_BLOG_ID = "blog_id";
+
+ private JSONObject mOtherSettings;
+ private JSONObject mDotcomSettings;
+ private Map<Long, JSONObject> mBlogSettings;
+
+ // The main notification settings channels (displayed at root of NoticationsSettingsFragment)
+ public enum Channel {
+ OTHER,
+ BLOGS,
+ DOTCOM
+ }
+
+ // The notification setting type, used in BLOGS and OTHER channels
+ public enum Type {
+ TIMELINE,
+ EMAIL,
+ DEVICE;
+
+ public String toString() {
+ switch (this) {
+ case TIMELINE:
+ return "timeline";
+ case EMAIL:
+ return "email";
+ case DEVICE:
+ return "device";
+ default:
+ return "";
+ }
+ }
+ }
+
+ public NotificationsSettings(JSONObject json) {
+ updateJson(json);
+ }
+
+ // Parses the json response from /me/notifications/settings endpoint and updates the instance variables
+ public void updateJson(JSONObject json) {
+ mBlogSettings = new HashMap<>();
+
+ mOtherSettings = JSONUtils.queryJSON(json, KEY_OTHER, new JSONObject());
+ mDotcomSettings = JSONUtils.queryJSON(json, KEY_DOTCOM, new JSONObject());
+
+ JSONArray siteSettingsArray = JSONUtils.queryJSON(json, KEY_BLOGS, new JSONArray());
+ for (int i=0; i < siteSettingsArray.length(); i++) {
+ try {
+ JSONObject siteSetting = siteSettingsArray.getJSONObject(i);
+ mBlogSettings.put(siteSetting.optLong(KEY_BLOG_ID), siteSetting);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.NOTIFS, "Could not parse blog JSON in notification settings");
+ }
+ }
+ }
+
+ // Updates a specific notification setting after a user makes a change
+ public void updateSettingForChannelAndType(Channel channel, Type type, String settingName, boolean newValue, long blogId) {
+ String typeName = type.toString();
+ try {
+ switch (channel) {
+ case BLOGS:
+ JSONObject blogJson = getBlogSettings().get(blogId);
+ if (blogJson != null) {
+ JSONObject blogSetting = JSONUtils.queryJSON(blogJson, typeName, new JSONObject());
+ blogSetting.put(settingName, newValue);
+ blogJson.put(typeName, blogSetting);
+
+ getBlogSettings().put(blogId, blogJson);
+ }
+ break;
+ case OTHER:
+ JSONObject otherSetting = JSONUtils.queryJSON(getOtherSettings(), typeName, new JSONObject());
+ otherSetting.put(settingName, newValue);
+ getOtherSettings().put(typeName, otherSetting);
+ break;
+ case DOTCOM:
+ getDotcomSettings().put(settingName, newValue);
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.NOTIFS, "Could not update notifications settings JSON");
+ }
+ }
+
+ public JSONObject getOtherSettings() {
+ return mOtherSettings;
+ }
+
+ public Map<Long, JSONObject> getBlogSettings() {
+ return mBlogSettings;
+ }
+
+ public JSONObject getDotcomSettings() {
+ return mDotcomSettings;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java b/WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java
new file mode 100644
index 000000000..4c62fd9c7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/PeopleListFilter.java
@@ -0,0 +1,24 @@
+package org.wordpress.android.models;
+
+import android.support.annotation.StringRes;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+
+public enum PeopleListFilter implements FilterCriteria {
+ TEAM(R.string.people_dropdown_item_team),
+ FOLLOWERS(R.string.people_dropdown_item_followers),
+ EMAIL_FOLLOWERS(R.string.people_dropdown_item_email_followers),
+ VIEWERS(R.string.people_dropdown_item_viewers);
+
+ private final int mLabelResId;
+
+ PeopleListFilter(@StringRes int labelResId) {
+ mLabelResId = labelResId;
+ }
+
+ @Override
+ public String getLabel() {
+ return WordPress.getContext().getString(mLabelResId);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Person.java b/WordPress/src/main/java/org/wordpress/android/models/Person.java
new file mode 100644
index 000000000..f5c841bb4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Person.java
@@ -0,0 +1,174 @@
+package org.wordpress.android.models;
+
+import android.support.annotation.Nullable;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.StringUtils;
+
+public class Person {
+ public enum PersonType { USER, FOLLOWER, EMAIL_FOLLOWER, VIEWER }
+
+ private long personID;
+ private int localTableBlogId;
+ private String displayName;
+ private String avatarUrl;
+ private PersonType personType;
+
+ // Only users have a role
+ private Role role;
+
+ // Users, followers & viewers has a username, email followers don't
+ private String username;
+
+ // Only followers & email followers have a subscribed date
+ private String subscribed;
+
+ public Person(long personID, int localTableBlogId) {
+ this.personID = personID;
+ this.localTableBlogId = localTableBlogId;
+ }
+
+ @Nullable
+ public static Person userFromJSON(JSONObject json, int localTableBlogId) throws JSONException {
+ if (json == null) {
+ return null;
+ }
+
+ // Response parameters are in: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/users/%24user_id/
+ try {
+ long personID = Long.parseLong(json.getString("ID"));
+ Person person = new Person(personID, localTableBlogId);
+ person.setUsername(json.optString("login"));
+ person.setDisplayName(json.optString("name"));
+ person.setAvatarUrl(json.optString("avatar_URL"));
+ person.personType = PersonType.USER;
+ // We don't support multiple roles, so the first role is picked just as it's in Calypso
+ String role = json.getJSONArray("roles").optString(0);
+ person.setRole(Role.fromString(role));
+
+ return person;
+ } catch (NumberFormatException e) {
+ AppLog.e(AppLog.T.PEOPLE, "The ID parsed from the JSON couldn't be converted to long: " + e);
+ }
+
+ return null;
+ }
+
+ @Nullable
+ public static Person followerFromJSON(JSONObject json, int localTableBlogId, boolean isEmailFollower)
+ throws JSONException {
+ if (json == null) {
+ return null;
+ }
+
+ // Response parameters are in: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/followers/
+ try {
+ long personID = Long.parseLong(json.getString("ID"));
+ Person person = new Person(personID, localTableBlogId);
+ person.setDisplayName(json.optString("label"));
+ person.setUsername(json.optString("login"));
+ person.setAvatarUrl(json.optString("avatar"));
+ person.setSubscribed(json.optString("date_subscribed"));
+ person.personType = isEmailFollower ? PersonType.EMAIL_FOLLOWER : PersonType.FOLLOWER;
+
+ return person;
+ } catch (NumberFormatException e) {
+ AppLog.e(AppLog.T.PEOPLE, "The ID parsed from the JSON couldn't be converted to long: " + e);
+ }
+
+ return null;
+ }
+
+ @Nullable
+ public static Person viewerFromJSON(JSONObject json, int localTableBlogId) throws JSONException {
+ if (json == null) {
+ return null;
+ }
+
+ // Similar response parameters in:
+ // https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/users/%24user_id/
+ try {
+ long personID = Long.parseLong(json.getString("ID"));
+ Person person = new Person(personID, localTableBlogId);
+ person.setUsername(json.optString("login"));
+ person.setDisplayName(json.optString("name"));
+ person.setAvatarUrl(json.optString("avatar_URL"));
+ person.setPersonType(PersonType.VIEWER);
+
+ return person;
+ } catch (NumberFormatException e) {
+ AppLog.e(AppLog.T.PEOPLE, "The ID parsed from the JSON couldn't be converted to long: " + e);
+ }
+
+ return null;
+ }
+
+ public long getPersonID() {
+ return personID;
+ }
+
+ public int getLocalTableBlogId() {
+ return localTableBlogId;
+ }
+
+ public String getUsername() {
+ return StringUtils.notNullStr(username);
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getDisplayName() {
+ return StringUtils.notNullStr(displayName);
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public Role getRole() {
+ return role;
+ }
+
+ public void setRole(Role role) {
+ this.role = role;
+ }
+
+ public String getAvatarUrl() {
+ return StringUtils.notNullStr(avatarUrl);
+ }
+
+ public void setAvatarUrl(String avatarUrl) {
+ this.avatarUrl = avatarUrl;
+ }
+
+ public String getSubscribed() {
+ return StringUtils.notNullStr(subscribed);
+ }
+
+ public void setSubscribed(String subscribed) {
+ this.subscribed = StringUtils.notNullStr(subscribed);
+ }
+
+ /*
+ * converts iso8601 subscribed date to an actual java date
+ */
+ private transient java.util.Date dtSubscribed;
+ public java.util.Date getDateSubscribed() {
+ if (dtSubscribed == null)
+ dtSubscribed = DateTimeUtils.dateFromIso8601(subscribed);
+ return dtSubscribed;
+ }
+
+ public PersonType getPersonType() {
+ return personType;
+ }
+
+ public void setPersonType(PersonType personType) {
+ this.personType = personType;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Post.java b/WordPress/src/main/java/org/wordpress/android/models/Post.java
new file mode 100644
index 000000000..c62e2a5f5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Post.java
@@ -0,0 +1,505 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.StringUtils;
+
+import java.io.Serializable;
+
+public class Post implements Serializable {
+ // Increment this value if this model changes
+ // See: http://www.javapractices.com/topic/TopicAction.do?Id=45
+ static final long serialVersionUID = 2L;
+
+ public static String QUICK_MEDIA_TYPE_PHOTO = "QuickPhoto";
+
+ private static long FEATURED_IMAGE_INIT_VALUE = -2;
+
+ private long localTablePostId;
+ private int localTableBlogId;
+ private String categories;
+ private String customFields;
+ private long dateCreated;
+ private long dateCreatedGmt;
+ private String description;
+ private String link;
+ private boolean allowComments;
+ private boolean allowPings;
+ private String excerpt;
+ private String keywords;
+ private String moreText;
+ private String permaLink;
+ private String status;
+ private String remotePostId;
+ private String title;
+ private String userId;
+ private String authorDisplayName;
+ private String authorId;
+ private String password;
+ private String postFormat;
+ private String slug;
+
+ private boolean localDraft;
+ private boolean mChangedFromLocalToPublished;
+ private boolean isPage;
+ private String pageParentId;
+ private String pageParentTitle;
+ private boolean isLocalChange;
+ private String mediaPaths;
+ private String quickPostType;
+ private PostLocation mPostLocation;
+
+ private long lastKnownRemoteFeaturedImageId;
+ private long featuredImageId = FEATURED_IMAGE_INIT_VALUE;
+
+ public Post() {
+ }
+
+ public Post(int blogId, boolean isPage) {
+ // creates a new, empty post for the passed in blogId
+ this.localTableBlogId = blogId;
+ this.isPage = isPage;
+ this.localDraft = true;
+ }
+
+ public long getLocalTablePostId() {
+ return localTablePostId;
+ }
+
+ public long getDateCreated() {
+ return dateCreated;
+ }
+
+ public void setDateCreated(long dateCreated) {
+ this.dateCreated = dateCreated;
+ }
+
+ public long getDate_created_gmt() {
+ return dateCreatedGmt;
+ }
+
+ public void setDate_created_gmt(long dateCreatedGmt) {
+ this.dateCreatedGmt = dateCreatedGmt;
+ }
+
+ public void setCategories(String postCategories) {
+ this.categories = postCategories;
+ }
+
+ public void setCustomFields(String customFields) {
+ this.customFields = customFields;
+ }
+
+ public int getLocalTableBlogId() {
+ return localTableBlogId;
+ }
+
+ public void setLocalTableBlogId(int localTableBlogId) {
+ this.localTableBlogId = localTableBlogId;
+ }
+
+ public boolean isLocalDraft() {
+ return localDraft;
+ }
+
+ public void setLocalDraft(boolean localDraft) {
+ this.localDraft = localDraft;
+ }
+
+ public JSONArray getJSONCategories() {
+ JSONArray jArray = null;
+ if (categories == null) {
+ categories = "[]";
+ }
+ try {
+ categories = StringUtils.unescapeHTML(categories);
+ if (TextUtils.isEmpty(categories)) {
+ jArray = new JSONArray();
+ } else {
+ jArray = new JSONArray(categories);
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ return jArray;
+ }
+
+ public void setJSONCategories(JSONArray categories) {
+ this.categories = categories.toString();
+ }
+
+ public JSONArray getCustomFields() {
+ if (customFields == null) {
+ return null;
+ }
+ JSONArray jArray = null;
+ try {
+ jArray = new JSONArray(customFields);
+ } catch (JSONException e) {
+ AppLog.e(T.POSTS, "No custom fields found for post.");
+ }
+ return jArray;
+ }
+
+ public JSONObject getCustomField(String key) {
+ JSONArray customFieldsJson = getCustomFields();
+ if (customFieldsJson == null) {
+ return null;
+ }
+
+ for (int i = 0; i < customFieldsJson.length(); i++) {
+ try {
+ JSONObject jsonObject = new JSONObject(customFieldsJson.getString(i));
+ String curentKey = jsonObject.getString("key");
+ if (key.equals(curentKey)) {
+ return jsonObject;
+ }
+ } catch (JSONException e) {
+ AppLog.e(T.POSTS, e);
+ }
+ }
+ return null;
+ }
+
+ public void setCustomFields(JSONArray customFields) {
+ this.customFields = customFields.toString();
+ }
+
+ public String getDescription() {
+ return StringUtils.notNullStr(description);
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getLink() {
+ return StringUtils.notNullStr(link);
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public boolean isAllowComments() {
+ return allowComments;
+ }
+
+ public void setAllowComments(boolean mtAllowComments) {
+ allowComments = mtAllowComments;
+ }
+
+ public boolean isAllowPings() {
+ return allowPings;
+ }
+
+ public void setAllowPings(boolean mtAllowPings) {
+ allowPings = mtAllowPings;
+ }
+
+ public String getPostExcerpt() {
+ return StringUtils.notNullStr(excerpt);
+ }
+
+ public void setPostExcerpt(String mtExcerpt) {
+ excerpt = mtExcerpt;
+ }
+
+ public String getKeywords() {
+ return StringUtils.notNullStr(keywords);
+ }
+
+ public void setKeywords(String mtKeywords) {
+ keywords = mtKeywords;
+ }
+
+ public String getMoreText() {
+ return StringUtils.notNullStr(moreText);
+ }
+
+ public void setMoreText(String mtTextMore) {
+ moreText = mtTextMore;
+ }
+
+ public String getPermaLink() {
+ return StringUtils.notNullStr(permaLink);
+ }
+
+ public void setPermaLink(String permaLink) {
+ this.permaLink = permaLink;
+ }
+
+ public String getPostStatus() {
+ return StringUtils.notNullStr(status);
+ }
+
+ public void setPostStatus(String postStatus) {
+ status = postStatus;
+ }
+
+ public PostStatus getStatusEnum() {
+ return PostStatus.fromPost(this);
+ }
+
+ public String getRemotePostId() {
+ return StringUtils.notNullStr(remotePostId);
+ }
+
+ public void setRemotePostId(String postId) {
+ this.remotePostId = postId;
+ }
+
+ public String getTitle() {
+ return StringUtils.notNullStr(title);
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getUserId() {
+ return StringUtils.notNullStr(userId);
+ }
+
+ public void setUserId(String userid) {
+ this.userId = userid;
+ }
+
+ public String getAuthorDisplayName() {
+ return StringUtils.notNullStr(authorDisplayName);
+ }
+
+ public void setAuthorDisplayName(String wpAuthorDisplayName) {
+ authorDisplayName = wpAuthorDisplayName;
+ }
+
+ public String getAuthorId() {
+ return StringUtils.notNullStr(authorId);
+ }
+
+ public void setAuthorId(String wpAuthorId) {
+ authorId = wpAuthorId;
+ }
+
+ public String getPassword() {
+ return StringUtils.notNullStr(password);
+ }
+
+ public void setPassword(String wpPassword) {
+ password = wpPassword;
+ }
+
+ public String getPostFormat() {
+ return StringUtils.notNullStr(postFormat);
+ }
+
+ public void setPostFormat(String wpPostForm) {
+ postFormat = wpPostForm;
+ }
+
+ public String getSlug() {
+ return StringUtils.notNullStr(slug);
+ }
+
+ public void setSlug(String wpSlug) {
+ slug = wpSlug;
+ }
+
+ public String getMediaPaths() {
+ return StringUtils.notNullStr(mediaPaths);
+ }
+
+ public void setMediaPaths(String mediaPaths) {
+ this.mediaPaths = mediaPaths;
+ }
+
+ public boolean supportsLocation() {
+ // Right now, we only disable for pages.
+ return !isPage();
+ }
+
+ public boolean hasLocation() {
+ return mPostLocation != null && mPostLocation.isValid();
+ }
+
+ public PostLocation getLocation() {
+ return mPostLocation;
+ }
+
+ public void setLocation(PostLocation location) {
+ mPostLocation = location;
+ }
+
+ public void unsetLocation() {
+ mPostLocation = null;
+ }
+
+ public void setLocation(double latitude, double longitude) {
+ try {
+ mPostLocation = new PostLocation(latitude, longitude);
+ } catch (IllegalArgumentException e) {
+ mPostLocation = null;
+ AppLog.e(T.POSTS, e);
+ }
+ }
+
+ public boolean isPage() {
+ return isPage;
+ }
+
+ public void setIsPage(boolean isPage) {
+ this.isPage = isPage;
+ }
+
+ public String getPageParentId() {
+ return StringUtils.notNullStr(pageParentId);
+ }
+
+ public void setPageParentId(String wp_page_parent_id) {
+ this.pageParentId = wp_page_parent_id;
+ }
+
+ public String getPageParentTitle() {
+ return StringUtils.notNullStr(pageParentTitle);
+ }
+
+ public void setPageParentTitle(String wp_page_parent_title) {
+ this.pageParentTitle = wp_page_parent_title;
+ }
+
+ public boolean isLocalChange() {
+ return isLocalChange;
+ }
+
+ public void setLocalChange(boolean isLocalChange) {
+ this.isLocalChange = isLocalChange;
+ }
+
+ public void setLocalTablePostId(long id) {
+ this.localTablePostId = id;
+ }
+
+ public void setQuickPostType(String type) {
+ this.quickPostType = type;
+ }
+
+ public String getQuickPostType() {
+ return StringUtils.notNullStr(quickPostType);
+ }
+
+ /**
+ * This indicates if the post has changed from a draft to published. This is primarily used
+ * for stats tracking purposes as we want to ensure that we properly track certain things when
+ * the user first publishes a post
+ * @return
+ */
+ public boolean hasChangedFromDraftToPublished() {
+ return mChangedFromLocalToPublished;
+ }
+
+ public void setChangedFromDraftToPublished(boolean changedFromDraftToPublished) {
+ this.mChangedFromLocalToPublished = changedFromDraftToPublished;
+ }
+
+ /**
+ * Checks if this post currently has data differing from another post.
+ *
+ * @param otherPost The post to compare to this post's editable data.
+ * @return True if this post's data differs from otherPost's data, False otherwise.
+ */
+ public boolean hasChanges(Post otherPost) {
+ return otherPost == null || !(StringUtils.equals(title, otherPost.title) &&
+ StringUtils.equals(description, otherPost.description) &&
+ StringUtils.equals(excerpt, otherPost.excerpt) &&
+ StringUtils.equals(keywords, otherPost.keywords) &&
+ StringUtils.equals(categories, otherPost.categories) &&
+ StringUtils.equals(status, otherPost.status) &&
+ StringUtils.equals(password, otherPost.password) &&
+ StringUtils.equals(postFormat, otherPost.postFormat) &&
+ this.dateCreatedGmt == otherPost.dateCreatedGmt &&
+ PostLocation.equals(this.mPostLocation, otherPost.mPostLocation)
+ );
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + localTableBlogId;
+ result = prime * result + (int) (localTablePostId ^ (localTablePostId >>> 32));
+ result = prime * result + (isPage ? 1231 : 1237);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this)
+ return true;
+ if (other instanceof Post) {
+ Post otherPost = (Post) other;
+ return (this.localTablePostId == otherPost.localTablePostId &&
+ this.isPage == otherPost.isPage &&
+ this.localTableBlogId == otherPost.localTableBlogId
+ );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the entire post content
+ * Joins description and moreText fields if both are valid
+ * @return post content as String
+ */
+ public String getContent() {
+ String postContent;
+ if (!TextUtils.isEmpty(getMoreText())) {
+ if (isLocalDraft()) {
+ postContent = getDescription() + "\n&lt;!--more--&gt;\n" + getMoreText();
+ } else {
+ postContent = getDescription() + "\n<!--more-->\n" + getMoreText();
+ }
+ } else {
+ postContent = getDescription();
+ }
+
+ return postContent;
+ }
+
+ public boolean isPublished() {
+ return !getRemotePostId().isEmpty();
+ }
+
+ public boolean isPublishable() {
+ return !(getContent().isEmpty() && getPostExcerpt().isEmpty() && getTitle().isEmpty());
+ }
+
+ public boolean hasEmptyContentFields() {
+ return TextUtils.isEmpty(this.getTitle()) && TextUtils.isEmpty(this.getContent());
+ }
+
+ public long getFeaturedImageId() {
+ if (featuredImageId == FEATURED_IMAGE_INIT_VALUE) {
+ return 0;
+ }
+
+ return featuredImageId;
+ }
+
+ public void setFeaturedImageId(long id) {
+ if (featuredImageId == FEATURED_IMAGE_INIT_VALUE) {
+ lastKnownRemoteFeaturedImageId = id;
+ }
+
+ featuredImageId = id;
+ }
+
+ public boolean featuredImageHasChanged() {
+ return (lastKnownRemoteFeaturedImageId != featuredImageId);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/PostLocation.java b/WordPress/src/main/java/org/wordpress/android/models/PostLocation.java
new file mode 100644
index 000000000..fb48e0bf6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/PostLocation.java
@@ -0,0 +1,93 @@
+package org.wordpress.android.models;
+
+import java.io.Serializable;
+
+public class PostLocation implements Serializable {
+ static final double INVALID_LATITUDE = 9999;
+ static final double INVALID_LONGITUDE = 9999;
+ static final double MIN_LATITUDE = -90;
+ static final double MAX_LATITUDE = 90;
+ static final double MIN_LONGITUDE = -180;
+ static final double MAX_LONGITUDE = 180;
+
+ private double mLatitude = INVALID_LATITUDE;
+ private double mLongitude = INVALID_LONGITUDE;
+
+ public PostLocation() { }
+
+ public PostLocation(double latitude, double longitude) {
+ setLatitude(latitude);
+ setLongitude(longitude);
+ }
+
+ public boolean isValid() {
+ return isValidLatitude(mLatitude) && isValidLongitude(mLongitude);
+ }
+
+ public double getLatitude() {
+ return mLatitude;
+ }
+
+ public void setLatitude(double latitude) {
+ if (!isValidLatitude(latitude)) {
+ throw new IllegalArgumentException(
+ "Invalid latitude; must be between the range " + MIN_LATITUDE + " and " + MAX_LATITUDE
+ );
+ }
+
+ mLatitude = latitude;
+ }
+
+ public double getLongitude() {
+ return mLongitude;
+ }
+
+ public void setLongitude(double longitude) {
+ if (!isValidLongitude(longitude)) {
+ throw new IllegalArgumentException(
+ "Invalid longitude; must be between the range " + MIN_LONGITUDE + " and " + MAX_LONGITUDE
+ );
+ }
+
+ mLongitude = longitude;
+ }
+
+ private boolean isValidLatitude(double latitude) {
+ return latitude >= MIN_LATITUDE && latitude <= MAX_LATITUDE;
+ }
+
+ private boolean isValidLongitude(double longitude) {
+ return longitude >= MIN_LONGITUDE && longitude <= MAX_LONGITUDE;
+ }
+
+ public int hashCode() {
+ final int prime = 31;
+ int hashCode = 1;
+
+ hashCode = prime * hashCode + (Double.valueOf(mLatitude).hashCode());
+ hashCode = prime * hashCode + (Double.valueOf(mLongitude).hashCode());
+
+ return hashCode;
+ }
+
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ } else if (other instanceof PostLocation) {
+ PostLocation otherLocation = (PostLocation) other;
+ return this.mLatitude == otherLocation.mLatitude
+ && this.mLongitude == otherLocation.mLongitude;
+ }
+ return false;
+ }
+
+ public static boolean equals(Object a, Object b) {
+ if (a == b) {
+ return true;
+ } else if (a == null || b == null) {
+ return false;
+ } else {
+ return a.equals(b);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/PostStatus.java b/WordPress/src/main/java/org/wordpress/android/models/PostStatus.java
new file mode 100644
index 000000000..192a273a4
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/PostStatus.java
@@ -0,0 +1,70 @@
+package org.wordpress.android.models;
+
+import java.util.Date;
+
+public enum PostStatus {
+ UNKNOWN,
+ PUBLISHED,
+ DRAFT,
+ PRIVATE,
+ PENDING,
+ TRASHED,
+ SCHEDULED; //NOTE: Only used locally, WP has a 'future' status but it is not returned from the metaWeblog API
+
+ private synchronized static PostStatus fromStringAndDateGMT(String value, long dateCreatedGMT) {
+ if (value == null) {
+ return PostStatus.UNKNOWN;
+ } else if (value.equals("publish")) {
+ // Check if post is scheduled
+ Date d = new Date();
+ // Subtract 10 seconds from the server GMT date, in case server and device time slightly differ
+ if (dateCreatedGMT - 10000 > d.getTime()) {
+ return SCHEDULED;
+ }
+ return PUBLISHED;
+ } else if (value.equals("draft")) {
+ return PostStatus.DRAFT;
+ } else if (value.equals("private")) {
+ return PostStatus.PRIVATE;
+ } else if (value.equals("pending")) {
+ return PENDING;
+ } else if (value.equals("trash")) {
+ return TRASHED;
+ } else if (value.equals("future")) {
+ return SCHEDULED;
+ } else {
+ return PostStatus.UNKNOWN;
+ }
+ }
+
+ public synchronized static PostStatus fromPost(Post post) {
+ String value = post.getPostStatus();
+ long dateCreatedGMT = post.getDate_created_gmt();
+ return fromStringAndDateGMT(value, dateCreatedGMT);
+ }
+
+ public synchronized static PostStatus fromPostsListPost(PostsListPost post) {
+ String value = post.getOriginalStatus();
+ long dateCreatedGMT = post.getDateCreatedGmt();
+ return fromStringAndDateGMT(value, dateCreatedGMT);
+ }
+
+ public static String toString(PostStatus status) {
+ switch (status) {
+ case PUBLISHED:
+ return "publish";
+ case DRAFT:
+ return "draft";
+ case PRIVATE:
+ return "private";
+ case PENDING:
+ return "pending";
+ case TRASHED:
+ return "trash";
+ case SCHEDULED:
+ return "future";
+ default:
+ return "";
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/PostsListPost.java b/WordPress/src/main/java/org/wordpress/android/models/PostsListPost.java
new file mode 100644
index 000000000..b9ddd7e26
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/PostsListPost.java
@@ -0,0 +1,183 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.ui.posts.services.PostUploadService;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.text.BreakIterator;
+import java.util.Date;
+
+/**
+ * Barebones post/page as listed in PostsListFragment
+ */
+public class PostsListPost {
+ private static final int MAX_EXCERPT_LEN = 150;
+
+ private final long postId;
+ private final long blogId;
+ private long dateCreatedGmt;
+ private final long featuredImageId;
+
+ private final String title;
+ private String excerpt;
+ private final String description;
+ private final String status;
+
+ private final boolean isLocalDraft;
+ private final boolean hasLocalChanges;
+ private final boolean isUploading;
+
+ // featuredImageUrl is generated by the adapter on the fly
+ private transient String featuredImageUrl;
+
+ public PostsListPost(Post post) {
+ postId = post.getLocalTablePostId();
+ blogId = post.getLocalTableBlogId();
+ featuredImageId = post.getFeaturedImageId();
+
+ title = post.getTitle();
+ description = post.getDescription();
+ excerpt = post.getPostExcerpt();
+
+ status = post.getPostStatus();
+ isLocalDraft = post.isLocalDraft();
+ hasLocalChanges = post.isLocalChange();
+ isUploading = PostUploadService.isPostUploading(postId);
+
+ setDateCreatedGmt(post.getDate_created_gmt());
+
+ // if the post doesn't have an excerpt, generate one from the description
+ if (!hasExcerpt() && hasDescription()) {
+ excerpt = makeExcerpt(description);
+ }
+ }
+
+ public long getPostId() {
+ return postId;
+ }
+
+ public long getBlogId() {
+ return blogId;
+ }
+
+ public String getTitle() {
+ return StringUtils.notNullStr(title);
+ }
+ public boolean hasTitle() {
+ return !TextUtils.isEmpty(title);
+ }
+
+ public String getDescription() {
+ return StringUtils.notNullStr(description);
+ }
+ public boolean hasDescription() {
+ return !TextUtils.isEmpty(description);
+ }
+
+ public String getExcerpt() {
+ return StringUtils.notNullStr(excerpt);
+ }
+ public boolean hasExcerpt() {
+ return !TextUtils.isEmpty(excerpt);
+ }
+
+ /*
+ * java's string.trim() doesn't handle non-breaking space chars (#160), which may appear at the
+ * end of post content - work around this by converting them to standard spaces before trimming
+ */
+ private static final String NBSP = String.valueOf((char) 160);
+ private static String trimEx(final String s) {
+ return s.replace(NBSP, " ").trim();
+ }
+
+ private static String makeExcerpt(String description) {
+ if (TextUtils.isEmpty(description)) {
+ return null;
+ }
+
+ String s = HtmlUtils.fastStripHtml(description);
+ if (s.length() < MAX_EXCERPT_LEN) {
+ return trimEx(s);
+ }
+
+ StringBuilder result = new StringBuilder();
+ BreakIterator wordIterator = BreakIterator.getWordInstance();
+ wordIterator.setText(s);
+ int start = wordIterator.first();
+ int end = wordIterator.next();
+ int totalLen = 0;
+ while (end != BreakIterator.DONE) {
+ String word = s.substring(start, end);
+ result.append(word);
+ totalLen += word.length();
+ if (totalLen >= MAX_EXCERPT_LEN) {
+ break;
+ }
+ start = end;
+ end = wordIterator.next();
+ }
+
+ if (totalLen == 0) {
+ return null;
+ }
+ return trimEx(result.toString()) + "...";
+ }
+
+ public long getFeaturedImageId() {
+ return featuredImageId;
+ }
+ public boolean hasFeaturedImageId() {
+ return featuredImageId != 0;
+ }
+
+ public String getFeaturedImageUrl() {
+ return StringUtils.notNullStr(featuredImageUrl);
+ }
+ public void setFeaturedImageUrl(String url) {
+ this.featuredImageUrl = StringUtils.notNullStr(url);
+ }
+ public boolean hasFeaturedImageUrl() {
+ return !TextUtils.isEmpty(featuredImageUrl);
+ }
+
+ public long getDateCreatedGmt() {
+ return dateCreatedGmt;
+ }
+ private void setDateCreatedGmt(long dateCreatedGmt) {
+ this.dateCreatedGmt = dateCreatedGmt;
+ }
+
+ public String getOriginalStatus() {
+ return StringUtils.notNullStr(status);
+ }
+
+ public PostStatus getStatusEnum() {
+ return PostStatus.fromPostsListPost(this);
+ }
+
+ public String getFormattedDate() {
+ if (getStatusEnum() == PostStatus.SCHEDULED) {
+ return DateUtils.formatDateTime(WordPress.getContext(), dateCreatedGmt, DateUtils.FORMAT_ABBREV_ALL);
+ } else {
+ return DateTimeUtils.javaDateToTimeSpan(new Date(dateCreatedGmt), WordPress.getContext());
+ }
+ }
+
+ public boolean isLocalDraft() {
+ return isLocalDraft;
+ }
+
+ public boolean hasLocalChanges() {
+ return hasLocalChanges;
+ }
+
+ public boolean isUploading() {
+ return isUploading;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/PostsListPostList.java b/WordPress/src/main/java/org/wordpress/android/models/PostsListPostList.java
new file mode 100644
index 000000000..4e5a20b32
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/PostsListPostList.java
@@ -0,0 +1,60 @@
+package org.wordpress.android.models;
+
+import java.util.ArrayList;
+
+public class PostsListPostList extends ArrayList<PostsListPost> {
+
+ public boolean isSameList(PostsListPostList newPostsList) {
+ if (newPostsList == null || this.size() != newPostsList.size()) {
+ return false;
+ }
+
+ for (int i = 0; i < newPostsList.size(); i++) {
+ PostsListPost newPost = newPostsList.get(i);
+ PostsListPost currentPost = this.get(i);
+
+ if (newPost.getPostId() != currentPost.getPostId())
+ return false;
+ if (!newPost.getTitle().equals(currentPost.getTitle()))
+ return false;
+ if (newPost.getDateCreatedGmt() != currentPost.getDateCreatedGmt())
+ return false;
+ if (!newPost.getOriginalStatus().equals(currentPost.getOriginalStatus()))
+ return false;
+ if (newPost.isUploading() != currentPost.isUploading())
+ return false;
+ if (newPost.isLocalDraft() != currentPost.isLocalDraft())
+ return false;
+ if (newPost.hasLocalChanges() != currentPost.hasLocalChanges())
+ return false;
+ if (!newPost.getDescription().equals(currentPost.getDescription()))
+ return false;
+ }
+
+ return true;
+ }
+
+ public int indexOfPost(PostsListPost post) {
+ if (post == null) {
+ return -1;
+ }
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).getPostId() == post.getPostId() && this.get(i).getBlogId() == post.getBlogId()) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public int indexOfFeaturedMediaId(long mediaId) {
+ if (mediaId == 0) {
+ return -1;
+ }
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).getFeaturedImageId() == mediaId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderBlog.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderBlog.java
new file mode 100644
index 000000000..e5c71a028
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderBlog.java
@@ -0,0 +1,169 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+
+public class ReaderBlog {
+ public long blogId;
+ public long feedId;
+
+ public boolean isPrivate;
+ public boolean isJetpack;
+ public boolean isFollowing;
+ public int numSubscribers;
+
+ private String name;
+ private String description;
+ private String url;
+ private String imageUrl;
+ private String feedUrl;
+
+ public static ReaderBlog fromJson(JSONObject json) {
+ ReaderBlog blog = new ReaderBlog();
+ if (json == null) {
+ return blog;
+ }
+
+ // if meta/data/site exists then JSON is for a read/following/mine?meta=site subscription,
+ // if meta/data/feed exists then JSON is for a read/following/mine?meta=feed subscription,
+ // otherwise JSON the response for a single site/$siteId or read/feed/$feedId
+ JSONObject jsonSite = JSONUtils.getJSONChild(json, "meta/data/site");
+ JSONObject jsonFeed = JSONUtils.getJSONChild(json, "meta/data/feed");
+ if (jsonSite != null) {
+ blog.blogId = jsonSite.optLong("ID");
+ blog.setName(JSONUtils.getStringDecoded(jsonSite, "name"));
+ blog.setDescription(JSONUtils.getStringDecoded(jsonSite, "description"));
+ blog.setUrl(JSONUtils.getString(jsonSite, "URL"));
+ blog.isJetpack = JSONUtils.getBool(jsonSite, "jetpack");
+ blog.isPrivate = JSONUtils.getBool(jsonSite, "is_private");
+ blog.isFollowing = JSONUtils.getBool(jsonSite, "is_following");
+ blog.numSubscribers = jsonSite.optInt("subscribers_count");
+ JSONObject jsonIcon = jsonSite.optJSONObject("icon");
+ if (jsonIcon != null) {
+ blog.setImageUrl(JSONUtils.getString(jsonIcon, "img"));
+ }
+ } else if (jsonFeed != null) {
+ blog.feedId = jsonFeed.optLong("feed_ID");
+ blog.setFeedUrl(JSONUtils.getString(jsonFeed, "feed_URL"));
+ blog.setName(JSONUtils.getStringDecoded(jsonFeed, "name"));
+ blog.setUrl(JSONUtils.getString(jsonFeed, "URL"));
+ blog.numSubscribers = jsonFeed.optInt("subscribers_count");
+ // read/following/mine doesn't include is_following for feeds, so assume to be true
+ blog.isFollowing = true;
+ } else {
+ blog.blogId = json.optLong("ID");
+ blog.feedId = json.optLong("feed_ID");
+ blog.setName(JSONUtils.getStringDecoded(json, "name"));
+ blog.setDescription(JSONUtils.getStringDecoded(json, "description"));
+ blog.setUrl(JSONUtils.getString(json, "URL"));
+ blog.setFeedUrl(JSONUtils.getString(json, "feed_URL"));
+ blog.isJetpack = JSONUtils.getBool(json, "jetpack");
+ blog.isPrivate = JSONUtils.getBool(json, "is_private");
+ blog.isFollowing = JSONUtils.getBool(json, "is_following");
+ blog.numSubscribers = json.optInt("subscribers_count");
+ }
+
+ // blogId will be empty for feeds, so set it to the feedId (consistent with /read/ endpoints)
+ if (blog.blogId == 0 && blog.feedId != 0) {
+ blog.blogId = blog.feedId;
+ }
+
+ JSONObject jsonIcon = JSONUtils.getJSONChild(json, "icon");
+ if (jsonIcon != null) {
+ blog.setImageUrl(JSONUtils.getString(jsonIcon, "img"));
+ if (!blog.hasImageUrl()) {
+ blog.setImageUrl(JSONUtils.getString(jsonIcon, "ico"));
+ }
+ }
+
+ return blog;
+ }
+
+ public String getName() {
+ return StringUtils.notNullStr(name);
+ }
+ public void setName(String blogName) {
+ this.name = StringUtils.notNullStr(blogName).trim();
+ }
+
+ public String getDescription() {
+ return StringUtils.notNullStr(description);
+ }
+ public void setDescription(String description) {
+ this.description = StringUtils.notNullStr(description).trim();
+ }
+
+ public String getImageUrl() {
+ return StringUtils.notNullStr(imageUrl);
+ }
+ public void setImageUrl(String imageUrl) {
+ this.imageUrl = StringUtils.notNullStr(imageUrl);
+ }
+
+ public String getUrl() {
+ return StringUtils.notNullStr(url);
+ }
+ public void setUrl(String url) {
+ this.url = StringUtils.notNullStr(url);
+ }
+
+ public String getFeedUrl() {
+ return StringUtils.notNullStr(feedUrl);
+ }
+ public void setFeedUrl(String feedUrl) {
+ this.feedUrl = StringUtils.notNullStr(feedUrl);
+ }
+
+ public boolean hasUrl() {
+ return !TextUtils.isEmpty(url);
+ }
+
+ public boolean hasFeedUrl() {
+ return !TextUtils.isEmpty(feedUrl);
+ }
+
+ public boolean hasImageUrl() {
+ return !TextUtils.isEmpty(imageUrl);
+ }
+ public boolean hasName() {
+ return !TextUtils.isEmpty(name);
+ }
+ public boolean hasDescription() {
+ return !TextUtils.isEmpty(description);
+ }
+
+ public boolean isExternal() {
+ return (feedId != 0);
+ }
+
+ /*
+ * returns the mshot url to use for this blog, ex:
+ * http://s.wordpress.com/mshots/v1/http%3A%2F%2Fnickbradbury.com?w=600
+ * note that while mshots support a "h=" parameter, this crops rather than
+ * scales the image to that height
+ * https://github.com/Automattic/mShots
+ */
+ public String getMshotsUrl(int width) {
+ return "http://s.wordpress.com/mshots/v1/"
+ + UrlUtils.urlEncode(getUrl())
+ + String.format("?w=%d", width);
+ }
+
+ public boolean isSameAs(ReaderBlog blogInfo) {
+ return blogInfo != null
+ && this.blogId == blogInfo.blogId
+ && this.feedId == blogInfo.feedId
+ && this.isFollowing == blogInfo.isFollowing
+ && this.isPrivate == blogInfo.isPrivate
+ && this.numSubscribers == blogInfo.numSubscribers
+ && this.getName().equals(blogInfo.getName())
+ && this.getDescription().equals(blogInfo.getDescription())
+ && this.getUrl().equals(blogInfo.getUrl())
+ && this.getFeedUrl().equals(blogInfo.getFeedUrl())
+ && this.getImageUrl().equals(blogInfo.getImageUrl());
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderBlogList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderBlogList.java
new file mode 100644
index 000000000..4351940a7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderBlogList.java
@@ -0,0 +1,87 @@
+package org.wordpress.android.models;
+
+import android.support.annotation.NonNull;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class ReaderBlogList extends ArrayList<ReaderBlog> {
+
+ @Override
+ public Object clone() {
+ return super.clone();
+ }
+
+ public static ReaderBlogList fromJson(JSONObject json) {
+ ReaderBlogList blogs = new ReaderBlogList();
+ if (json == null) {
+ return blogs;
+ }
+
+ // read/following/mine response
+ JSONArray jsonBlogs = json.optJSONArray("subscriptions");
+ if (jsonBlogs != null) {
+ for (int i = 0; i < jsonBlogs.length(); i++) {
+ ReaderBlog blog = ReaderBlog.fromJson(jsonBlogs.optJSONObject(i));
+ // make sure blog is valid before adding to the list - this can happen if user
+ // added a URL that isn't a feed or a blog since as of 29-May-2014 the API
+ // will let you follow any URL regardless if it's valid
+ if (blog.hasName() || blog.hasDescription() || blog.hasUrl()) {
+ blogs.add(blog);
+ }
+ }
+ }
+
+ return blogs;
+ }
+
+ private int indexOfBlogId(long blogId) {
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).blogId == blogId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public boolean isSameList(ReaderBlogList blogs) {
+ if (blogs == null || blogs.size() != this.size()) {
+ return false;
+ }
+
+ for (ReaderBlog blogInfo: blogs) {
+ int index = indexOfBlogId(blogInfo.blogId);
+ if (index == -1) {
+ return false;
+ }
+ ReaderBlog thisInfo = this.get(index);
+ if (!thisInfo.isSameAs(blogInfo)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /*
+ * returns true if the passed blog list has the same blogs that are in this list - differs
+ * from isSameList() in that isSameList() checks for *any* changes (subscription count, etc.)
+ * whereas this only checks if the passed list has any blogs that are not in this list, or
+ * this list has any blogs that are not in the passed list
+ */
+ public boolean hasSameBlogs(@NonNull ReaderBlogList blogs) {
+ if (blogs.size() != this.size()) {
+ return false;
+ }
+
+ for (ReaderBlog blogInfo: blogs) {
+ if (indexOfBlogId(blogInfo.blogId) == -1) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderComment.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderComment.java
new file mode 100644
index 000000000..f0d92cf08
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderComment.java
@@ -0,0 +1,138 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+public class ReaderComment {
+ public long commentId;
+ public long blogId;
+ public long postId;
+ public long parentId;
+
+ private String authorName;
+ private String authorAvatar;
+ private String authorUrl;
+ private String status;
+ private String text;
+
+ private String published;
+ public long timestamp;
+
+ public long authorId;
+ public long authorBlogId;
+
+ public int numLikes;
+ public boolean isLikedByCurrentUser;
+
+ public int pageNumber;
+
+ // not stored in db - denotes the indentation level when displaying this comment
+ public transient int level = 0;
+
+ public static ReaderComment fromJson(JSONObject json, long blogId) {
+ if (json == null) {
+ throw new IllegalArgumentException("null json comment");
+ }
+
+ ReaderComment comment = new ReaderComment();
+
+ comment.blogId = blogId;
+ comment.commentId = json.optLong("ID");
+ comment.status = JSONUtils.getString(json, "status");
+
+ // note that content may contain html, adapter needs to handle it
+ comment.text = HtmlUtils.stripScript(JSONUtils.getString(json, "content"));
+
+ comment.published = JSONUtils.getString(json, "date");
+ comment.timestamp = DateTimeUtils.timestampFromIso8601(comment.published);
+
+ JSONObject jsonPost = json.optJSONObject("post");
+ if (jsonPost != null) {
+ comment.postId = jsonPost.optLong("ID");
+ }
+
+ JSONObject jsonAuthor = json.optJSONObject("author");
+ if (jsonAuthor!=null) {
+ // author names may contain html entities (esp. pingbacks)
+ comment.authorName = JSONUtils.getStringDecoded(jsonAuthor, "name");
+ comment.authorAvatar = JSONUtils.getString(jsonAuthor, "avatar_URL");
+ comment.authorUrl = JSONUtils.getString(jsonAuthor, "URL");
+ comment.authorId = jsonAuthor.optLong("ID");
+ comment.authorBlogId = jsonAuthor.optLong("site_ID");
+ }
+
+ JSONObject jsonParent = json.optJSONObject("parent");
+ if (jsonParent != null) {
+ comment.parentId = jsonParent.optLong("ID");
+ }
+
+ // like info is found under meta/data/likes when meta=likes query param is used
+ JSONObject jsonLikes = JSONUtils.getJSONChild(json, "meta/data/likes");
+ if (jsonLikes != null) {
+ comment.numLikes = jsonLikes.optInt("found");
+ comment.isLikedByCurrentUser = JSONUtils.getBool(jsonLikes, "i_like");
+ }
+
+ return comment;
+ }
+
+ public String getAuthorName() {
+ return StringUtils.notNullStr(authorName);
+ }
+
+ public void setAuthorName(String authorName) {
+ this.authorName = StringUtils.notNullStr(authorName);
+ }
+
+ public String getAuthorAvatar() {
+ return StringUtils.notNullStr(authorAvatar);
+ }
+ public void setAuthorAvatar(String authorAvatar) {
+ this.authorAvatar = StringUtils.notNullStr(authorAvatar);
+ }
+
+ public String getAuthorUrl() {
+ return StringUtils.notNullStr(authorUrl);
+ }
+ public void setAuthorUrl(String authorUrl) {
+ this.authorUrl = StringUtils.notNullStr(authorUrl);
+ }
+
+ public String getText() {
+ return StringUtils.notNullStr(text);
+ }
+ public void setText(String text) {
+ this.text = StringUtils.notNullStr(text);
+ }
+
+ public String getStatus() {
+ return StringUtils.notNullStr(status);
+ }
+ public void setStatus(String status) {
+ this.status = StringUtils.notNullStr(status);
+ }
+
+ public String getPublished() {
+ return StringUtils.notNullStr(published);
+ }
+ public void setPublished(String published) {
+ this.published = StringUtils.notNullStr(published);
+ }
+
+ public boolean hasAuthorUrl() {
+ return !TextUtils.isEmpty(authorUrl);
+ }
+
+ public boolean hasAuthorBlogId() {
+ return (authorBlogId != 0);
+ }
+
+ public boolean hasAuthorAvatar() {
+ return !TextUtils.isEmpty(authorAvatar);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderCommentList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderCommentList.java
new file mode 100644
index 000000000..7a329d689
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderCommentList.java
@@ -0,0 +1,115 @@
+package org.wordpress.android.models;
+
+import java.util.ArrayList;
+
+public class ReaderCommentList extends ArrayList<ReaderComment> {
+
+ private boolean commentIdExists(long commentId) {
+ return (indexOfCommentId(commentId) > -1);
+ }
+
+ public int indexOfCommentId(long commentId) {
+ for (int i=0; i < this.size(); i++) {
+ if (commentId==this.get(i).commentId)
+ return i;
+ }
+ return -1;
+ }
+
+ /*
+ * does passed list contain the same comments as this list?
+ */
+ public boolean isSameList(ReaderCommentList comments) {
+ if (comments==null || comments.size()!=this.size())
+ return false;
+
+ for (ReaderComment comment: comments) {
+ if (!commentIdExists(comment.commentId))
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean replaceComment(long commentId, ReaderComment newComment) {
+ if (newComment == null) {
+ return false;
+ }
+
+ int index = indexOfCommentId(commentId);
+ if (index == -1) {
+ return false;
+ }
+
+ // make sure the new comment has the same level as the old one
+ newComment.level = this.get(index).level;
+
+ this.set(index, newComment);
+ return true;
+ }
+
+ /*
+ * builds a new list from the passed one with child comments placed under their parents and indent levels applied
+ */
+ public static ReaderCommentList getLevelList(ReaderCommentList thisList) {
+ if (thisList==null)
+ return new ReaderCommentList();
+
+ // first check if there are any child comments - if not, just return the passed list
+ boolean hasChildComments = false;
+ for (ReaderComment comment: thisList) {
+ if (comment.parentId!=0) {
+ hasChildComments = true;
+ break;
+ }
+ }
+ if (!hasChildComments)
+ return thisList;
+
+ ReaderCommentList result = new ReaderCommentList();
+
+ // reset all levels, and add root comments to result
+ for (ReaderComment comment: thisList) {
+ comment.level = 0;
+ if (comment.parentId==0)
+ result.add(comment);
+ }
+
+ // add child comments under their parents
+ boolean done;
+ do {
+ done = true;
+ for (ReaderComment comment: thisList) {
+ // only look at comments that have a parentId but no level assigned yet
+ if (comment.parentId!=0 && comment.level==0) {
+ int parentIndex = result.indexOfCommentId(comment.parentId);
+ if (parentIndex > -1) {
+ comment.level = result.get(parentIndex).level + 1;
+
+ // insert this comment after the last comment of this level that has this parent
+ int commentIndex=parentIndex+1;
+ while (commentIndex < result.size()) {
+ if (result.get(commentIndex).level!=comment.level || result.get(commentIndex).parentId!=comment.parentId)
+ break;
+ commentIndex++;
+ }
+ result.add(commentIndex, comment);
+
+
+ done = false;
+ }
+ }
+ }
+ } while (!done);
+
+ // handle orphans (child comments whose parents weren't found above)
+ for (ReaderComment comment: thisList) {
+ if (comment.level==0 && comment.parentId!=0) {
+ comment.level = 1; // give it a non-zero level so it's indented by ReaderCommentAdapter
+ result.add(comment);
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java
new file mode 100644
index 000000000..ee96aaa5f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java
@@ -0,0 +1,718 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId;
+import org.wordpress.android.ui.reader.utils.ImageSizeMap;
+import org.wordpress.android.ui.reader.utils.ReaderImageScanner;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.text.BreakIterator;
+import java.util.Iterator;
+
+public class ReaderPost {
+ private String pseudoId;
+ public long postId;
+ public long blogId;
+ public long feedId;
+ public long feedItemId;
+ public long authorId;
+
+ private String title;
+ private String text;
+ private String excerpt;
+ private String authorName;
+ private String authorFirstName;
+ private String blogName;
+ private String blogUrl;
+ private String postAvatar;
+
+ private String primaryTag; // most popular tag on this post based on usage in blog
+ private String secondaryTag; // second most popular tag on this post based on usage in blog
+
+ /*
+ * the "date" field is a generic date which depends on the stream the post is from:
+ * - for tagged posts, this is the date the post was tagged
+ * - for liked posts, this is the date the post was liked
+ * - for other posts, this is the date the post was published
+ * this date is used when requesting older posts from the backend, and is also used
+ * to generate the sortIndex below (which determines how posts are sorted for display)
+ */
+ private String date;
+ private String pubDate;
+ public double sortIndex;
+
+ private String url;
+ private String shortUrl;
+ private String featuredImage;
+ private String featuredVideo;
+
+ public int numReplies; // includes comments, trackbacks & pingbacks
+ public int numLikes;
+
+ public boolean isLikedByCurrentUser;
+ public boolean isFollowedByCurrentUser;
+ public boolean isCommentsOpen;
+ public boolean isExternal;
+ public boolean isPrivate;
+ public boolean isVideoPress;
+ public boolean isJetpack;
+
+ private String attachmentsJson;
+ private String discoverJson;
+ private String format;
+
+ public long xpostPostId;
+ public long xpostBlogId;
+
+ private String railcarJson;
+
+ public static ReaderPost fromJson(JSONObject json) {
+ if (json == null) {
+ throw new IllegalArgumentException("null json post");
+ }
+
+ ReaderPost post = new ReaderPost();
+
+ post.postId = json.optLong("ID");
+ post.blogId = json.optLong("site_ID");
+ post.feedId = json.optLong("feed_ID");
+ post.feedItemId = json.optLong("feed_item_ID");
+
+ if (json.has("pseudo_ID")) {
+ post.pseudoId = JSONUtils.getString(json, "pseudo_ID"); // read/ endpoint
+ } else {
+ post.pseudoId = JSONUtils.getString(json, "global_ID"); // sites/ endpoint
+ }
+
+ // remove HTML from the excerpt
+ post.excerpt = HtmlUtils.fastStripHtml(JSONUtils.getString(json, "excerpt")).trim();
+
+ post.text = JSONUtils.getString(json, "content");
+ post.title = JSONUtils.getStringDecoded(json, "title");
+ post.format = JSONUtils.getString(json, "format");
+ post.url = JSONUtils.getString(json, "URL");
+ post.shortUrl = JSONUtils.getString(json, "short_URL");
+ post.setBlogUrl(JSONUtils.getString(json, "site_URL"));
+
+ post.numLikes = json.optInt("like_count");
+ post.isLikedByCurrentUser = JSONUtils.getBool(json, "i_like");
+ post.isFollowedByCurrentUser = JSONUtils.getBool(json, "is_following");
+ post.isExternal = JSONUtils.getBool(json, "is_external");
+ post.isPrivate = JSONUtils.getBool(json, "site_is_private");
+ post.isJetpack = JSONUtils.getBool(json, "is_jetpack");
+
+ JSONObject jsonDiscussion = json.optJSONObject("discussion");
+ if (jsonDiscussion != null) {
+ post.isCommentsOpen = JSONUtils.getBool(jsonDiscussion, "comments_open");
+ post.numReplies = jsonDiscussion.optInt("comment_count");
+ } else {
+ post.isCommentsOpen = JSONUtils.getBool(json, "comments_open");
+ post.numReplies = json.optInt("comment_count");
+ }
+
+ // parse the author section
+ assignAuthorFromJson(post, json.optJSONObject("author"));
+
+ post.featuredImage = JSONUtils.getString(json, "featured_image");
+ post.blogName = JSONUtils.getStringDecoded(json, "site_name");
+ post.pubDate = JSONUtils.getString(json, "date");
+
+ // a post's date is the liked date for liked posts, tagged date for tag streams, and
+ // published date for all others
+ if (json.has("date_liked")) {
+ post.date = JSONUtils.getString(json, "date_liked");
+ } else if (json.has("tagged_on")) {
+ post.date = JSONUtils.getString(json, "tagged_on");
+ } else {
+ post.date = post.pubDate;
+ }
+
+ // sort index determines how posts are sorted, which is based on the date retrieved above
+ post.sortIndex = DateTimeUtils.timestampFromIso8601(post.date);
+
+ // if the post is untitled, make up a title from the excerpt
+ if (!post.hasTitle() && post.hasExcerpt()) {
+ post.title = extractTitle(post.excerpt, 50);
+ }
+
+ // remove html from title (rare, but does happen)
+ if (post.hasTitle() && post.title.contains("<") && post.title.contains(">")) {
+ post.title = HtmlUtils.stripHtml(post.title);
+ }
+
+ // parse the tags section
+ assignTagsFromJson(post, json.optJSONObject("tags"));
+
+ // parse the attachments
+ JSONObject jsonAttachments = json.optJSONObject("attachments");
+ if (jsonAttachments != null && jsonAttachments.length() > 0) {
+ post.attachmentsJson = jsonAttachments.toString();
+ }
+
+ // site metadata - returned when ?meta=site was added to the request
+ JSONObject jsonSite = JSONUtils.getJSONChild(json, "meta/data/site");
+ if (jsonSite != null) {
+ post.blogId = jsonSite.optInt("ID");
+ post.blogName = JSONUtils.getString(jsonSite, "name");
+ post.setBlogUrl(JSONUtils.getString(jsonSite, "URL"));
+ post.isPrivate = JSONUtils.getBool(jsonSite, "is_private");
+ // TODO: as of 29-Sept-2014, this is broken - endpoint returns false when it should be true
+ post.isJetpack = JSONUtils.getBool(jsonSite, "jetpack");
+ }
+
+ // "discover" posts
+ JSONObject jsonDiscover = json.optJSONObject("discover_metadata");
+ if (jsonDiscover != null) {
+ post.setDiscoverJson(jsonDiscover.toString());
+ }
+
+ // xpost info
+ assignXpostIdsFromJson(post, json.optJSONArray("metadata"));
+
+ // if there's no featured image, check if featured media has been set - this is sometimes
+ // a YouTube or Vimeo video, in which case store it as the featured video so we can treat
+ // it as a video
+ if (!post.hasFeaturedImage()) {
+ JSONObject jsonMedia = json.optJSONObject("featured_media");
+ if (jsonMedia != null && jsonMedia.length() > 0) {
+ String mediaUrl = JSONUtils.getString(jsonMedia, "uri");
+ if (!TextUtils.isEmpty(mediaUrl)) {
+ String type = JSONUtils.getString(jsonMedia, "type");
+ boolean isVideo = (type != null && type.equals("video"));
+ if (isVideo) {
+ post.featuredVideo = mediaUrl;
+ } else {
+ post.featuredImage = mediaUrl;
+ }
+ }
+ }
+ }
+ // if the post still doesn't have a featured image but we have attachment data, check whether
+ // we can find a suitable featured image from the attachments
+ if (!post.hasFeaturedImage() && post.hasAttachments()) {
+ post.featuredImage = new ImageSizeMap(post.attachmentsJson)
+ .getLargestImageUrl(ReaderConstants.MIN_FEATURED_IMAGE_WIDTH);
+ }
+ // if we *still* don't have a featured image but the text contains an IMG tag, check whether
+ // we can find a suitable image from the text
+ if (!post.hasFeaturedImage() && post.hasText() && post.text.contains("<img")) {
+ post.featuredImage = new ReaderImageScanner(post.text, post.isPrivate)
+ .getLargestImage(ReaderConstants.MIN_FEATURED_IMAGE_WIDTH);
+ }
+
+ // "railcar" data - currently used in search streams, used by TrainTracks
+ JSONObject jsonRailcar = json.optJSONObject("railcar");
+ if (jsonRailcar != null) {
+ post.setRailcarJson(jsonRailcar.toString());
+ }
+
+ return post;
+ }
+
+ /*
+ * assigns cross post blog & post IDs from post's metadata section
+ * "metadata": [
+ * {
+ * "id": "21192",
+ * "key": "xpost_origin",
+ * "value": "11326809:18427"
+ * }
+ * ],
+ */
+ private static void assignXpostIdsFromJson(ReaderPost post, JSONArray jsonMetadata) {
+ if (jsonMetadata == null) return;
+
+ for (int i = 0; i < jsonMetadata.length(); i++) {
+ JSONObject jsonMetaItem = jsonMetadata.optJSONObject(i);
+ String metaKey = jsonMetaItem.optString("key");
+ if (!TextUtils.isEmpty(metaKey) && metaKey.equals("xpost_origin")) {
+ String value = jsonMetaItem.optString("value");
+ if (!TextUtils.isEmpty(value) && value.contains(":")) {
+ String[] valuePair = value.split(":");
+ if (valuePair.length == 2) {
+ post.xpostBlogId = StringUtils.stringToLong(valuePair[0]);
+ post.xpostPostId = StringUtils.stringToLong(valuePair[1]);
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /*
+ * assigns author-related info to the passed post from the passed JSON "author" object
+ */
+ private static void assignAuthorFromJson(ReaderPost post, JSONObject jsonAuthor) {
+ if (jsonAuthor == null) return;
+
+ post.authorName = JSONUtils.getStringDecoded(jsonAuthor, "name");
+ post.authorFirstName = JSONUtils.getStringDecoded(jsonAuthor, "first_name");
+ post.postAvatar = JSONUtils.getString(jsonAuthor, "avatar_URL");
+ post.authorId = jsonAuthor.optLong("ID");
+
+ // site_URL doesn't exist for /sites/ endpoints, so get it from the author
+ if (TextUtils.isEmpty(post.blogUrl)) {
+ post.setBlogUrl(JSONUtils.getString(jsonAuthor, "URL"));
+ }
+ }
+
+ /*
+ * assigns primary/secondary tags to the passed post from the passed JSON "tags" object
+ */
+ private static void assignTagsFromJson(ReaderPost post, JSONObject jsonTags) {
+ if (jsonTags == null) {
+ return;
+ }
+
+ Iterator<String> it = jsonTags.keys();
+ if (!it.hasNext()) {
+ return;
+ }
+
+ // most popular tag & second most popular tag, based on usage count on this blog
+ String mostPopularTag = null;
+ String nextMostPopularTag = null;
+ int popularCount = 0;
+
+ while (it.hasNext()) {
+ JSONObject jsonThisTag = jsonTags.optJSONObject(it.next());
+
+ // if the number of posts on this blog that use this tag is higher than previous,
+ // set this as the most popular tag, and set the second most popular tag to
+ // the current most popular tag
+ int postCount = jsonThisTag.optInt("post_count");
+ if (postCount > popularCount) {
+ nextMostPopularTag = mostPopularTag;
+ mostPopularTag = JSONUtils.getStringDecoded(jsonThisTag, "slug");
+ popularCount = postCount;
+ }
+ }
+
+ // don't set primary tag if one is already set
+ if (!post.hasPrimaryTag()) {
+ post.setPrimaryTag(mostPopularTag);
+ }
+ post.setSecondaryTag(nextMostPopularTag);
+ }
+
+ /*
+ * extracts a title from a post's excerpt - used when the post has no title
+ */
+ private static String extractTitle(final String excerpt, int maxLen) {
+ if (TextUtils.isEmpty(excerpt))
+ return null;
+
+ if (excerpt.length() < maxLen)
+ return excerpt.trim();
+
+ StringBuilder result = new StringBuilder();
+ BreakIterator wordIterator = BreakIterator.getWordInstance();
+ wordIterator.setText(excerpt);
+ int start = wordIterator.first();
+ int end = wordIterator.next();
+ int totalLen = 0;
+ while (end != BreakIterator.DONE) {
+ String word = excerpt.substring(start, end);
+ result.append(word);
+ totalLen += word.length();
+ if (totalLen >= maxLen)
+ break;
+ start = end;
+ end = wordIterator.next();
+ }
+
+ if (totalLen==0)
+ return null;
+ return result.toString().trim() + "...";
+ }
+
+ // --------------------------------------------------------------------------------------------
+
+ public String getAuthorName() {
+ return StringUtils.notNullStr(authorName);
+ }
+ public void setAuthorName(String name) {
+ this.authorName = StringUtils.notNullStr(name);
+ }
+
+ public String getAuthorFirstName() {
+ return StringUtils.notNullStr(authorFirstName);
+ }
+ public void setAuthorFirstName(String name) {
+ this.authorFirstName = StringUtils.notNullStr(name);
+ }
+
+ public String getTitle() {
+ return StringUtils.notNullStr(title);
+ }
+ public void setTitle(String title) {
+ this.title = StringUtils.notNullStr(title);
+ }
+
+ public String getText() {
+ return StringUtils.notNullStr(text);
+ }
+ public void setText(String text) {
+ this.text = StringUtils.notNullStr(text);
+ }
+
+ public String getExcerpt() {
+ return StringUtils.notNullStr(excerpt);
+ }
+ public void setExcerpt(String excerpt) {
+ this.excerpt = StringUtils.notNullStr(excerpt);
+ }
+
+ // https://codex.wordpress.org/Post_Formats
+ public String getFormat() {
+ return StringUtils.notNullStr(format);
+ }
+ public void setFormat(String format) {
+ this.format = StringUtils.notNullStr(format);
+ }
+
+ public boolean isGallery() {
+ return format != null && format.equals("gallery");
+ }
+
+
+ public String getUrl() {
+ return StringUtils.notNullStr(url);
+ }
+ public void setUrl(String url) {
+ this.url = StringUtils.notNullStr(url);
+ }
+
+ public String getShortUrl() {
+ return StringUtils.notNullStr(shortUrl);
+ }
+ public void setShortUrl(String url) {
+ this.shortUrl = StringUtils.notNullStr(url);
+ }
+ public boolean hasShortUrl() {
+ return !TextUtils.isEmpty(shortUrl);
+ }
+
+ public String getFeaturedImage() {
+ return StringUtils.notNullStr(featuredImage);
+ }
+ public void setFeaturedImage(String featuredImage) {
+ this.featuredImage = StringUtils.notNullStr(featuredImage);
+ }
+
+ public String getFeaturedVideo() {
+ return StringUtils.notNullStr(featuredVideo);
+ }
+ public void setFeaturedVideo(String featuredVideo) {
+ this.featuredVideo = StringUtils.notNullStr(featuredVideo);
+ }
+
+ public String getBlogName() {
+ return StringUtils.notNullStr(blogName);
+ }
+ public void setBlogName(String blogName) {
+ this.blogName = StringUtils.notNullStr(blogName);
+ }
+
+ public String getBlogUrl() {
+ return StringUtils.notNullStr(blogUrl);
+ }
+ public void setBlogUrl(String blogUrl) {
+ this.blogUrl = StringUtils.notNullStr(blogUrl);
+ }
+
+ public String getPostAvatar() {
+ return StringUtils.notNullStr(postAvatar);
+ }
+ public void setPostAvatar(String postAvatar) {
+ this.postAvatar = StringUtils.notNullStr(postAvatar);
+ }
+
+ public String getPseudoId() {
+ return StringUtils.notNullStr(pseudoId);
+ }
+ public void setPseudoId(String pseudoId) {
+ this.pseudoId = StringUtils.notNullStr(pseudoId);
+ }
+
+ public String getDate() {
+ return StringUtils.notNullStr(date);
+ }
+ public void setDate(String dateStr) {
+ this.date = StringUtils.notNullStr(dateStr);
+ }
+
+ public String getPubDate() {
+ return StringUtils.notNullStr(pubDate);
+ }
+ public void setPubDate(String published) {
+ this.pubDate = StringUtils.notNullStr(published);
+ }
+
+ public String getPrimaryTag() {
+ return StringUtils.notNullStr(primaryTag);
+ }
+ public void setPrimaryTag(String tagName) {
+ // this is a bit of a hack to avoid setting the primary tag to one of the defaults
+ if (!ReaderTag.isDefaultTagTitle(tagName)) {
+ this.primaryTag = StringUtils.notNullStr(tagName);
+ }
+ }
+ boolean hasPrimaryTag() {
+ return !TextUtils.isEmpty(primaryTag);
+ }
+
+ public String getSecondaryTag() {
+ return StringUtils.notNullStr(secondaryTag);
+ }
+ public void setSecondaryTag(String tagName) {
+ if (!ReaderTag.isDefaultTagTitle(tagName)) {
+ this.secondaryTag = StringUtils.notNullStr(tagName);
+ }
+ }
+
+ /*
+ * attachments are stored as the actual JSON to avoid having a separate table for
+ * them, may need to revisit this if/when attachments become more important
+ */
+ public String getAttachmentsJson() {
+ return StringUtils.notNullStr(attachmentsJson);
+ }
+ public void setAttachmentsJson(String json) {
+ attachmentsJson = StringUtils.notNullStr(json);
+ }
+ public boolean hasAttachments() {
+ return !TextUtils.isEmpty(attachmentsJson);
+ }
+
+ /*
+ * "discover" posts also store the actual JSON
+ */
+ public String getDiscoverJson() {
+ return StringUtils.notNullStr(discoverJson);
+ }
+ public void setDiscoverJson(String json) {
+ discoverJson = StringUtils.notNullStr(json);
+ }
+ public boolean isDiscoverPost() {
+ return !TextUtils.isEmpty(discoverJson);
+ }
+
+ private transient ReaderPostDiscoverData discoverData;
+ public ReaderPostDiscoverData getDiscoverData() {
+ if (discoverData == null && !TextUtils.isEmpty(discoverJson)) {
+ try {
+ discoverData = new ReaderPostDiscoverData(new JSONObject(discoverJson));
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+ return discoverData;
+ }
+
+ public boolean hasText() {
+ return !TextUtils.isEmpty(text);
+ }
+
+ public boolean hasUrl() {
+ return !TextUtils.isEmpty(url);
+ }
+
+ public boolean hasExcerpt() {
+ return !TextUtils.isEmpty(excerpt);
+ }
+
+ public boolean hasFeaturedImage() {
+ return !TextUtils.isEmpty(featuredImage);
+ }
+
+ public boolean hasFeaturedVideo() {
+ return !TextUtils.isEmpty(featuredVideo);
+ }
+
+ public boolean hasPostAvatar() {
+ return !TextUtils.isEmpty(postAvatar);
+ }
+
+ public boolean hasBlogName() {
+ return !TextUtils.isEmpty(blogName);
+ }
+
+ public boolean hasAuthorName() {
+ return !TextUtils.isEmpty(authorName);
+ }
+
+ public boolean hasAuthorFirstName() {
+ return !TextUtils.isEmpty(authorFirstName);
+ }
+
+ public boolean hasTitle() {
+ return !TextUtils.isEmpty(title);
+ }
+
+ public boolean hasBlogUrl() {
+ return !TextUtils.isEmpty(blogUrl);
+ }
+
+ /*
+ * returns true if this post is from a WordPress blog
+ */
+ public boolean isWP() {
+ return !isExternal;
+ }
+
+ /*
+ * returns true if this is a cross-post
+ */
+ public boolean isXpost() {
+ return xpostBlogId != 0 && xpostPostId != 0;
+ }
+
+ /*
+ * returns true if the passed post appears to be the same as this one - used when posts are
+ * retrieved to determine which ones are new/changed/unchanged
+ */
+ public boolean isSamePost(ReaderPost post) {
+ return post != null
+ && post.blogId == this.blogId
+ && post.postId == this.postId
+ && post.feedId == this.feedId
+ && post.feedItemId == this.feedItemId
+ && post.numLikes == this.numLikes
+ && post.numReplies == this.numReplies
+ && post.isFollowedByCurrentUser == this.isFollowedByCurrentUser
+ && post.isLikedByCurrentUser == this.isLikedByCurrentUser
+ && post.isCommentsOpen == this.isCommentsOpen
+ && post.getTitle().equals(this.getTitle())
+ && post.getExcerpt().equals(this.getExcerpt())
+ && post.getText().equals(this.getText());
+ }
+
+ public boolean hasIds(ReaderBlogIdPostId ids) {
+ return ids != null
+ && ids.getBlogId() == this.blogId
+ && ids.getPostId() == this.postId;
+ }
+
+ /*
+ * liking is enabled for all wp.com and jp posts with the exception of discover posts
+ */
+ public boolean canLikePost() {
+ return (isWP() || isJetpack) && (!isDiscoverPost());
+ }
+
+
+ public String getRailcarJson() {
+ return StringUtils.notNullStr(railcarJson);
+ }
+ public void setRailcarJson(String jsonRailcar) {
+ this.railcarJson = StringUtils.notNullStr(jsonRailcar);
+ }
+ public boolean hasRailcar() {
+ return !TextUtils.isEmpty(railcarJson);
+ }
+
+ /****
+ * the following are transient variables - not stored in the db or returned in the json - whose
+ * sole purpose is to cache commonly-used values for the post that speeds up using them inside
+ * adapters
+ ****/
+
+ /*
+ * returns the featured image url as a photon url set to the passed width/height
+ */
+ private transient String featuredImageForDisplay;
+ public String getFeaturedImageForDisplay(int width, int height) {
+ if (featuredImageForDisplay == null) {
+ if (!hasFeaturedImage()) {
+ featuredImageForDisplay = "";
+ } else {
+ featuredImageForDisplay = ReaderUtils.getResizedImageUrl(featuredImage, width, height, isPrivate);
+ }
+ }
+ return featuredImageForDisplay;
+ }
+
+ /*
+ * returns the avatar url as a photon url set to the passed size
+ */
+ private transient String avatarForDisplay;
+ public String getPostAvatarForDisplay(int size) {
+ if (avatarForDisplay == null) {
+ if (!hasPostAvatar()) {
+ return "";
+ }
+ avatarForDisplay = GravatarUtils.fixGravatarUrl(postAvatar, size);
+ }
+ return avatarForDisplay;
+ }
+
+ /*
+ * returns the blog's blavatar url as a photon url set to the passed size
+ */
+ private transient String blavatarForDisplay;
+ public String getPostBlavatarForDisplay(int size) {
+ if (blavatarForDisplay == null) {
+ if (!hasBlogUrl()) {
+ return "";
+ }
+ blavatarForDisplay = GravatarUtils.blavatarFromUrl(getBlogUrl(), size);
+ }
+ return blavatarForDisplay;
+ }
+
+ /*
+ * converts iso8601 pubDate to a java date for display - this is the date that appears on posts
+ */
+ private transient java.util.Date dtDisplay;
+ public java.util.Date getDisplayDate() {
+ if (dtDisplay == null) {
+ dtDisplay = DateTimeUtils.dateFromIso8601(this.pubDate);
+ }
+ return dtDisplay;
+ }
+
+ /*
+ * determine which tag to display for this post
+ * - no tag if this is a private blog or there is no primary tag for this post
+ * - primary tag, unless it's the same as the currently selected tag
+ * - secondary tag if primary tag is the same as the currently selected tag
+ */
+ private transient String tagForDisplay;
+ public String getTagForDisplay(final String currentTagName) {
+ if (tagForDisplay == null) {
+ if (!isPrivate && hasPrimaryTag()) {
+ if (getPrimaryTag().equalsIgnoreCase(currentTagName)) {
+ tagForDisplay = getSecondaryTag();
+ } else {
+ tagForDisplay = getPrimaryTag();
+ }
+ } else {
+ tagForDisplay = "";
+ }
+ }
+ return tagForDisplay;
+ }
+
+ /*
+ * used when a unique numeric id is required by an adapter (when hasStableIds() = true)
+ */
+ private transient long stableId;
+ public long getStableId() {
+ if (stableId == 0) {
+ stableId = (pseudoId != null ? pseudoId.hashCode() : 0);
+ }
+ return stableId;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderPostDiscoverData.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderPostDiscoverData.java
new file mode 100644
index 000000000..b525708a7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPostDiscoverData.java
@@ -0,0 +1,187 @@
+package org.wordpress.android.models;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+/**
+ * additional data for "discover" posts in the reader - these are posts chosen by
+ * Editorial which highlight other posts or sites - the reader shows an attribution
+ * line for these posts, and when tapped they open the original post - the like
+ * and comment counts come from the original post
+ */
+public class ReaderPostDiscoverData {
+
+ public enum DiscoverType {
+ EDITOR_PICK,
+ SITE_PICK,
+ OTHER
+ }
+
+ private String authorName;
+ private String authorUrl;
+ private String blogName;
+ private String blogUrl;
+ private String avatarUrl;
+ private final String permaLink;
+
+ private long blogId;
+ private long postId;
+
+ private int numLikes;
+ private int numComments;
+
+ private DiscoverType discoverType = DiscoverType.OTHER;
+
+ /*
+ * passed JSONObject is the "discover_metadata" section of a reader post
+ */
+ public ReaderPostDiscoverData(@NonNull JSONObject json) {
+ permaLink = json.optString("permalink");
+
+ JSONObject jsonAttribution = json.optJSONObject("attribution");
+ if (jsonAttribution != null) {
+ authorName = jsonAttribution.optString("author_name");
+ authorUrl = jsonAttribution.optString("author_url");
+ blogName = jsonAttribution.optString("blog_name");
+ blogUrl = jsonAttribution.optString("blog_url");
+ avatarUrl = jsonAttribution.optString("avatar_url");
+ }
+
+ JSONObject jsonWpcomData = json.optJSONObject("featured_post_wpcom_data");
+ if (jsonWpcomData != null) {
+ blogId = jsonWpcomData.optLong("blog_id");
+ postId = jsonWpcomData.optLong("post_id");
+ numLikes = jsonWpcomData.optInt("like_count");
+ numComments = jsonWpcomData.optInt("comment_count");
+ }
+
+ // walk the post formats array until we find one we know we should handle differently
+ // - image-pick, quote-pick, and standard-pick all display as editors picks
+ // - site-pick displays as a site pick
+ // - collection + feature can be ignored because those display the same as normal posts
+ JSONArray jsonPostFormats = json.optJSONArray("discover_fp_post_formats");
+ if (jsonPostFormats != null) {
+ for (int i = 0; i < jsonPostFormats.length(); i++) {
+ String slug = JSONUtils.getString(jsonPostFormats.optJSONObject(i), "slug");
+ if (slug.equals("site-pick")) {
+ discoverType = DiscoverType.SITE_PICK;
+ break;
+ } else if (slug.equals("standard-pick") || slug.equals("image-pick") || slug.equals("quote-pick")) {
+ discoverType = DiscoverType.EDITOR_PICK;
+ break;
+ }
+ }
+ }
+ }
+
+ public long getBlogId() {
+ return blogId;
+ }
+
+ public long getPostId() {
+ return postId;
+ }
+
+ private String getAuthorName() {
+ return StringUtils.notNullStr(authorName);
+ }
+
+ private String getAuthorUrl() {
+ return StringUtils.notNullStr(authorUrl);
+ }
+
+ public String getBlogName() {
+ return StringUtils.notNullStr(blogName);
+ }
+
+ public String getBlogUrl() {
+ return StringUtils.notNullStr(blogUrl);
+ }
+
+ public String getAvatarUrl() {
+ return StringUtils.notNullStr(avatarUrl);
+ }
+
+ public String getPermaLink() {
+ return StringUtils.notNullStr(permaLink);
+ }
+
+ public boolean hasBlogUrl() {
+ return !TextUtils.isEmpty(blogUrl);
+ }
+
+ public boolean hasBlogName() {
+ return !TextUtils.isEmpty(blogName);
+ }
+
+ private boolean hasAuthorName() {
+ return !TextUtils.isEmpty(authorName);
+ }
+
+ public boolean hasPermalink() {
+ return !TextUtils.isEmpty(permaLink);
+ }
+
+ public boolean hasAvatarUrl() {
+ return !TextUtils.isEmpty(avatarUrl);
+ }
+
+ public DiscoverType getDiscoverType() {
+ return discoverType;
+ }
+
+ /*
+ * returns the spanned html for the attribution line
+ */
+ private transient Spanned attributionHtml;
+ public Spanned getAttributionHtml() {
+ if (attributionHtml == null) {
+ String html;
+ String author = "<strong>" + getAuthorName() + "</strong>";
+ String blog = "<strong>" + getBlogName() + "</strong>";
+ Context context = WordPress.getContext();
+
+ switch (getDiscoverType()) {
+ case EDITOR_PICK:
+ if (hasBlogName() && hasAuthorName()) {
+ // "Originally posted by [AuthorName] on [BlogName]"
+ html = String.format(context.getString(R.string.reader_discover_attribution_author_and_blog), author, blog);
+ } else if (hasBlogName()) {
+ // "Originally posted on [BlogName]"
+ html = String.format(context.getString(R.string.reader_discover_attribution_blog), blog);
+ } else if (hasAuthorName()) {
+ // "Originally posted by [AuthorName]"
+ html = String.format(context.getString(R.string.reader_discover_attribution_author), author);
+ } else {
+ return null;
+ }
+ break;
+
+ case SITE_PICK:
+ if (blogId != 0 && hasBlogName()) {
+ // "Visit [BlogName]" - opens blog preview when tapped
+ html = String.format(context.getString(R.string.reader_discover_visit_blog), blog);
+ } else {
+ return null;
+ }
+ break;
+
+ default:
+ return null;
+ }
+
+ attributionHtml = Html.fromHtml(html);
+ }
+ return attributionHtml;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderPostList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderPostList.java
new file mode 100644
index 000000000..bcc10ec0f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPostList.java
@@ -0,0 +1,90 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId;
+
+import java.util.ArrayList;
+
+public class ReaderPostList extends ArrayList<ReaderPost> {
+
+ public static ReaderPostList fromJson(JSONObject json) {
+ if (json == null) {
+ throw new IllegalArgumentException("null json post list");
+ }
+
+ ReaderPostList posts = new ReaderPostList();
+ JSONArray jsonPosts = json.optJSONArray("posts");
+ if (jsonPosts != null) {
+ for (int i = 0; i < jsonPosts.length(); i++) {
+ posts.add(ReaderPost.fromJson(jsonPosts.optJSONObject(i)));
+ }
+ }
+
+ return posts;
+ }
+
+ @Override
+ public Object clone() {
+ return super.clone();
+ }
+
+ public int indexOfPost(ReaderPost post) {
+ if (post == null) {
+ return -1;
+ }
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).postId == post.postId) {
+ if (post.isExternal && post.feedId == this.get(i).feedId) {
+ return i;
+ } else if (!post.isExternal && post.blogId == this.get(i).blogId) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ public int indexOfIds(ReaderBlogIdPostId ids) {
+ if (ids == null) {
+ return -1;
+ }
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).hasIds(ids)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /*
+ * does passed list contain the same posts as this list?
+ */
+ public boolean isSameList(ReaderPostList posts) {
+ if (posts == null || posts.size() != this.size()) {
+ return false;
+ }
+
+ for (ReaderPost post: posts) {
+ int index = indexOfPost(post);
+ if (index == -1 || !post.isSamePost(this.get(index))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /*
+ * returns posts in this list which are in the passed blog
+ */
+ public ReaderPostList getPostsInBlog(long blogId) {
+ ReaderPostList postsInBlog = new ReaderPostList();
+ for (ReaderPost post: this) {
+ if (post.blogId == blogId) {
+ postsInBlog.add(post);
+ }
+ }
+ return postsInBlog;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendBlogList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendBlogList.java
new file mode 100644
index 000000000..2076a543a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendBlogList.java
@@ -0,0 +1,54 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class ReaderRecommendBlogList extends ArrayList<ReaderRecommendedBlog> {
+
+ @Override
+ public Object clone() {
+ return super.clone();
+ }
+
+ public static ReaderRecommendBlogList fromJson(JSONObject json) {
+ ReaderRecommendBlogList blogs = new ReaderRecommendBlogList();
+
+ if (json == null) {
+ return blogs;
+ }
+
+ JSONArray jsonBlogs = json.optJSONArray("blogs");
+ if (jsonBlogs != null) {
+ for (int i = 0; i < jsonBlogs.length(); i++)
+ blogs.add(ReaderRecommendedBlog.fromJson(jsonBlogs.optJSONObject(i)));
+ }
+
+ return blogs;
+ }
+
+ private int indexOfBlogId(long blogId) {
+ for (int i = 0; i < size(); i++) {
+ if (this.get(i).blogId == blogId)
+ return i;
+ }
+ return -1;
+ }
+
+ public boolean isSameList(ReaderRecommendBlogList blogs) {
+ if (blogs == null || blogs.size() != this.size()) {
+ return false;
+ }
+
+ for (ReaderRecommendedBlog blog: blogs) {
+ int index = indexOfBlogId(blog.blogId);
+ if (index == -1 || !this.get(index).isSameAs(blog)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendedBlog.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendedBlog.java
new file mode 100644
index 000000000..5f314b2d7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderRecommendedBlog.java
@@ -0,0 +1,79 @@
+package org.wordpress.android.models;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+public class ReaderRecommendedBlog {
+ public long blogId;
+ public long followRecoId;
+ public int score;
+
+ private String title;
+ private String blogUrl;
+ private String imageUrl;
+ private String reason;
+
+ /*
+ * populated by response from get/read/recommendations/mine/
+ */
+ public static ReaderRecommendedBlog fromJson(JSONObject json) {
+ if (json == null) {
+ return null;
+ }
+
+ ReaderRecommendedBlog blog = new ReaderRecommendedBlog();
+
+ blog.blogId = json.optLong("blog_id");
+ blog.followRecoId = json.optLong("follow_reco_id");
+ blog.score = json.optInt("score");
+
+ blog.setTitle(JSONUtils.getString(json, "title"));
+ blog.setImageUrl(JSONUtils.getString(json, "image"));
+ blog.setReason(JSONUtils.getStringDecoded(json, "reason"));
+
+ // the "url" field points to an API endpoint, "blog_domain" contains the actual url
+ blog.setBlogUrl(JSONUtils.getString(json, "blog_domain"));
+
+ return blog;
+ }
+
+ public String getTitle() {
+ return StringUtils.notNullStr(title);
+ }
+ public void setTitle(String title) {
+ this.title = StringUtils.notNullStr(title);
+ }
+
+ public String getReason() {
+ return StringUtils.notNullStr(reason);
+ }
+ public void setReason(String reason) {
+ this.reason = StringUtils.notNullStr(reason);
+ }
+
+ public String getBlogUrl() {
+ return StringUtils.notNullStr(blogUrl);
+ }
+ public void setBlogUrl(String blogUrl) {
+ this.blogUrl = StringUtils.notNullStr(blogUrl);
+ }
+
+ public String getImageUrl() {
+ return StringUtils.notNullStr(imageUrl);
+ }
+ public void setImageUrl(String imageUrl) {
+ this.imageUrl = StringUtils.notNullStr(imageUrl);
+ }
+
+ protected boolean isSameAs(ReaderRecommendedBlog blog) {
+ if (blog == null) {
+ return false;
+ }
+ return (blog.blogId == this.blogId
+ && blog.score == this.score
+ && blog.followRecoId == this.followRecoId);
+ }
+
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java
new file mode 100644
index 000000000..0fa75a8b2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java
@@ -0,0 +1,214 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.io.Serializable;
+import java.util.regex.Pattern;
+
+public class ReaderTag implements Serializable, FilterCriteria {
+ private String tagSlug; // tag for API calls
+ private String tagDisplayName; // tag for display, usually the same as the slug
+ private String tagTitle; // title, used for default tags
+ private String endpoint; // endpoint for updating posts with this tag
+ public final ReaderTagType tagType;
+
+ // these are the default tags, which aren't localized in the /read/menu/ response
+ private static final String TAG_TITLE_LIKED = "Posts I Like";
+ private static final String TAG_TITLE_DISCOVER = "Discover";
+ public static final String TAG_TITLE_DEFAULT = TAG_TITLE_DISCOVER;
+ public static final String TAG_TITLE_FOLLOWED_SITES = "Followed Sites";
+
+ public ReaderTag(String slug,
+ String displayName,
+ String title,
+ String endpoint,
+ ReaderTagType tagType) {
+ // we need a slug since it's used to uniquely ID the tag (including setting it as the
+ // primary key in the tag table)
+ if (TextUtils.isEmpty(slug)) {
+ if (!TextUtils.isEmpty(title)) {
+ setTagSlug(ReaderUtils.sanitizeWithDashes(title));
+ } else {
+ setTagSlug(getTagSlugFromEndpoint(endpoint));
+ }
+ } else {
+ setTagSlug(slug);
+ }
+
+ setTagDisplayName(displayName);
+ setTagTitle(title);
+ setEndpoint(endpoint);
+ this.tagType = tagType;
+ }
+
+ public String getEndpoint() {
+ return StringUtils.notNullStr(endpoint);
+ }
+ private void setEndpoint(String endpoint) {
+ this.endpoint = StringUtils.notNullStr(endpoint);
+ }
+
+ public String getTagTitle() {
+ return StringUtils.notNullStr(tagTitle);
+ }
+ private void setTagTitle(String title) {
+ this.tagTitle = StringUtils.notNullStr(title);
+ }
+ private boolean hasTagTitle() {
+ return !TextUtils.isEmpty(tagTitle);
+ }
+
+ public String getTagDisplayName() {
+ return StringUtils.notNullStr(tagDisplayName);
+ }
+ private void setTagDisplayName(String displayName) {
+ this.tagDisplayName = StringUtils.notNullStr(displayName);
+ }
+
+ public String getTagSlug() {
+ return StringUtils.notNullStr(tagSlug);
+ }
+ private void setTagSlug(String slug) {
+ this.tagSlug = StringUtils.notNullStr(slug);
+ }
+
+ /*
+ * returns the tag name for use in the application log - if this is a default tag it returns
+ * the full tag name, otherwise it abbreviates the tag name since exposing followed tags
+ * in the log could be considered a privacy issue
+ */
+ public String getTagNameForLog() {
+ String tagSlug = getTagSlug();
+ if (tagType == ReaderTagType.DEFAULT) {
+ return tagSlug;
+ } else if (tagSlug.length() >= 6) {
+ return tagSlug.substring(0, 3) + "...";
+ } else if (tagSlug.length() >= 4) {
+ return tagSlug.substring(0, 2) + "...";
+ } else if (tagSlug.length() >= 2) {
+ return tagSlug.substring(0, 1) + "...";
+ } else {
+ return "...";
+ }
+ }
+
+ /*
+ * used to ensure a tag name is valid before adding it
+ */
+ private static final Pattern INVALID_CHARS = Pattern.compile("^.*[~#@*+%{}<>\\[\\]|\"\\_].*$");
+ public static boolean isValidTagName(String tagName) {
+ return !TextUtils.isEmpty(tagName)
+ && !INVALID_CHARS.matcher(tagName).matches();
+ }
+
+ /*
+ * extracts the tag slug from a valid read/tags/[tagSlug]/posts endpoint
+ */
+ private static String getTagSlugFromEndpoint(final String endpoint) {
+ if (TextUtils.isEmpty(endpoint))
+ return "";
+
+ // make sure passed endpoint is valid
+ if (!endpoint.endsWith("/posts"))
+ return "";
+ int start = endpoint.indexOf("/read/tags/");
+ if (start == -1)
+ return "";
+
+ // skip "/read/tags/" then find the next "/"
+ start += 11;
+ int end = endpoint.indexOf("/", start);
+ if (end == -1)
+ return "";
+
+ return endpoint.substring(start, end);
+ }
+
+ /*
+ * is the passed string one of the default tags?
+ */
+ public static boolean isDefaultTagTitle(String title) {
+ if (TextUtils.isEmpty(title)) {
+ return false;
+ }
+ return (title.equalsIgnoreCase(TAG_TITLE_FOLLOWED_SITES)
+ || title.equalsIgnoreCase(TAG_TITLE_DISCOVER)
+ || title.equalsIgnoreCase(TAG_TITLE_LIKED));
+ }
+
+ public static boolean isSameTag(ReaderTag tag1, ReaderTag tag2) {
+ if (tag1 == null || tag2 == null) {
+ return false;
+ }
+ return tag1.tagType == tag2.tagType
+ && tag1.getTagSlug().equalsIgnoreCase(tag2.getTagSlug());
+ }
+
+ public boolean isPostsILike() {
+ return tagType == ReaderTagType.DEFAULT && getEndpoint().endsWith("/read/liked");
+ }
+
+ public boolean isFollowedSites() {
+ return tagType == ReaderTagType.DEFAULT && getEndpoint().endsWith("/read/following");
+ }
+
+ public boolean isDiscover() {
+ return tagType == ReaderTagType.DEFAULT && getTagSlug().equals(TAG_TITLE_DISCOVER);
+ }
+
+ public boolean isTagTopic() {
+ String endpoint = getEndpoint();
+ return endpoint.toLowerCase().contains("/read/tags/");
+ }
+ public boolean isListTopic() {
+ String endpoint = getEndpoint();
+ return endpoint.toLowerCase().contains("/read/list/");
+ }
+
+ /*
+ * the label is the text displayed in the dropdown filter
+ */
+ @Override
+ public String getLabel() {
+ if (tagType == ReaderTagType.DEFAULT) {
+ return getTagTitle();
+ } else if (isTagDisplayNameAlphaNumeric()) {
+ return getTagDisplayName().toLowerCase();
+ } else if (hasTagTitle()) {
+ return getTagTitle();
+ } else {
+ return getTagDisplayName();
+ }
+ }
+
+ /*
+ * returns true if the tag display name contains only alpha-numeric characters or hyphens
+ */
+ private boolean isTagDisplayNameAlphaNumeric() {
+ if (TextUtils.isEmpty(tagDisplayName)) {
+ return false;
+ }
+
+ for (int i=0; i < tagDisplayName.length(); i++) {
+ char c = tagDisplayName.charAt(i);
+ if (!Character.isLetterOrDigit(c) && c != '-') {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object object){
+ if (object instanceof ReaderTag) {
+ ReaderTag tag = (ReaderTag) object;
+ return (tag.tagType == this.tagType && tag.getLabel().equals(this.getLabel()));
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagList.java
new file mode 100644
index 000000000..87464b722
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagList.java
@@ -0,0 +1,69 @@
+package org.wordpress.android.models;
+
+import java.util.ArrayList;
+
+public class ReaderTagList extends ArrayList<ReaderTag> {
+
+ public int indexOfTagName(String tagName) {
+ if (tagName == null || isEmpty()) {
+ return -1;
+ }
+
+ for (int i = 0; i < size(); i++) {
+ if (tagName.equals(this.get(i).getTagSlug())) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private int indexOfTag(ReaderTag tag) {
+ if (tag == null || isEmpty()) {
+ return -1;
+ }
+
+ for (int i = 0; i < this.size(); i++) {
+ if (ReaderTag.isSameTag(tag, this.get(i))) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ public boolean isSameList(ReaderTagList otherList) {
+ if (otherList == null || otherList.size() != this.size()) {
+ return false;
+ }
+
+ for (ReaderTag otherTag: otherList) {
+ int i = this.indexOfTag(otherTag);
+ if (i == -1) {
+ return false;
+ } else if (!otherTag.getEndpoint().equals(this.get(i).getEndpoint())) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /*
+ * returns a list of tags that are in this list but not in the passed list
+ */
+ public ReaderTagList getDeletions(ReaderTagList otherList) {
+ ReaderTagList deletions = new ReaderTagList();
+ if (otherList == null) {
+ return deletions;
+ }
+
+ for (ReaderTag thisTag: this) {
+ if (otherList.indexOfTag(thisTag) == -1) {
+ deletions.add(thisTag);
+ }
+ }
+
+ return deletions;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java
new file mode 100644
index 000000000..5075e81d3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java
@@ -0,0 +1,45 @@
+package org.wordpress.android.models;
+
+public enum ReaderTagType {
+ FOLLOWED,
+ DEFAULT,
+ RECOMMENDED,
+ CUSTOM_LIST,
+ SEARCH;
+
+ private static final int INT_DEFAULT = 0;
+ private static final int INT_FOLLOWED = 1;
+ private static final int INT_RECOMMENDED = 2;
+ private static final int INT_CUSTOM_LIST = 3;
+ private static final int INT_SEARCH = 4;
+
+ public static ReaderTagType fromInt(int value) {
+ switch (value) {
+ case INT_RECOMMENDED :
+ return RECOMMENDED;
+ case INT_FOLLOWED :
+ return FOLLOWED;
+ case INT_CUSTOM_LIST:
+ return CUSTOM_LIST;
+ case INT_SEARCH:
+ return SEARCH;
+ default :
+ return DEFAULT;
+ }
+ }
+
+ public int toInt() {
+ switch (this) {
+ case FOLLOWED:
+ return INT_FOLLOWED;
+ case RECOMMENDED:
+ return INT_RECOMMENDED;
+ case CUSTOM_LIST:
+ return INT_CUSTOM_LIST;
+ case SEARCH:
+ return INT_SEARCH;
+ default :
+ return INT_DEFAULT;
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderUrlList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderUrlList.java
new file mode 100644
index 000000000..17c97f1eb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderUrlList.java
@@ -0,0 +1,36 @@
+package org.wordpress.android.models;
+
+import org.wordpress.android.util.UrlUtils;
+
+import java.util.HashSet;
+
+/**
+ * URLs are normalized before being added and during comparison to ensure better comparison
+ * of URLs that may be different strings but point to the same URL
+ */
+public class ReaderUrlList extends HashSet<String> {
+ @Override
+ public boolean add(String url) {
+ return super.add(UrlUtils.normalizeUrl(url));
+ }
+
+ @Override
+ public boolean remove(Object object) {
+ if (object instanceof String) {
+ return super.remove(UrlUtils.normalizeUrl((String) object));
+ } else {
+ return super.remove(object);
+ }
+ }
+
+ @Override
+ public boolean contains(Object object) {
+ if (object instanceof String) {
+ return super.contains(UrlUtils.normalizeUrl((String) object));
+ } else {
+ return super.contains(object);
+ }
+ }
+
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderUser.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderUser.java
new file mode 100644
index 000000000..abd905df1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderUser.java
@@ -0,0 +1,121 @@
+package org.wordpress.android.models;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+
+public class ReaderUser {
+ public long userId;
+ public long blogId;
+ private String userName;
+ private String displayName;
+ private String url;
+ private String profileUrl;
+ private String avatarUrl;
+
+ public static ReaderUser fromJson(JSONObject json) {
+ ReaderUser user = new ReaderUser();
+ if (json==null)
+ return user;
+
+ user.userId = json.optLong("ID");
+ user.blogId = json.optLong("site_ID");
+
+ user.userName = JSONUtils.getString(json, "username");
+ user.url = JSONUtils.getString(json, "URL"); // <-- this isn't necessarily a wp blog
+ user.profileUrl = JSONUtils.getString(json, "profile_URL");
+ user.avatarUrl = JSONUtils.getString(json, "avatar_URL");
+
+ // "me" api call (current user) has "display_name", others have "name"
+ if (json.has("display_name")) {
+ user.displayName = JSONUtils.getStringDecoded(json, "display_name");
+ } else {
+ user.displayName = JSONUtils.getStringDecoded(json, "name");
+ }
+
+ return user;
+ }
+
+ public String getUserName() {
+ return StringUtils.notNullStr(userName);
+ }
+ public void setUserName(String userName) {
+ this.userName = StringUtils.notNullStr(userName);
+ }
+
+ public String getDisplayName() {
+ return StringUtils.notNullStr(displayName);
+ }
+ public void setDisplayName(String displayName) {
+ this.displayName = StringUtils.notNullStr(displayName);
+ }
+
+ public String getUrl() {
+ return StringUtils.notNullStr(url);
+ }
+ public void setUrl(String url) {
+ this.url = StringUtils.notNullStr(url);
+ }
+
+ public String getProfileUrl() {
+ return StringUtils.notNullStr(profileUrl);
+ }
+ public void setProfileUrl(String profileUrl) {
+ this.profileUrl = StringUtils.notNullStr(profileUrl);
+ }
+
+ public String getAvatarUrl() {
+ return StringUtils.notNullStr(avatarUrl);
+ }
+ public void setAvatarUrl(String avatarUrl) {
+ this.avatarUrl = StringUtils.notNullStr(avatarUrl);
+ }
+
+ public boolean hasUrl() {
+ return !TextUtils.isEmpty(url);
+ }
+
+ public boolean hasAvatarUrl() {
+ return !TextUtils.isEmpty(avatarUrl);
+ }
+
+ public boolean hasBlogId() {
+ return (blogId != 0);
+ }
+
+ /*
+ * not stored - used by ReaderUserAdapter for performance
+ */
+ private transient String urlDomain;
+ public String getUrlDomain() {
+ if (urlDomain == null) {
+ if (hasUrl()) {
+ urlDomain = UrlUtils.getHost(getUrl());
+ } else {
+ urlDomain = "";
+ }
+ }
+ return urlDomain;
+ }
+
+ public boolean isSameUser(ReaderUser user) {
+ if (user == null)
+ return false;
+ if (this.userId != user.userId)
+ return false;
+ if (!this.getAvatarUrl().equals(user.getAvatarUrl()))
+ return false;
+ if (!this.getDisplayName().equals(user.getDisplayName()))
+ return false;
+ if (!this.getUserName().equals(user.getUserName()))
+ return false;
+ if (!this.getUrl().equals(user.getUrl()))
+ return false;
+ if (!this.getProfileUrl().equals(user.getProfileUrl()))
+ return false;
+ return true;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderUserIdList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderUserIdList.java
new file mode 100644
index 000000000..8073e1c61
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderUserIdList.java
@@ -0,0 +1,14 @@
+package org.wordpress.android.models;
+
+import java.util.HashSet;
+
+public class ReaderUserIdList extends HashSet<Long> {
+ /*
+ * returns true if passed list contains the same userIds as this list
+ */
+ public boolean isSameList(ReaderUserIdList compareIds) {
+ if (compareIds==null || compareIds.size()!=this.size())
+ return false;
+ return this.containsAll(compareIds);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderUserList.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderUserList.java
new file mode 100644
index 000000000..cd77a99d1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderUserList.java
@@ -0,0 +1,44 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class ReaderUserList extends ArrayList<ReaderUser> {
+ /*
+ * returns all userIds in this list
+ */
+ public ReaderUserIdList getUserIds() {
+ ReaderUserIdList ids = new ReaderUserIdList();
+ for (ReaderUser user: this)
+ ids.add(user.userId);
+ return ids;
+ }
+
+ public int indexOfUserId(long userId) {
+ for (int i = 0; i < this.size(); i++) {
+ if (userId == this.get(i).userId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /*
+ * passed json is response from getting likes for a post
+ */
+ public static ReaderUserList fromJsonLikes(JSONObject json) {
+ ReaderUserList users = new ReaderUserList();
+ if (json==null)
+ return users;
+
+ JSONArray jsonLikes = json.optJSONArray("likes");
+ if (jsonLikes!=null) {
+ for (int i=0; i < jsonLikes.length(); i++)
+ users.add(ReaderUser.fromJson(jsonLikes.optJSONObject(i)));
+ }
+
+ return users;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Role.java b/WordPress/src/main/java/org/wordpress/android/models/Role.java
new file mode 100644
index 000000000..4266ba561
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Role.java
@@ -0,0 +1,102 @@
+package org.wordpress.android.models;
+
+import android.support.annotation.StringRes;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.CrashlyticsUtils;
+
+public enum Role {
+ ADMIN(R.string.role_admin),
+ EDITOR(R.string.role_editor),
+ AUTHOR(R.string.role_author),
+ CONTRIBUTOR(R.string.role_contributor),
+ FOLLOWER(R.string.role_follower),
+ VIEWER(R.string.role_viewer);
+
+ private final int mLabelResId;
+
+ Role(@StringRes int labelResId) {
+ mLabelResId = labelResId;
+ }
+
+ public String toDisplayString() {
+ return WordPress.getContext().getString(mLabelResId);
+ }
+
+ public static Role fromString(String role) {
+ switch (role) {
+ case "administrator":
+ return ADMIN;
+ case "editor":
+ return EDITOR;
+ case "author":
+ return AUTHOR;
+ case "contributor":
+ return CONTRIBUTOR;
+ case "follower":
+ return FOLLOWER;
+ case "viewer":
+ return VIEWER;
+ }
+ Exception e = new IllegalArgumentException("All roles must be handled: " + role);
+ CrashlyticsUtils.logException(e, CrashlyticsUtils.ExceptionType.SPECIFIC, AppLog.T.PEOPLE);
+
+ // All roles should have been handled, but in case an edge case occurs,
+ // using "Contributor" role is the safest option
+ return CONTRIBUTOR;
+ }
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case ADMIN:
+ return "administrator";
+ case EDITOR:
+ return "editor";
+ case AUTHOR:
+ return "author";
+ case CONTRIBUTOR:
+ return "contributor";
+ case FOLLOWER:
+ return "follower";
+ case VIEWER:
+ return "viewer";
+ }
+ throw new IllegalArgumentException("All roles must be handled");
+ }
+
+ /**
+ * @return the string representation of the role, as used by the REST API
+ */
+ public String toRESTString() {
+ switch (this) {
+ case ADMIN:
+ return "administrator";
+ case EDITOR:
+ return "editor";
+ case AUTHOR:
+ return "author";
+ case CONTRIBUTOR:
+ return "contributor";
+ case FOLLOWER:
+ return "follower";
+ case VIEWER:
+ // the remote expects "follower" as the role parameter even if the role is "viewer"
+ return "follower";
+ }
+ throw new IllegalArgumentException("All roles must be handled");
+ }
+
+ public static Role[] userRoles() {
+ return new Role[] { ADMIN, EDITOR, AUTHOR, CONTRIBUTOR };
+ }
+
+ public static Role[] inviteRoles(boolean isPrivateSite) {
+ if (isPrivateSite) {
+ return new Role[] { VIEWER, ADMIN, EDITOR, AUTHOR, CONTRIBUTOR };
+ }
+ return new Role[] { FOLLOWER, ADMIN, EDITOR, AUTHOR, CONTRIBUTOR };
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/SiteSettingsModel.java b/WordPress/src/main/java/org/wordpress/android/models/SiteSettingsModel.java
new file mode 100644
index 000000000..16f905329
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/SiteSettingsModel.java
@@ -0,0 +1,418 @@
+package org.wordpress.android.models;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Holds blog settings and provides methods to (de)serialize .com and self-hosted network calls.
+ */
+
+public class SiteSettingsModel {
+ public static final int RELATED_POSTS_ENABLED_FLAG = 0x1;
+ public static final int RELATED_POST_HEADER_FLAG = 0x2;
+ public static final int RELATED_POST_IMAGE_FLAG = 0x4;
+
+ // Settings table column names
+ public static final String ID_COLUMN_NAME = "id";
+ public static final String ADDRESS_COLUMN_NAME = "address";
+ public static final String USERNAME_COLUMN_NAME = "username";
+ public static final String PASSWORD_COLUMN_NAME = "password";
+ public static final String TITLE_COLUMN_NAME = "title";
+ public static final String TAGLINE_COLUMN_NAME = "tagline";
+ public static final String LANGUAGE_COLUMN_NAME = "language";
+ public static final String PRIVACY_COLUMN_NAME = "privacy";
+ public static final String LOCATION_COLUMN_NAME = "location";
+ public static final String DEF_CATEGORY_COLUMN_NAME = "defaultCategory";
+ public static final String DEF_POST_FORMAT_COLUMN_NAME = "defaultPostFormat";
+ public static final String CATEGORIES_COLUMN_NAME = "categories";
+ public static final String POST_FORMATS_COLUMN_NAME = "postFormats";
+ public static final String CREDS_VERIFIED_COLUMN_NAME = "credsVerified";
+ public static final String RELATED_POSTS_COLUMN_NAME = "relatedPosts";
+ public static final String ALLOW_COMMENTS_COLUMN_NAME = "allowComments";
+ public static final String SEND_PINGBACKS_COLUMN_NAME = "sendPingbacks";
+ public static final String RECEIVE_PINGBACKS_COLUMN_NAME = "receivePingbacks";
+ public static final String SHOULD_CLOSE_AFTER_COLUMN_NAME = "shouldCloseAfter";
+ public static final String CLOSE_AFTER_COLUMN_NAME = "closeAfter";
+ public static final String SORT_BY_COLUMN_NAME = "sortBy";
+ public static final String SHOULD_THREAD_COLUMN_NAME = "shouldThread";
+ public static final String THREADING_COLUMN_NAME = "threading";
+ public static final String SHOULD_PAGE_COLUMN_NAME = "shouldPage";
+ public static final String PAGING_COLUMN_NAME = "paging";
+ public static final String MANUAL_APPROVAL_COLUMN_NAME = "manualApproval";
+ public static final String IDENTITY_REQUIRED_COLUMN_NAME = "identityRequired";
+ public static final String USER_ACCOUNT_REQUIRED_COLUMN_NAME = "userAccountRequired";
+ public static final String WHITELIST_COLUMN_NAME = "whitelist";
+ public static final String MODERATION_KEYS_COLUMN_NAME = "moderationKeys";
+ public static final String BLACKLIST_KEYS_COLUMN_NAME = "blacklistKeys";
+
+ public static final String SETTINGS_TABLE_NAME = "site_settings";
+ public static final String CREATE_SETTINGS_TABLE_SQL =
+ "CREATE TABLE IF NOT EXISTS " +
+ SETTINGS_TABLE_NAME +
+ " (" +
+ ID_COLUMN_NAME + " INTEGER PRIMARY KEY, " +
+ ADDRESS_COLUMN_NAME + " TEXT, " +
+ USERNAME_COLUMN_NAME + " TEXT, " +
+ PASSWORD_COLUMN_NAME + " TEXT, " +
+ TITLE_COLUMN_NAME + " TEXT, " +
+ TAGLINE_COLUMN_NAME + " TEXT, " +
+ LANGUAGE_COLUMN_NAME + " INTEGER, " +
+ PRIVACY_COLUMN_NAME + " INTEGER, " +
+ LOCATION_COLUMN_NAME + " BOOLEAN, " +
+ DEF_CATEGORY_COLUMN_NAME + " TEXT, " +
+ DEF_POST_FORMAT_COLUMN_NAME + " TEXT, " +
+ CATEGORIES_COLUMN_NAME + " TEXT, " +
+ POST_FORMATS_COLUMN_NAME + " TEXT, " +
+ CREDS_VERIFIED_COLUMN_NAME + " BOOLEAN, " +
+ RELATED_POSTS_COLUMN_NAME + " INTEGER, " +
+ ALLOW_COMMENTS_COLUMN_NAME + " BOOLEAN, " +
+ SEND_PINGBACKS_COLUMN_NAME + " BOOLEAN, " +
+ RECEIVE_PINGBACKS_COLUMN_NAME + " BOOLEAN, " +
+ SHOULD_CLOSE_AFTER_COLUMN_NAME + " BOOLEAN, " +
+ CLOSE_AFTER_COLUMN_NAME + " INTEGER, " +
+ SORT_BY_COLUMN_NAME + " INTEGER, " +
+ SHOULD_THREAD_COLUMN_NAME + " BOOLEAN, " +
+ THREADING_COLUMN_NAME + " INTEGER, " +
+ SHOULD_PAGE_COLUMN_NAME + " BOOLEAN, " +
+ PAGING_COLUMN_NAME + " INTEGER, " +
+ MANUAL_APPROVAL_COLUMN_NAME + " BOOLEAN, " +
+ IDENTITY_REQUIRED_COLUMN_NAME + " BOOLEAN, " +
+ USER_ACCOUNT_REQUIRED_COLUMN_NAME + " BOOLEAN, " +
+ WHITELIST_COLUMN_NAME + " BOOLEAN, " +
+ MODERATION_KEYS_COLUMN_NAME + " TEXT, " +
+ BLACKLIST_KEYS_COLUMN_NAME + " TEXT" +
+ ");";
+
+ public boolean isInLocalTable;
+ public boolean hasVerifiedCredentials;
+ public long localTableId;
+ public String address;
+ public String username;
+ public String password;
+ public String title;
+ public String tagline;
+ public String language;
+ public int languageId;
+ public int privacy;
+ public boolean location;
+ public int defaultCategory;
+ public CategoryModel[] categories;
+ public String defaultPostFormat;
+ public Map<String, String> postFormats;
+ public boolean showRelatedPosts;
+ public boolean showRelatedPostHeader;
+ public boolean showRelatedPostImages;
+ public boolean allowComments;
+ public boolean sendPingbacks;
+ public boolean receivePingbacks;
+ public boolean shouldCloseAfter;
+ public int closeCommentAfter;
+ public int sortCommentsBy;
+ public boolean shouldThreadComments;
+ public int threadingLevels;
+ public boolean shouldPageComments;
+ public int commentsPerPage;
+ public boolean commentApprovalRequired;
+ public boolean commentsRequireIdentity;
+ public boolean commentsRequireUserAccount;
+ public boolean commentAutoApprovalKnownUsers;
+ public int maxLinks;
+ public List<String> holdForModeration;
+ public List<String> blacklist;
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof SiteSettingsModel)) return false;
+ SiteSettingsModel otherModel = (SiteSettingsModel) other;
+
+ return localTableId == otherModel.localTableId &&
+ address.equals(otherModel.address) &&
+ username.equals(otherModel.username) &&
+ password.equals(otherModel.password) &&
+ title.equals(otherModel.title) &&
+ tagline.equals(otherModel.tagline) &&
+ languageId == otherModel.languageId &&
+ privacy == otherModel.privacy &&
+ location == otherModel.location &&
+ defaultPostFormat.equals(otherModel.defaultPostFormat) &&
+ defaultCategory == otherModel.defaultCategory &&
+ showRelatedPosts == otherModel.showRelatedPosts &&
+ showRelatedPostHeader == otherModel.showRelatedPostHeader &&
+ showRelatedPostImages == otherModel.showRelatedPostImages &&
+ allowComments == otherModel.allowComments &&
+ sendPingbacks == otherModel.sendPingbacks &&
+ receivePingbacks == otherModel.receivePingbacks &&
+ closeCommentAfter == otherModel.closeCommentAfter &&
+ sortCommentsBy == otherModel.sortCommentsBy &&
+ threadingLevels == otherModel.threadingLevels &&
+ commentsPerPage == otherModel.commentsPerPage &&
+ commentApprovalRequired == otherModel.commentApprovalRequired &&
+ commentsRequireIdentity == otherModel.commentsRequireIdentity &&
+ commentsRequireUserAccount == otherModel.commentsRequireUserAccount &&
+ commentAutoApprovalKnownUsers == otherModel.commentAutoApprovalKnownUsers &&
+ maxLinks == otherModel.maxLinks &&
+ holdForModeration != null && holdForModeration.equals(otherModel.holdForModeration) &&
+ blacklist != null && blacklist.equals(otherModel.blacklist);
+ }
+
+ /**
+ * Copies data from another {@link SiteSettingsModel}.
+ */
+ public void copyFrom(SiteSettingsModel other) {
+ if (other == null) return;
+
+ isInLocalTable = other.isInLocalTable;
+ hasVerifiedCredentials = other.hasVerifiedCredentials;
+ localTableId = other.localTableId;
+ address = other.address;
+ username = other.username;
+ password = other.password;
+ title = other.title;
+ tagline = other.tagline;
+ language = other.language;
+ languageId = other.languageId;
+ privacy = other.privacy;
+ location = other.location;
+ defaultCategory = other.defaultCategory;
+ categories = other.categories;
+ defaultPostFormat = other.defaultPostFormat;
+ postFormats = other.postFormats;
+ showRelatedPosts = other.showRelatedPosts;
+ showRelatedPostHeader = other.showRelatedPostHeader;
+ showRelatedPostImages = other.showRelatedPostImages;
+ allowComments = other.allowComments;
+ sendPingbacks = other.sendPingbacks;
+ receivePingbacks = other.receivePingbacks;
+ shouldCloseAfter = other.shouldCloseAfter;
+ closeCommentAfter = other.closeCommentAfter;
+ sortCommentsBy = other.sortCommentsBy;
+ shouldThreadComments = other.shouldThreadComments;
+ threadingLevels = other.threadingLevels;
+ shouldPageComments = other.shouldPageComments;
+ commentsPerPage = other.commentsPerPage;
+ commentApprovalRequired = other.commentApprovalRequired;
+ commentsRequireIdentity = other.commentsRequireIdentity;
+ commentsRequireUserAccount = other.commentsRequireUserAccount;
+ commentAutoApprovalKnownUsers = other.commentAutoApprovalKnownUsers;
+ maxLinks = other.maxLinks;
+ if (other.holdForModeration != null) {
+ holdForModeration = new ArrayList<>(other.holdForModeration);
+ }
+ if (other.blacklist != null) {
+ blacklist = new ArrayList<>(other.blacklist);
+ }
+ }
+
+ /**
+ * Sets values from a local database {@link Cursor}.
+ */
+ public void deserializeOptionsDatabaseCursor(Cursor cursor, Map<Integer, CategoryModel> models) {
+ if (cursor == null || !cursor.moveToFirst() || cursor.getCount() == 0) return;
+
+ localTableId = getIntFromCursor(cursor, ID_COLUMN_NAME);
+ address = getStringFromCursor(cursor, ADDRESS_COLUMN_NAME);
+ username = getStringFromCursor(cursor, USERNAME_COLUMN_NAME);
+ password = getStringFromCursor(cursor, PASSWORD_COLUMN_NAME);
+ title = getStringFromCursor(cursor, TITLE_COLUMN_NAME);
+ tagline = getStringFromCursor(cursor, TAGLINE_COLUMN_NAME);
+ languageId = getIntFromCursor(cursor, LANGUAGE_COLUMN_NAME);
+ privacy = getIntFromCursor(cursor, PRIVACY_COLUMN_NAME);
+ defaultCategory = getIntFromCursor(cursor, DEF_CATEGORY_COLUMN_NAME);
+ defaultPostFormat = getStringFromCursor(cursor, DEF_POST_FORMAT_COLUMN_NAME);
+ location = getBooleanFromCursor(cursor, LOCATION_COLUMN_NAME);
+ hasVerifiedCredentials = getBooleanFromCursor(cursor, CREDS_VERIFIED_COLUMN_NAME);
+ allowComments = getBooleanFromCursor(cursor, ALLOW_COMMENTS_COLUMN_NAME);
+ sendPingbacks = getBooleanFromCursor(cursor, SEND_PINGBACKS_COLUMN_NAME);
+ receivePingbacks = getBooleanFromCursor(cursor, RECEIVE_PINGBACKS_COLUMN_NAME);
+ shouldCloseAfter = getBooleanFromCursor(cursor, SHOULD_CLOSE_AFTER_COLUMN_NAME);
+ closeCommentAfter = getIntFromCursor(cursor, CLOSE_AFTER_COLUMN_NAME);
+ sortCommentsBy = getIntFromCursor(cursor, SORT_BY_COLUMN_NAME);
+ shouldThreadComments = getBooleanFromCursor(cursor, SHOULD_THREAD_COLUMN_NAME);
+ threadingLevels = getIntFromCursor(cursor, THREADING_COLUMN_NAME);
+ shouldPageComments = getBooleanFromCursor(cursor, SHOULD_PAGE_COLUMN_NAME);
+ commentsPerPage = getIntFromCursor(cursor, PAGING_COLUMN_NAME);
+ commentApprovalRequired = getBooleanFromCursor(cursor, MANUAL_APPROVAL_COLUMN_NAME);
+ commentsRequireIdentity = getBooleanFromCursor(cursor, IDENTITY_REQUIRED_COLUMN_NAME);
+ commentsRequireUserAccount = getBooleanFromCursor(cursor, USER_ACCOUNT_REQUIRED_COLUMN_NAME);
+ commentAutoApprovalKnownUsers = getBooleanFromCursor(cursor, WHITELIST_COLUMN_NAME);
+
+ String moderationKeys = getStringFromCursor(cursor, MODERATION_KEYS_COLUMN_NAME);
+ String blacklistKeys = getStringFromCursor(cursor, BLACKLIST_KEYS_COLUMN_NAME);
+ holdForModeration = new ArrayList<>();
+ blacklist = new ArrayList<>();
+ if (!TextUtils.isEmpty(moderationKeys)) {
+ Collections.addAll(holdForModeration, moderationKeys.split("\n"));
+ }
+ if (!TextUtils.isEmpty(blacklistKeys)) {
+ Collections.addAll(blacklist, blacklistKeys.split("\n"));
+ }
+
+ setRelatedPostsFlags(Math.max(0, getIntFromCursor(cursor, RELATED_POSTS_COLUMN_NAME)));
+
+ String cachedCategories = getStringFromCursor(cursor, CATEGORIES_COLUMN_NAME);
+ String cachedFormats = getStringFromCursor(cursor, POST_FORMATS_COLUMN_NAME);
+ if (models != null && !TextUtils.isEmpty(cachedCategories)) {
+ String[] split = cachedCategories.split(",");
+ categories = new CategoryModel[split.length];
+ for (int i = 0; i < split.length; ++i) {
+ int catId = Integer.parseInt(split[i]);
+ categories[i] = models.get(catId);
+ }
+ }
+ if (!TextUtils.isEmpty(cachedFormats)) {
+ String[] split = cachedFormats.split(";");
+ postFormats = new HashMap<>();
+ for (String format : split) {
+ String[] kvp = format.split(",");
+ postFormats.put(kvp[0], kvp[1]);
+ }
+ }
+
+ int cachedRelatedPosts = getIntFromCursor(cursor, RELATED_POSTS_COLUMN_NAME);
+ if (cachedRelatedPosts != -1) {
+ setRelatedPostsFlags(cachedRelatedPosts);
+ }
+
+ isInLocalTable = true;
+ }
+
+ /**
+ * Creates the {@link ContentValues} object to store this category data in a local database.
+ */
+ public ContentValues serializeToDatabase() {
+ ContentValues values = new ContentValues();
+ values.put(ID_COLUMN_NAME, localTableId);
+ values.put(ADDRESS_COLUMN_NAME, address);
+ values.put(USERNAME_COLUMN_NAME, username);
+ values.put(PASSWORD_COLUMN_NAME, password);
+ values.put(TITLE_COLUMN_NAME, title);
+ values.put(TAGLINE_COLUMN_NAME, tagline);
+ values.put(PRIVACY_COLUMN_NAME, privacy);
+ values.put(LANGUAGE_COLUMN_NAME, languageId);
+ values.put(LOCATION_COLUMN_NAME, location);
+ values.put(DEF_CATEGORY_COLUMN_NAME, defaultCategory);
+ values.put(CATEGORIES_COLUMN_NAME, categoryIdList(categories));
+ values.put(DEF_POST_FORMAT_COLUMN_NAME, defaultPostFormat);
+ values.put(POST_FORMATS_COLUMN_NAME, postFormatList(postFormats));
+ values.put(CREDS_VERIFIED_COLUMN_NAME, hasVerifiedCredentials);
+ values.put(RELATED_POSTS_COLUMN_NAME, getRelatedPostsFlags());
+ values.put(ALLOW_COMMENTS_COLUMN_NAME, allowComments);
+ values.put(SEND_PINGBACKS_COLUMN_NAME, sendPingbacks);
+ values.put(RECEIVE_PINGBACKS_COLUMN_NAME, receivePingbacks);
+ values.put(SHOULD_CLOSE_AFTER_COLUMN_NAME, shouldCloseAfter);
+ values.put(CLOSE_AFTER_COLUMN_NAME, closeCommentAfter);
+ values.put(SORT_BY_COLUMN_NAME, sortCommentsBy);
+ values.put(SHOULD_THREAD_COLUMN_NAME, shouldThreadComments);
+ values.put(THREADING_COLUMN_NAME, threadingLevels);
+ values.put(SHOULD_PAGE_COLUMN_NAME, shouldPageComments);
+ values.put(PAGING_COLUMN_NAME, commentsPerPage);
+ values.put(MANUAL_APPROVAL_COLUMN_NAME, commentApprovalRequired);
+ values.put(IDENTITY_REQUIRED_COLUMN_NAME, commentsRequireIdentity);
+ values.put(USER_ACCOUNT_REQUIRED_COLUMN_NAME, commentsRequireUserAccount);
+ values.put(WHITELIST_COLUMN_NAME, commentAutoApprovalKnownUsers);
+
+ String moderationKeys = "";
+ if (holdForModeration != null) {
+ for (String key : holdForModeration) {
+ moderationKeys += key + "\n";
+ }
+ }
+ String blacklistKeys = "";
+ if (blacklist != null) {
+ for (String key : blacklist) {
+ blacklistKeys += key + "\n";
+ }
+ }
+ values.put(MODERATION_KEYS_COLUMN_NAME, moderationKeys);
+ values.put(BLACKLIST_KEYS_COLUMN_NAME, blacklistKeys);
+
+ return values;
+ }
+
+ public int getRelatedPostsFlags() {
+ int flags = 0;
+
+ if (showRelatedPosts) flags |= RELATED_POSTS_ENABLED_FLAG;
+ if (showRelatedPostHeader) flags |= RELATED_POST_HEADER_FLAG;
+ if (showRelatedPostImages) flags |= RELATED_POST_IMAGE_FLAG;
+
+ return flags;
+ }
+
+ public void setRelatedPostsFlags(int flags) {
+ showRelatedPosts = (flags & RELATED_POSTS_ENABLED_FLAG) > 0;
+ showRelatedPostHeader = (flags & RELATED_POST_HEADER_FLAG) > 0;
+ showRelatedPostImages = (flags & RELATED_POST_IMAGE_FLAG) > 0;
+ }
+
+ /**
+ * Used to serialize post formats to store in a local database.
+ *
+ * @param formats
+ * map of post formats where the key is the format ID and the value is the format name
+ * @return
+ * a String of semi-colon separated KVP's of Post Formats; Post Format ID -> Post Format Name
+ */
+ private static String postFormatList(Map<String, String> formats) {
+ if (formats == null || formats.size() == 0) return "";
+
+ StringBuilder builder = new StringBuilder();
+ for (String key : formats.keySet()) {
+ builder.append(key).append(",").append(formats.get(key)).append(";");
+ }
+ builder.setLength(builder.length() - 1);
+
+ return builder.toString();
+ }
+
+ /**
+ * Used to serialize categories to store in a local database.
+ *
+ * @param elements
+ * {@link CategoryModel} array to create String ID list from
+ * @return
+ * a String of comma-separated integer Category ID's
+ */
+ private static String categoryIdList(CategoryModel[] elements) {
+ if (elements == null || elements.length == 0) return "";
+
+ StringBuilder builder = new StringBuilder();
+ for (CategoryModel element : elements) {
+ builder.append(String.valueOf(element.id)).append(",");
+ }
+ builder.setLength(builder.length() - 1);
+
+ return builder.toString();
+ }
+
+ /**
+ * Helper method to get an integer value from a given column in a Cursor.
+ */
+ private int getIntFromCursor(Cursor cursor, String columnName) {
+ int columnIndex = cursor.getColumnIndex(columnName);
+ return columnIndex != -1 ? cursor.getInt(columnIndex) : -1;
+ }
+
+ /**
+ * Helper method to get a String value from a given column in a Cursor.
+ */
+ private String getStringFromCursor(Cursor cursor, String columnName) {
+ int columnIndex = cursor.getColumnIndex(columnName);
+ return columnIndex != -1 ? cursor.getString(columnIndex) : "";
+ }
+
+ /**
+ * Helper method to get a boolean value (stored as an int) from a given column in a Cursor.
+ */
+ private boolean getBooleanFromCursor(Cursor cursor, String columnName) {
+ int columnIndex = cursor.getColumnIndex(columnName);
+ return columnIndex != -1 && cursor.getInt(columnIndex) != 0;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Suggestion.java b/WordPress/src/main/java/org/wordpress/android/models/Suggestion.java
new file mode 100644
index 000000000..0d4b1d752
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Suggestion.java
@@ -0,0 +1,71 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Suggestion {
+ private static final String MENTION_TAXONOMY = "mention";
+
+ public long siteID;
+
+ private String userLogin;
+ private String displayName;
+ private String imageUrl;
+ private String taxonomy;
+
+ public Suggestion(long siteID,
+ String userLogin,
+ String displayName,
+ String imageUrl,
+ String taxonomy) {
+ this.siteID = siteID;
+ this.userLogin = userLogin;
+ this.displayName = displayName;
+ this.imageUrl = imageUrl;
+ this.taxonomy = taxonomy;
+ }
+
+ public static Suggestion fromJSON(JSONObject json, long siteID) {
+ if (json == null) {
+ return null;
+ }
+
+ String userLogin = JSONUtils.getString(json, "user_login");
+ String displayName = JSONUtils.getString(json, "display_name");
+ String imageUrl = JSONUtils.getString(json, "image_URL");
+
+ // the api currently doesn't return a taxonomy field but we want to be ready for when it does
+ return new Suggestion(siteID, userLogin, displayName, imageUrl, MENTION_TAXONOMY);
+ }
+
+ public static List<Suggestion> suggestionListFromJSON(JSONArray jsonArray, long siteID) {
+ if (jsonArray == null) {
+ return null;
+ }
+
+ ArrayList<Suggestion> suggestions = new ArrayList<Suggestion>(jsonArray.length());
+
+ for (int i = 0; i < jsonArray.length(); i++) {
+ Suggestion suggestion = Suggestion.fromJSON(jsonArray.optJSONObject(i), siteID);
+ suggestions.add(suggestion);
+ }
+
+ return suggestions;
+ }
+
+ public String getUserLogin() {
+ return StringUtils.notNullStr(userLogin);
+ }
+ public String getDisplayName() {
+ return StringUtils.notNullStr(displayName);
+ }
+ public String getImageUrl() {
+ return StringUtils.notNullStr(imageUrl);
+ }
+ public String getTaxonomy() { return StringUtils.notNullStr(taxonomy); }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Tag.java b/WordPress/src/main/java/org/wordpress/android/models/Tag.java
new file mode 100644
index 000000000..87b756854
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Tag.java
@@ -0,0 +1,51 @@
+package org.wordpress.android.models;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Tag {
+ public long siteID;
+
+ private String tag;
+
+ public Tag(long siteID,
+ String tag) {
+ this.siteID = siteID;
+ this.tag = tag;
+ }
+
+ public static Tag fromJSON(JSONObject json, long siteID) {
+ if (json == null) {
+ return null;
+ }
+
+ String tag = JSONUtils.getString(json, "name");
+
+ // the api currently doesn't return a taxonomy field but we want to be ready for when it does
+ return new Tag(siteID, tag);
+ }
+
+ public static List<Tag> tagListFromJSON(JSONArray jsonArray, long siteID) {
+ if (jsonArray == null) {
+ return null;
+ }
+
+ ArrayList<Tag> suggestions = new ArrayList<Tag>(jsonArray.length());
+
+ for (int i = 0; i < jsonArray.length(); i++) {
+ Tag suggestion = Tag.fromJSON(jsonArray.optJSONObject(i), siteID);
+ suggestions.add(suggestion);
+ }
+
+ return suggestions;
+ }
+
+ public String getTag() {
+ return StringUtils.notNullStr(tag);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Theme.java b/WordPress/src/main/java/org/wordpress/android/models/Theme.java
new file mode 100644
index 000000000..0ef8cc650
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/models/Theme.java
@@ -0,0 +1,183 @@
+package org.wordpress.android.models;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+
+public class Theme {
+ public static final String ID = "id";
+ public static final String AUTHOR = "author";
+ public static final String SCREENSHOT = "screenshot";
+ public static final String AUTHOR_URI = "author_uri";
+ public static final String DEMO_URI = "demo_uri";
+ public static final String NAME = "name";
+ public static final String STYLESHEET = "stylesheet";
+ public static final String PRICE = "price";
+ public static final String BLOG_ID = "blogId";
+ public static final String IS_CURRENT = "isCurrent";
+
+ public static final String PREVIEW_URL = "preview_url";
+ public static final String COST = "cost";
+ public static final String DISPLAY = "display";
+
+ private String mId;
+ private String mAuthor;
+ private String mScreenshot;
+ private String mAuthorURI;
+ private String mDemoURI;
+ private String mName;
+ private String mStylesheet;
+ private String mPrice;
+ private String mBlogId;
+ private boolean mIsCurrent;
+
+ public static Theme fromJSONV1_1(JSONObject object) throws JSONException {
+ if (object == null) {
+ return null;
+ } else {
+ String id = object.getString(ID);
+ String author = "";
+ String screenshot = object.getString(SCREENSHOT);
+ String authorURI = "";
+ String demoURI = object.getString(PREVIEW_URL);
+ String name = object.getString(NAME);
+ String stylesheet = "";
+ String price;
+ try {
+ JSONObject cost = object.getJSONObject(COST);
+ price = cost.getString(DISPLAY);
+ } catch (JSONException e) {
+ price = "";
+ }
+
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getRemoteBlogId());
+
+ return new Theme(id, author, screenshot, authorURI, demoURI, name, stylesheet, price, blogId, false);
+ }
+ }
+
+ public static Theme fromJSONV1_2(JSONObject object) throws JSONException {
+ if (object == null) {
+ return null;
+ } else {
+ String id = object.getString(ID);
+ String author = object.getString(AUTHOR);
+ String screenshot = object.getString(SCREENSHOT);
+ String authorURI = object.getString(AUTHOR_URI);
+ String demoURI = object.getString(DEMO_URI);
+ String name = object.getString(NAME);
+ String stylesheet = object.getString(STYLESHEET);
+ String price;
+ try {
+ price = object.getString(PRICE);
+ } catch (JSONException e) {
+ price = "";
+ }
+
+ String blogId = String.valueOf(WordPress.getCurrentBlog().getRemoteBlogId());
+
+ return new Theme(id, author, screenshot, authorURI, demoURI, name, stylesheet, price, blogId, false);
+ }
+ }
+
+ public Theme(String id, String author, String screenshot, String authorURI, String demoURI, String name, String stylesheet, String price, String blogId, boolean isCurrent) {
+ setId(id);
+ setAuthor(author);
+ setScreenshot(screenshot);
+ setAuthorURI(authorURI);
+ setDemoURI(demoURI);
+ setName(name);
+ setStylesheet(stylesheet);
+ setPrice(price);
+ setBlogId(blogId);
+ setIsCurrent(isCurrent);
+ }
+
+ public void setId(String id) {
+ mId = id;
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public void setAuthor(String author) {
+ mAuthor = author;
+ }
+
+ public String getAuthor() {
+ return mAuthor;
+ }
+
+ public String getScreenshot() {
+ return mScreenshot;
+ }
+
+ public void setScreenshot(String mScreenshot) {
+ this.mScreenshot = mScreenshot;
+ }
+
+ public String getAuthorURI() {
+ return mAuthorURI;
+ }
+
+ public void setAuthorURI(String mAuthorURI) {
+ this.mAuthorURI = mAuthorURI;
+ }
+
+ public String getDemoURI() {
+ return mDemoURI;
+ }
+
+ public void setDemoURI(String mDemoURI) {
+ this.mDemoURI = mDemoURI;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setName(String mName) {
+ this.mName = mName;
+ }
+
+ public String getStylesheet() {
+ return mStylesheet;
+ }
+
+ public void setStylesheet(String mStylesheet) {
+ this.mStylesheet = mStylesheet;
+ }
+
+ public String getPrice() {
+ return mPrice;
+ }
+
+ public void setPrice(String mPrice) {
+ this.mPrice = mPrice;
+ }
+
+ public String getBlogId() {
+ return mBlogId;
+ }
+
+ public void setBlogId(String blogId) {
+ mBlogId = blogId;
+ }
+
+ public boolean getIsCurrent() {
+ return mIsCurrent;
+ }
+
+ public void setIsCurrent(boolean isCurrent) {
+ mIsCurrent = isCurrent;
+ }
+
+ public boolean isPremium() {
+ return !mPrice.equals("");
+ }
+
+ public void save() {
+ WordPress.wpDB.saveTheme(this);
+ }
+}