blob: 6398b1e07665f1c769b5144b8cb4c1ca5377e160 [file] [log] [blame]
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);
}
}