aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/ui/reader/actions
diff options
context:
space:
mode:
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/reader/actions')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderActions.java90
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderBlogActions.java477
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderCommentActions.java181
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java359
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java168
5 files changed, 1275 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderActions.java
new file mode 100644
index 000000000..5dd01edef
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderActions.java
@@ -0,0 +1,90 @@
+package org.wordpress.android.ui.reader.actions;
+
+import org.wordpress.android.models.ReaderBlog;
+import org.wordpress.android.models.ReaderComment;
+
+/**
+ * classes in this package serve as a middleman between local data and server data - used by
+ * reader activities/fragments/adapters that wish to perform actions on posts, blogs & topics,
+ * or wish to get the latest data from the server.
+ *
+ * methods in this package which change state (like, follow, etc.) are generally optimistic
+ * and work like this:
+ *
+ * 1. caller asks method to send a network request which changes state
+ * 2. method changes state in local data and returns to caller *before* network request completes
+ * 3. caller can access local state change without waiting for the network request
+ * 4. if the network request fails, the method restores the previous state of the local data
+ * 5. if caller passes a listener, it can be alerted to the actual success/failure of the request
+ *
+ * note that all methods MUST be called from the UI thread in order to guarantee that listeners
+ * are alerted on the UI thread
+ */
+public class ReaderActions {
+
+ private ReaderActions() {
+ throw new AssertionError();
+ }
+
+ /*
+ * listener when a specific action is performed (liking a post, etc.)
+ */
+ public interface ActionListener {
+ void onActionResult(boolean succeeded);
+ }
+
+ /*
+ * helper routine for telling an action listener the call succeeded or failed w/o having to null check
+ */
+ public static void callActionListener(ActionListener actionListener, boolean succeeded) {
+ if (actionListener != null) {
+ actionListener.onActionResult(succeeded);
+ }
+ }
+
+ /*
+ * listener when the failure status code is required
+ */
+ public interface OnRequestListener {
+ void onSuccess();
+ void onFailure(int statusCode);
+ }
+
+ /*
+ * listener when submitting a comment
+ */
+ public interface CommentActionListener {
+ void onActionResult(boolean succeeded, ReaderComment newComment);
+ }
+
+ /*
+ * result when updating data (getting latest posts or comments for a post, etc.)
+ */
+ public enum UpdateResult {
+ HAS_NEW, // new posts/comments/etc. have been retrieved
+ CHANGED, // no new posts/comments, but existing ones have changed
+ UNCHANGED, // no new or changed posts/comments
+ FAILED; // request failed
+ public boolean isNewOrChanged() {
+ return (this == HAS_NEW || this == CHANGED);
+ }
+ }
+ public interface UpdateResultListener {
+ void onUpdateResult(UpdateResult result);
+ }
+
+ /*
+ * used by adapters to notify when more data should be loaded
+ */
+ public interface DataRequestedListener {
+ void onRequestData();
+ }
+
+ /*
+ * used by blog preview when requesting latest info about a blog
+ */
+ public interface UpdateBlogInfoListener {
+ void onResult(ReaderBlog blogInfo);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderBlogActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderBlogActions.java
new file mode 100644
index 000000000..6398b1e07
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderBlogActions.java
@@ -0,0 +1,477 @@
+package org.wordpress.android.ui.reader.actions;
+
+import android.text.TextUtils;
+
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.StringRequest;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.ReaderBlogTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.ReaderBlog;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostList;
+import org.wordpress.android.ui.reader.actions.ReaderActions.ActionListener;
+import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateBlogInfoListener;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.VolleyUtils;
+
+import java.net.HttpURLConnection;
+
+public class ReaderBlogActions {
+
+ public static class BlockedBlogResult {
+ public long blogId;
+ public ReaderPostList deletedPosts;
+ public boolean wasFollowing;
+ }
+
+ private static String jsonToString(JSONObject json) {
+ return (json != null ? json.toString() : "");
+ }
+
+ public static boolean followBlogById(final long blogId,
+ final boolean isAskingToFollow,
+ final ActionListener actionListener) {
+ if (blogId == 0) {
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ return false;
+ }
+
+ ReaderBlogTable.setIsFollowedBlogId(blogId, isAskingToFollow);
+ ReaderPostTable.setFollowStatusForPostsInBlog(blogId, isAskingToFollow);
+
+ if (isAskingToFollow) {
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.READER_BLOG_FOLLOWED, blogId);
+ } else {
+ AnalyticsUtils.trackWithBlogDetails(AnalyticsTracker.Stat.READER_BLOG_UNFOLLOWED, blogId);
+ }
+
+ final String actionName = (isAskingToFollow ? "follow" : "unfollow");
+ final String path = "sites/" + blogId + "/follows/" + (isAskingToFollow ? "new" : "mine/delete");
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ boolean success = isFollowActionSuccessful(jsonObject, isAskingToFollow);
+ if (success) {
+ AppLog.d(T.READER, "blog " + actionName + " succeeded");
+ } else {
+ AppLog.w(T.READER, "blog " + actionName + " failed - " + jsonToString(jsonObject) + " - " + path);
+ localRevertFollowBlogId(blogId, isAskingToFollow);
+ }
+ if (actionListener != null) {
+ actionListener.onActionResult(success);
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.w(T.READER, "blog " + actionName + " failed with error");
+ AppLog.e(T.READER, volleyError);
+ localRevertFollowBlogId(blogId, isAskingToFollow);
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ }
+ };
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+
+ return true;
+ }
+
+ public static boolean followFeedById(final long feedId,
+ final boolean isAskingToFollow,
+ final ActionListener actionListener) {
+ ReaderBlog blogInfo = ReaderBlogTable.getFeedInfo(feedId);
+ if (blogInfo != null) {
+ return internalFollowFeed(blogInfo.feedId, blogInfo.getFeedUrl(), isAskingToFollow, actionListener);
+ }
+
+ updateFeedInfo(feedId, null, new UpdateBlogInfoListener() {
+ @Override
+ public void onResult(ReaderBlog blogInfo) {
+ if (blogInfo != null) {
+ internalFollowFeed(
+ blogInfo.feedId,
+ blogInfo.getFeedUrl(),
+ isAskingToFollow,
+ actionListener);
+ } else if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ }
+ });
+
+ return true;
+ }
+
+ public static void followFeedByUrl(final String feedUrl,
+ final ActionListener actionListener) {
+ if (TextUtils.isEmpty(feedUrl)) {
+ ReaderActions.callActionListener(actionListener, false);
+ return;
+ }
+
+ // use existing blog info if we can
+ ReaderBlog blogInfo = ReaderBlogTable.getFeedInfo(ReaderBlogTable.getFeedIdFromUrl(feedUrl));
+ if (blogInfo != null) {
+ internalFollowFeed(blogInfo.feedId, blogInfo.getFeedUrl(), true, actionListener);
+ return;
+ }
+
+ // otherwise, look it up via the endpoint
+ updateFeedInfo(0, feedUrl, new UpdateBlogInfoListener() {
+ @Override
+ public void onResult(ReaderBlog blogInfo) {
+ // note we attempt to follow even when the look up fails (blogInfo = null) because that
+ // endpoint doesn't perform feed discovery, whereas the endpoint to follow a feed does
+ long feedIdToFollow = blogInfo != null ? blogInfo.feedId : 0;
+ String feedUrlToFollow = (blogInfo != null && blogInfo.hasFeedUrl()) ? blogInfo.getFeedUrl() : feedUrl;
+ internalFollowFeed(
+ feedIdToFollow,
+ feedUrlToFollow,
+ true,
+ actionListener);
+ }
+ });
+ }
+
+ private static boolean internalFollowFeed(
+ final long feedId,
+ final String feedUrl,
+ final boolean isAskingToFollow,
+ final ActionListener actionListener)
+ {
+ // feedUrl is required
+ if (TextUtils.isEmpty(feedUrl)) {
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ return false;
+ }
+
+ if (feedId != 0) {
+ ReaderBlogTable.setIsFollowedFeedId(feedId, isAskingToFollow);
+ ReaderPostTable.setFollowStatusForPostsInFeed(feedId, isAskingToFollow);
+ }
+
+ if (isAskingToFollow) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_BLOG_FOLLOWED);
+ } else {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_BLOG_UNFOLLOWED);
+ }
+
+ final String actionName = (isAskingToFollow ? "follow" : "unfollow");
+ final String path = "read/following/mine/"
+ + (isAskingToFollow ? "new" : "delete")
+ + "?url=" + UrlUtils.urlEncode(feedUrl);
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ boolean success = isFollowActionSuccessful(jsonObject, isAskingToFollow);
+ if (success) {
+ AppLog.d(T.READER, "feed " + actionName + " succeeded");
+ } else {
+ AppLog.w(T.READER, "feed " + actionName + " failed - " + jsonToString(jsonObject) + " - " + path);
+ localRevertFollowFeedId(feedId, isAskingToFollow);
+ }
+ if (actionListener != null) {
+ actionListener.onActionResult(success);
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.w(T.READER, "feed " + actionName + " failed with error");
+ AppLog.e(T.READER, volleyError);
+ localRevertFollowFeedId(feedId, isAskingToFollow);
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ }
+ };
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+
+ return true;
+ }
+
+ /*
+ * helper routine when following a blog from a post view
+ */
+ public static boolean followBlogForPost(ReaderPost post,
+ boolean isAskingToFollow,
+ ActionListener actionListener) {
+ if (post == null) {
+ AppLog.w(T.READER, "follow action performed with null post");
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ return false;
+ }
+ if (post.feedId != 0) {
+ return followFeedById(post.feedId, isAskingToFollow, actionListener);
+ } else {
+ return followBlogById(post.blogId, isAskingToFollow, actionListener);
+ }
+ }
+
+ /*
+ * called when a follow/unfollow fails, restores local data to previous state
+ */
+ private static void localRevertFollowBlogId(long blogId, boolean isAskingToFollow) {
+ ReaderBlogTable.setIsFollowedBlogId(blogId, !isAskingToFollow);
+ ReaderPostTable.setFollowStatusForPostsInBlog(blogId, !isAskingToFollow);
+ }
+ private static void localRevertFollowFeedId(long feedId, boolean isAskingToFollow) {
+ ReaderBlogTable.setIsFollowedFeedId(feedId, !isAskingToFollow);
+ ReaderPostTable.setFollowStatusForPostsInFeed(feedId, !isAskingToFollow);
+ }
+
+ /*
+ * returns whether a follow/unfollow was successful based on the response to:
+ * read/follows/new
+ * read/follows/delete
+ * site/$site/follows/new
+ * site/$site/follows/mine/delete
+ */
+ private static boolean isFollowActionSuccessful(JSONObject json, boolean isAskingToFollow) {
+ if (json == null) {
+ return false;
+ }
+
+ boolean isSubscribed;
+ if (json.has("subscribed")) {
+ // read/follows/
+ isSubscribed = json.optBoolean("subscribed", false);
+ } else {
+ // site/$site/follows/
+ isSubscribed = json.has("is_following") && json.optBoolean("is_following", false);
+ }
+ return (isSubscribed == isAskingToFollow);
+ }
+
+ /*
+ * request info about a specific blog
+ */
+ public static void updateBlogInfo(long blogId,
+ final String blogUrl,
+ final UpdateBlogInfoListener infoListener) {
+ // must pass either a valid id or url
+ final boolean hasBlogId = (blogId != 0);
+ final boolean hasBlogUrl = !TextUtils.isEmpty(blogUrl);
+ if (!hasBlogId && !hasBlogUrl) {
+ AppLog.w(T.READER, "cannot get blog info without either id or url");
+ if (infoListener != null) {
+ infoListener.onResult(null);
+ }
+ return;
+ }
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdateBlogInfoResponse(jsonObject, infoListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ // authentication error may indicate that API access has been disabled for this blog
+ int statusCode = VolleyUtils.statusCodeFromVolleyError(volleyError);
+ boolean isAuthErr = (statusCode == HttpURLConnection.HTTP_FORBIDDEN);
+ // if we failed to get the blog info using the id and this isn't an authentication
+ // error, try again using just the domain
+ if (!isAuthErr && hasBlogId && hasBlogUrl) {
+ AppLog.w(T.READER, "failed to get blog info by id, retrying with url");
+ updateBlogInfo(0, blogUrl, infoListener);
+ } else {
+ AppLog.e(T.READER, volleyError);
+ if (infoListener != null) {
+ infoListener.onResult(null);
+ }
+ }
+ }
+ };
+
+ if (hasBlogId) {
+ WordPress.getRestClientUtilsV1_1().get("read/sites/" + blogId, listener, errorListener);
+ } else {
+ WordPress.getRestClientUtilsV1_1().get("read/sites/" + UrlUtils.urlEncode(UrlUtils.getHost(blogUrl)), listener, errorListener);
+ }
+ }
+ public static void updateFeedInfo(long feedId, String feedUrl, final UpdateBlogInfoListener infoListener) {
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdateBlogInfoResponse(jsonObject, infoListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ if (infoListener != null) {
+ infoListener.onResult(null);
+ }
+ }
+ };
+ String path;
+ if (feedId != 0) {
+ path = "read/feed/" + feedId;
+ } else {
+ path = "read/feed/" + UrlUtils.urlEncode(feedUrl);
+ }
+ WordPress.getRestClientUtilsV1_1().get(path, listener, errorListener);
+ }
+ private static void handleUpdateBlogInfoResponse(JSONObject jsonObject, UpdateBlogInfoListener infoListener) {
+ if (jsonObject == null) {
+ if (infoListener != null) {
+ infoListener.onResult(null);
+ }
+ return;
+ }
+
+ ReaderBlog blogInfo = ReaderBlog.fromJson(jsonObject);
+ ReaderBlogTable.addOrUpdateBlog(blogInfo);
+
+ if (infoListener != null) {
+ infoListener.onResult(blogInfo);
+ }
+ }
+
+ /*
+ * tests whether the passed url can be reached - does NOT use authentication, and does not
+ * account for 404 replacement pages used by ISPs such as Charter
+ */
+ public static void checkUrlReachable(final String blogUrl,
+ final ReaderActions.OnRequestListener requestListener) {
+ // listener is required
+ if (requestListener == null) return;
+
+ Response.Listener<String> listener = new Response.Listener<String>() {
+ @Override
+ public void onResponse(String response) {
+ requestListener.onSuccess();
+ }
+ };
+ Response.ErrorListener errorListener = new Response.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ int statusCode;
+ // check specifically for auth failure class rather than relying on status code
+ // since a redirect to an unauthorized url may return a 301 rather than a 401
+ if (volleyError instanceof com.android.volley.AuthFailureError) {
+ statusCode = 401;
+ } else {
+ statusCode = VolleyUtils.statusCodeFromVolleyError(volleyError);
+ }
+ // Volley treats a 301 redirect as a failure here, we should treat it as
+ // success since it means the blog url is reachable
+ if (statusCode == 301) {
+ requestListener.onSuccess();
+ } else {
+ requestListener.onFailure(statusCode);
+ }
+ }
+ };
+
+ // TODO: this should be a HEAD request, but even though Volley supposedly supports HEAD
+ // using it results in "java.lang.IllegalStateException: Unknown method type"
+ StringRequest request = new StringRequest(
+ Request.Method.GET,
+ blogUrl,
+ listener,
+ errorListener);
+ WordPress.requestQueue.add(request);
+ }
+
+ /*
+ * block a blog - result includes the list of posts that were deleted by the block so they
+ * can be restored if the user undoes the block
+ */
+ public static BlockedBlogResult blockBlogFromReader(final long blogId, final ActionListener actionListener) {
+ final BlockedBlogResult blockResult = new BlockedBlogResult();
+ blockResult.blogId = blogId;
+ blockResult.deletedPosts = ReaderPostTable.getPostsInBlog(blogId, 0, false);
+ blockResult.wasFollowing = ReaderBlogTable.isFollowedBlog(blogId);
+
+ ReaderPostTable.deletePostsInBlog(blogId);
+ ReaderBlogTable.setIsFollowedBlogId(blogId, false);
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (actionListener != null) {
+ actionListener.onActionResult(true);
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ ReaderPostTable.addOrUpdatePosts(null, blockResult.deletedPosts);
+ if (blockResult.wasFollowing) {
+ ReaderBlogTable.setIsFollowedBlogId(blogId, true);
+ }
+ if (actionListener != null) {
+ actionListener.onActionResult(false);
+ }
+ }
+ };
+
+ AppLog.i(T.READER, "blocking blog " + blogId);
+ String path = "me/block/sites/" + Long.toString(blogId) + "/new";
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+
+ return blockResult;
+ }
+
+ public static void undoBlockBlogFromReader(final BlockedBlogResult blockResult) {
+ if (blockResult == null) {
+ return;
+ }
+ if (blockResult.deletedPosts != null) {
+ ReaderPostTable.addOrUpdatePosts(null, blockResult.deletedPosts);
+ }
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ boolean success = (jsonObject != null && jsonObject.optBoolean("success"));
+ // re-follow the blog if it was being followed prior to the block
+ if (success && blockResult.wasFollowing) {
+ followBlogById(blockResult.blogId, true, null);
+ } else if (!success) {
+ AppLog.w(T.READER, "failed to unblock blog " + blockResult.blogId);
+ }
+
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ }
+ };
+
+ AppLog.i(T.READER, "unblocking blog " + blockResult.blogId);
+ String path = "me/block/sites/" + Long.toString(blockResult.blogId) + "/delete";
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderCommentActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderCommentActions.java
new file mode 100644
index 000000000..1674102ba
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderCommentActions.java
@@ -0,0 +1,181 @@
+package org.wordpress.android.ui.reader.actions;
+
+import android.text.TextUtils;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderCommentTable;
+import org.wordpress.android.datasets.ReaderLikeTable;
+import org.wordpress.android.datasets.ReaderUserTable;
+import org.wordpress.android.models.ReaderComment;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderUser;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.VolleyUtils;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ReaderCommentActions {
+ /*
+ * used by post detail to generate a temporary "fake" comment id (see below)
+ */
+ public static long generateFakeCommentId() {
+ return System.currentTimeMillis();
+ }
+
+ /*
+ * add the passed comment text to the passed post - caller must pass a unique "fake" comment id
+ * to give the comment that's generated locally
+ */
+ public static ReaderComment submitPostComment(final ReaderPost post,
+ final long fakeCommentId,
+ final String commentText,
+ final long replyToCommentId,
+ final ReaderActions.CommentActionListener actionListener) {
+ if (post == null || TextUtils.isEmpty(commentText)) {
+ return null;
+ }
+
+ // determine which page this new comment should be assigned to
+ final int pageNumber;
+ if (replyToCommentId != 0) {
+ pageNumber = ReaderCommentTable.getPageNumberForComment(post.blogId, post.postId, replyToCommentId);
+ } else {
+ pageNumber = ReaderCommentTable.getLastPageNumberForPost(post.blogId, post.postId);
+ }
+
+ // create a "fake" comment that's added to the db so it can be shown right away - will be
+ // replaced with actual comment if it succeeds to be posted, or deleted if comment fails
+ // to be posted
+ ReaderComment newComment = new ReaderComment();
+ newComment.commentId = fakeCommentId;
+ newComment.postId = post.postId;
+ newComment.blogId = post.blogId;
+ newComment.parentId = replyToCommentId;
+ newComment.pageNumber = pageNumber;
+ newComment.setText(commentText);
+
+ Date dtPublished = DateTimeUtils.nowUTC();
+ newComment.setPublished(DateTimeUtils.iso8601FromDate(dtPublished));
+ newComment.timestamp = dtPublished.getTime();
+
+ ReaderUser currentUser = ReaderUserTable.getCurrentUser();
+ if (currentUser != null) {
+ newComment.setAuthorAvatar(currentUser.getAvatarUrl());
+ newComment.setAuthorName(currentUser.getDisplayName());
+ }
+
+ ReaderCommentTable.addOrUpdateComment(newComment);
+
+ // different endpoint depending on whether the new comment is a reply to another comment
+ final String path;
+ if (replyToCommentId == 0) {
+ path = "sites/" + post.blogId + "/posts/" + post.postId + "/replies/new";
+ } else {
+ path = "sites/" + post.blogId + "/comments/" + Long.toString(replyToCommentId) + "/replies/new";
+ }
+
+ Map<String, String> params = new HashMap<>();
+ params.put("content", commentText);
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ ReaderCommentTable.deleteComment(post, fakeCommentId);
+ AppLog.i(T.READER, "comment succeeded");
+ ReaderComment newComment = ReaderComment.fromJson(jsonObject, post.blogId);
+ newComment.pageNumber = pageNumber;
+ ReaderCommentTable.addOrUpdateComment(newComment);
+ if (actionListener != null) {
+ actionListener.onActionResult(true, newComment);
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ ReaderCommentTable.deleteComment(post, fakeCommentId);
+ AppLog.w(T.READER, "comment failed");
+ AppLog.e(T.READER, volleyError);
+ if (actionListener != null) {
+ actionListener.onActionResult(false, null);
+ }
+ }
+ };
+
+ AppLog.i(T.READER, "submitting comment");
+ WordPress.getRestClientUtilsV1_1().post(path, params, null, listener, errorListener);
+
+ return newComment;
+ }
+
+ /*
+ * like or unlike the passed comment
+ */
+ public static boolean performLikeAction(final ReaderComment comment, boolean isAskingToLike) {
+ if (comment == null) {
+ return false;
+ }
+
+ // make sure like status is changing
+ boolean isCurrentlyLiked = ReaderCommentTable.isCommentLikedByCurrentUser(comment);
+ if (isCurrentlyLiked == isAskingToLike) {
+ AppLog.w(T.READER, "comment like unchanged");
+ return false;
+ }
+
+ // update like status and like count in local db
+ int newNumLikes = (isAskingToLike ? comment.numLikes + 1 : comment.numLikes - 1);
+ ReaderCommentTable.setLikesForComment(comment, newNumLikes, isAskingToLike);
+ ReaderLikeTable.setCurrentUserLikesComment(comment, isAskingToLike);
+
+ // sites/$site/comments/$comment_ID/likes/new
+ final String actionName = isAskingToLike ? "like" : "unlike";
+ String path = "sites/" + comment.blogId + "/comments/" + comment.commentId + "/likes/";
+ if (isAskingToLike) {
+ path += "new";
+ } else {
+ path += "mine/delete";
+ }
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ boolean success = (jsonObject != null && JSONUtils.getBool(jsonObject, "success"));
+ if (success) {
+ AppLog.d(T.READER, String.format("comment %s succeeded", actionName));
+ } else {
+ AppLog.w(T.READER, String.format("comment %s failed", actionName));
+ ReaderCommentTable.setLikesForComment(comment, comment.numLikes, comment.isLikedByCurrentUser);
+ ReaderLikeTable.setCurrentUserLikesComment(comment, comment.isLikedByCurrentUser);
+ }
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ String error = VolleyUtils.errStringFromVolleyError(volleyError);
+ if (TextUtils.isEmpty(error)) {
+ AppLog.w(T.READER, String.format("comment %s failed", actionName));
+ } else {
+ AppLog.w(T.READER, String.format("comment %s failed (%s)", actionName, error));
+ }
+ AppLog.e(T.READER, volleyError);
+ ReaderCommentTable.setLikesForComment(comment, comment.numLikes, comment.isLikedByCurrentUser);
+ ReaderLikeTable.setCurrentUserLikesComment(comment, comment.isLikedByCurrentUser);
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+ return true;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java
new file mode 100644
index 000000000..fa772de60
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java
@@ -0,0 +1,359 @@
+package org.wordpress.android.ui.reader.actions;
+
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.StringRequest;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderLikeTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.ReaderUserTable;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostList;
+import org.wordpress.android.models.ReaderUserIdList;
+import org.wordpress.android.models.ReaderUserList;
+import org.wordpress.android.ui.reader.ReaderEvents;
+import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResult;
+import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResultListener;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.VolleyUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+import de.greenrobot.event.EventBus;
+
+public class ReaderPostActions {
+
+ private static final String TRACKING_REFERRER = "https://wordpress.com/";
+ private static final Random mRandom = new Random();
+
+ private ReaderPostActions() {
+ throw new AssertionError();
+ }
+
+ /**
+ * like/unlike the passed post
+ */
+ public static boolean performLikeAction(final ReaderPost post,
+ final boolean isAskingToLike) {
+ // do nothing if post's like state is same as passed
+ boolean isCurrentlyLiked = ReaderPostTable.isPostLikedByCurrentUser(post);
+ if (isCurrentlyLiked == isAskingToLike) {
+ AppLog.w(T.READER, "post like unchanged");
+ return false;
+ }
+
+ // update like status and like count in local db
+ int numCurrentLikes = ReaderPostTable.getNumLikesForPost(post.blogId, post.postId);
+ int newNumLikes = (isAskingToLike ? numCurrentLikes + 1 : numCurrentLikes - 1);
+ if (newNumLikes < 0) {
+ newNumLikes = 0;
+ }
+ ReaderPostTable.setLikesForPost(post, newNumLikes, isAskingToLike);
+ ReaderLikeTable.setCurrentUserLikesPost(post, isAskingToLike);
+
+ final String actionName = isAskingToLike ? "like" : "unlike";
+ String path = "sites/" + post.blogId + "/posts/" + post.postId + "/likes/";
+ if (isAskingToLike) {
+ path += "new";
+ } else {
+ path += "mine/delete";
+ }
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ AppLog.d(T.READER, String.format("post %s succeeded", actionName));
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ String error = VolleyUtils.errStringFromVolleyError(volleyError);
+ if (TextUtils.isEmpty(error)) {
+ AppLog.w(T.READER, String.format("post %s failed", actionName));
+ } else {
+ AppLog.w(T.READER, String.format("post %s failed (%s)", actionName, error));
+ }
+ AppLog.e(T.READER, volleyError);
+ ReaderPostTable.setLikesForPost(post, post.numLikes, post.isLikedByCurrentUser);
+ ReaderLikeTable.setCurrentUserLikesPost(post, post.isLikedByCurrentUser);
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+ return true;
+ }
+
+ /*
+ * get the latest version of this post - note that the post is only considered changed if the
+ * like/comment count has changed, or if the current user's like/follow status has changed
+ */
+ public static void updatePost(final ReaderPost localPost,
+ final UpdateResultListener resultListener) {
+ String path = "read/sites/" + localPost.blogId + "/posts/" + localPost.postId + "/?meta=site,likes";
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdatePostResponse(localPost, jsonObject, resultListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ if (resultListener != null) {
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ }
+ }
+ };
+ AppLog.d(T.READER, "updating post");
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ private static void handleUpdatePostResponse(final ReaderPost localPost,
+ final JSONObject jsonObject,
+ final UpdateResultListener resultListener) {
+ if (jsonObject == null) {
+ if (resultListener != null) {
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ }
+ return;
+ }
+
+ final Handler handler = new Handler();
+
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderPost serverPost = ReaderPost.fromJson(jsonObject);
+
+ // TODO: this temporary fix was added 25-Apr-2016 as a workaround for the fact that
+ // the read/sites/{blogId}/posts/{postId} endpoint doesn't contain the feedId or
+ // feedItemId of the post. because of this, we need to copy them from the local post
+ // before calling isSamePost (since the difference in those IDs causes it to return false)
+ if (serverPost.feedId == 0 && localPost.feedId != 0) {
+ serverPost.feedId = localPost.feedId;
+ }
+
+ if (serverPost.feedItemId == 0 && localPost.feedItemId != 0) {
+ serverPost.feedItemId = localPost.feedItemId;
+ }
+
+ boolean hasChanges = !serverPost.isSamePost(localPost);
+
+ if (hasChanges) {
+ AppLog.d(T.READER, "post updated");
+ // copy changes over to the local post - this is done instead of simply overwriting
+ // the local post with the server post because the server post was retrieved using
+ // the read/sites/$siteId/posts/$postId endpoint which is missing some information
+ // https://github.com/wordpress-mobile/WordPress-Android/issues/3164
+ localPost.numReplies = serverPost.numReplies;
+ localPost.numLikes = serverPost.numLikes;
+ localPost.isFollowedByCurrentUser = serverPost.isFollowedByCurrentUser;
+ localPost.isLikedByCurrentUser = serverPost.isLikedByCurrentUser;
+ localPost.isCommentsOpen = serverPost.isCommentsOpen;
+ localPost.setTitle(serverPost.getTitle());
+ localPost.setText(serverPost.getText());
+ ReaderPostTable.addOrUpdatePost(localPost);
+ }
+
+ // always update liking users regardless of whether changes were detected - this
+ // ensures that the liking avatars are immediately available to post detail
+ if (handlePostLikes(serverPost, jsonObject)) {
+ hasChanges = true;
+ }
+
+ if (resultListener != null) {
+ final UpdateResult result = (hasChanges ? UpdateResult.CHANGED : UpdateResult.UNCHANGED);
+ handler.post(new Runnable() {
+ public void run() {
+ resultListener.onUpdateResult(result);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /*
+ * updates local liking users based on the "likes" meta section of the post's json - requires
+ * using the /sites/ endpoint with ?meta=likes - returns true if likes have changed
+ */
+ private static boolean handlePostLikes(final ReaderPost post, JSONObject jsonPost) {
+ if (post == null || jsonPost == null) {
+ return false;
+ }
+
+ JSONObject jsonLikes = JSONUtils.getJSONChild(jsonPost, "meta/data/likes");
+ if (jsonLikes == null) {
+ return false;
+ }
+
+ ReaderUserList likingUsers = ReaderUserList.fromJsonLikes(jsonLikes);
+ ReaderUserIdList likingUserIds = likingUsers.getUserIds();
+
+ ReaderUserIdList existingIds = ReaderLikeTable.getLikesForPost(post);
+ if (likingUserIds.isSameList(existingIds)) {
+ return false;
+ }
+
+ ReaderUserTable.addOrUpdateUsers(likingUsers);
+ ReaderLikeTable.setLikesForPost(post, likingUserIds);
+ return true;
+ }
+
+ /**
+ * similar to updatePost, but used when post doesn't already exist in local db
+ **/
+ public static void requestPost(final long blogId,
+ final long postId,
+ final ReaderActions.OnRequestListener requestListener) {
+ String path = "read/sites/" + blogId + "/posts/" + postId + "/?meta=site,likes";
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ ReaderPost post = ReaderPost.fromJson(jsonObject);
+ ReaderPostTable.addOrUpdatePost(post);
+ handlePostLikes(post, jsonObject);
+ if (requestListener != null) {
+ requestListener.onSuccess();
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ if (requestListener != null) {
+ int statusCode = 0;
+ // first try to get the error code from the JSON response, example:
+ // {"code":403,"headers":[{"name":"Content-Type","value":"application\/json"}],
+ // "body":{"error":"unauthorized","message":"User cannot access this private blog."}}
+ JSONObject jsonObject = VolleyUtils.volleyErrorToJSON(volleyError);
+ if (jsonObject != null && jsonObject.has("code")) {
+ statusCode = jsonObject.optInt("code");
+ }
+ if (statusCode == 0) {
+ statusCode = VolleyUtils.statusCodeFromVolleyError(volleyError);
+ }
+ requestListener.onFailure(statusCode);
+ }
+ }
+ };
+ AppLog.d(T.READER, "requesting post");
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ private static String getTrackingPixelForPost(@NonNull ReaderPost post) {
+ return "https://pixel.wp.com/g.gif?v=wpcom&reader=1"
+ + "&blog=" + post.blogId
+ + "&post=" + post.postId
+ + "&host=" + UrlUtils.urlEncode(UrlUtils.getHost(post.getBlogUrl()))
+ + "&ref=" + UrlUtils.urlEncode(TRACKING_REFERRER)
+ + "&t=" + mRandom.nextInt();
+ }
+
+ public static void bumpPageViewForPost(long blogId, long postId) {
+ bumpPageViewForPost(ReaderPostTable.getPost(blogId, postId, true));
+ }
+ public static void bumpPageViewForPost(ReaderPost post) {
+ if (post == null) {
+ return;
+ }
+
+ // don't bump stats for posts in blogs the current user is an admin of, unless
+ // this is a private post since we count views for private posts from admins
+ if (!post.isPrivate && WordPress.wpDB.isCurrentUserAdminOfRemoteBlogId(post.blogId)) {
+ AppLog.d(T.READER, "skipped bump page view - user is admin");
+ return;
+ }
+
+ Response.Listener<String> listener = new Response.Listener<String>() {
+ @Override
+ public void onResponse(String response) {
+ AppLog.d(T.READER, "bump page view succeeded");
+ }
+ };
+ Response.ErrorListener errorListener = new Response.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ AppLog.w(T.READER, "bump page view failed");
+ }
+ };
+
+ Request request = new StringRequest(
+ Request.Method.GET,
+ getTrackingPixelForPost(post),
+ listener,
+ errorListener) {
+ @Override
+ public Map<String, String> getHeaders() throws AuthFailureError {
+ // call will fail without correct refer(r)er
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Referer", TRACKING_REFERRER);
+ return headers;
+ }
+ };
+
+ WordPress.requestQueue.add(request);
+ }
+
+ /*
+ * request posts related to the passed one
+ */
+ public static void requestRelatedPosts(final ReaderPost sourcePost) {
+ if (sourcePost == null) return;
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleRelatedPostsResponse(sourcePost, jsonObject);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.w(T.READER, "updateRelatedPosts failed");
+ AppLog.e(T.READER, volleyError);
+
+ }
+ };
+
+ String path = "/read/site/" + sourcePost.blogId + "/post/" + sourcePost.postId + "/related";
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ private static void handleRelatedPostsResponse(final ReaderPost sourcePost, final JSONObject jsonObject) {
+ if (jsonObject == null) return;
+
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderPostList relatedPosts = ReaderPostList.fromJson(jsonObject);
+ if (relatedPosts != null && relatedPosts.size() > 0) {
+ ReaderPostTable.addOrUpdatePosts(null, relatedPosts);
+ EventBus.getDefault().post(new ReaderEvents.RelatedPostsUpdated(sourcePost, relatedPosts));
+ }
+ }
+ }.start();
+
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java
new file mode 100644
index 000000000..6732c49fa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java
@@ -0,0 +1,168 @@
+package org.wordpress.android.ui.reader.actions;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagList;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.VolleyUtils;
+
+public class ReaderTagActions {
+ private ReaderTagActions() {
+ throw new AssertionError();
+ }
+
+ public static boolean deleteTag(final ReaderTag tag,
+ final ReaderActions.ActionListener actionListener) {
+ if (tag == null) {
+ ReaderActions.callActionListener(actionListener, false);
+ return false;
+ }
+
+ final String tagNameForApi = ReaderUtils.sanitizeWithDashes(tag.getTagSlug());
+ final String path = "read/tags/" + tagNameForApi + "/mine/delete";
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ AppLog.i(T.READER, "delete tag succeeded");
+ ReaderActions.callActionListener(actionListener, true);
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ // treat it as a success if the error says the user isn't following the deleted tag
+ String error = VolleyUtils.errStringFromVolleyError(volleyError);
+ if (error.equals("not_subscribed")) {
+ AppLog.w(T.READER, "delete tag succeeded with error " + error);
+ ReaderActions.callActionListener(actionListener, true);
+ return;
+ }
+
+ AppLog.w(T.READER, " delete tag failed");
+ AppLog.e(T.READER, volleyError);
+
+ // add back original tag
+ ReaderTagTable.addOrUpdateTag(tag);
+
+ ReaderActions.callActionListener(actionListener, false);
+ }
+ };
+
+ ReaderTagTable.deleteTag(tag);
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+
+ return true;
+ }
+
+ public static boolean addTag(final ReaderTag tag,
+ final ReaderActions.ActionListener actionListener) {
+ if (tag == null) {
+ ReaderActions.callActionListener(actionListener, false);
+ return false;
+ }
+
+ final String tagNameForApi = ReaderUtils.sanitizeWithDashes(tag.getTagSlug());
+ final String path = "read/tags/" + tagNameForApi + "/mine/new";
+ String endpoint = "/read/tags/" + tagNameForApi + "/posts";
+
+ ReaderTag newTag = new ReaderTag(
+ tag.getTagSlug(),
+ tag.getTagDisplayName(),
+ tag.getTagTitle(),
+ endpoint,
+ ReaderTagType.FOLLOWED);
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ AppLog.i(T.READER, "add tag succeeded");
+ // the response will contain the list of the user's followed tags
+ ReaderTagList tags = parseFollowedTags(jsonObject);
+ ReaderTagTable.replaceFollowedTags(tags);
+ ReaderActions.callActionListener(actionListener, true);
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ // treat is as a success if we're adding a tag and the error says the user is
+ // already following it
+ String error = VolleyUtils.errStringFromVolleyError(volleyError);
+ if (error.equals("already_subscribed")) {
+ AppLog.w(T.READER, "add tag succeeded with error " + error);
+ ReaderActions.callActionListener(actionListener, true);
+ return;
+ }
+
+ AppLog.w(T.READER, "add tag failed");
+ AppLog.e(T.READER, volleyError);
+
+ // revert on failure
+ ReaderTagTable.deleteTag(tag);
+
+ ReaderActions.callActionListener(actionListener, false);
+ }
+ };
+
+ ReaderTagTable.addOrUpdateTag(newTag);
+ WordPress.getRestClientUtilsV1_1().post(path, listener, errorListener);
+
+ return true;
+ }
+
+ /*
+ * return the user's followed tags from the response to read/tags/{tag}/mine/new
+ */
+ /*
+ {
+ "added_tag": "84776",
+ "subscribed": true,
+ "tags": [
+ {
+ "display_name": "fitness",
+ "ID": "5189",
+ "slug": "fitness",
+ "title": "Fitness",
+ "URL": "https://public-api.wordpress.com/rest/v1.1/read/tags/fitness/posts"
+ },
+ ...
+ }
+ */
+ private static ReaderTagList parseFollowedTags(JSONObject jsonObject) {
+ if (jsonObject == null) {
+ return null;
+ }
+
+ JSONArray jsonTags = jsonObject.optJSONArray(ReaderConstants.JSON_TAG_TAGS_ARRAY);
+ if (jsonTags == null || jsonTags.length() == 0) {
+ return null;
+ }
+
+ ReaderTagList tags = new ReaderTagList();
+ for (int i=0; i < jsonTags.length(); i++) {
+ JSONObject jsonThisTag = jsonTags.optJSONObject(i);
+ String tagTitle = JSONUtils.getStringDecoded(jsonThisTag, ReaderConstants.JSON_TAG_TITLE);
+ String tagDisplayName = JSONUtils.getStringDecoded(jsonThisTag, ReaderConstants.JSON_TAG_DISPLAY_NAME);
+ String tagSlug = JSONUtils.getStringDecoded(jsonThisTag, ReaderConstants.JSON_TAG_SLUG);
+ String endpoint = JSONUtils.getString(jsonThisTag, ReaderConstants.JSON_TAG_URL);
+ tags.add(new ReaderTag(tagSlug, tagDisplayName, tagTitle, endpoint, ReaderTagType.FOLLOWED));
+ }
+
+ return tags;
+ }
+
+}