diff options
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderBlogActions.java')
-rw-r--r-- | WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderBlogActions.java | 477 |
1 files changed, 477 insertions, 0 deletions
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); + } +} |