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