aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderPostService.java
diff options
context:
space:
mode:
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderPostService.java')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderPostService.java391
1 files changed, 391 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderPostService.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderPostService.java
new file mode 100644
index 000000000..a0cf613bb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderPostService.java
@@ -0,0 +1,391 @@
+package org.wordpress.android.ui.reader.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+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.ReaderPostTable;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.models.ReaderPostList;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.reader.ReaderConstants;
+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.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * service which updates posts with specific tags or in specific blogs/feeds - relies on
+ * EventBus to alert of update status
+ */
+
+public class ReaderPostService extends Service {
+
+ private static final String ARG_TAG = "tag";
+ private static final String ARG_ACTION = "action";
+ private static final String ARG_BLOG_ID = "blog_id";
+ private static final String ARG_FEED_ID = "feed_id";
+
+ public enum UpdateAction {
+ REQUEST_NEWER, // request the newest posts for this tag/blog/feed
+ REQUEST_OLDER, // request posts older than the oldest existing one for this tag/blog/feed
+ REQUEST_OLDER_THAN_GAP // request posts older than the one with the gap marker for this tag (not supported for blog/feed)
+ }
+
+ /*
+ * update posts with the passed tag
+ */
+ public static void startServiceForTag(Context context, ReaderTag tag, UpdateAction action) {
+ Intent intent = new Intent(context, ReaderPostService.class);
+ intent.putExtra(ARG_TAG, tag);
+ intent.putExtra(ARG_ACTION, action);
+ context.startService(intent);
+ }
+
+ /*
+ * update posts in the passed blog
+ */
+ public static void startServiceForBlog(Context context, long blogId, UpdateAction action) {
+ Intent intent = new Intent(context, ReaderPostService.class);
+ intent.putExtra(ARG_BLOG_ID, blogId);
+ intent.putExtra(ARG_ACTION, action);
+ context.startService(intent);
+ }
+
+ /*
+ * update posts in the passed feed
+ */
+ public static void startServiceForFeed(Context context, long feedId, UpdateAction action) {
+ Intent intent = new Intent(context, ReaderPostService.class);
+ intent.putExtra(ARG_FEED_ID, feedId);
+ intent.putExtra(ARG_ACTION, action);
+ context.startService(intent);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.READER, "reader post service > created");
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.READER, "reader post service > destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ return START_NOT_STICKY;
+ }
+
+ UpdateAction action;
+ if (intent.hasExtra(ARG_ACTION)) {
+ action = (UpdateAction) intent.getSerializableExtra(ARG_ACTION);
+ } else {
+ action = UpdateAction.REQUEST_NEWER;
+ }
+
+ EventBus.getDefault().post(new ReaderEvents.UpdatePostsStarted(action));
+
+ if (intent.hasExtra(ARG_TAG)) {
+ ReaderTag tag = (ReaderTag) intent.getSerializableExtra(ARG_TAG);
+ updatePostsWithTag(tag, action);
+ } else if (intent.hasExtra(ARG_BLOG_ID)) {
+ long blogId = intent.getLongExtra(ARG_BLOG_ID, 0);
+ updatePostsInBlog(blogId, action);
+ } else if (intent.hasExtra(ARG_FEED_ID)) {
+ long feedId = intent.getLongExtra(ARG_FEED_ID, 0);
+ updatePostsInFeed(feedId, action);
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ private void updatePostsWithTag(final ReaderTag tag, final UpdateAction action) {
+ requestPostsWithTag(
+ tag,
+ action,
+ new UpdateResultListener() {
+ @Override
+ public void onUpdateResult(UpdateResult result) {
+ EventBus.getDefault().post(new ReaderEvents.UpdatePostsEnded(tag, result, action));
+ stopSelf();
+ }
+ });
+ }
+
+ private void updatePostsInBlog(long blogId, final UpdateAction action) {
+ UpdateResultListener listener = new UpdateResultListener() {
+ @Override
+ public void onUpdateResult(UpdateResult result) {
+ EventBus.getDefault().post(new ReaderEvents.UpdatePostsEnded(result, action));
+ stopSelf();
+ }
+ };
+ requestPostsForBlog(blogId, action, listener);
+ }
+
+ private void updatePostsInFeed(long feedId, final UpdateAction action) {
+ UpdateResultListener listener = new UpdateResultListener() {
+ @Override
+ public void onUpdateResult(UpdateResult result) {
+ EventBus.getDefault().post(new ReaderEvents.UpdatePostsEnded(result, action));
+ stopSelf();
+ }
+ };
+ requestPostsForFeed(feedId, action, listener);
+ }
+
+ private static void requestPostsWithTag(final ReaderTag tag,
+ final UpdateAction updateAction,
+ final UpdateResultListener resultListener) {
+ String path = getRelativeEndpointForTag(tag);
+ if (TextUtils.isEmpty(path)) {
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder(path);
+
+ // append #posts to retrieve
+ sb.append("?number=").append(ReaderConstants.READER_MAX_POSTS_TO_REQUEST);
+
+ // return newest posts first (this is the default, but make it explicit since it's important)
+ sb.append("&order=DESC");
+
+ String beforeDate;
+ switch (updateAction) {
+ case REQUEST_OLDER:
+ // request posts older than the oldest existing post with this tag
+ beforeDate = ReaderPostTable.getOldestDateWithTag(tag);
+ break;
+ case REQUEST_OLDER_THAN_GAP:
+ // request posts older than the post with the gap marker for this tag
+ beforeDate = ReaderPostTable.getGapMarkerDateForTag(tag);
+ break;
+ default:
+ beforeDate = null;
+ break;
+ }
+ if (!TextUtils.isEmpty(beforeDate)) {
+ sb.append("&before=").append(UrlUtils.urlEncode(beforeDate));
+ }
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ // remember when this tag was updated if newer posts were requested
+ if (updateAction == UpdateAction.REQUEST_NEWER) {
+ ReaderTagTable.setTagLastUpdated(tag);
+ }
+ handleUpdatePostsResponse(tag, jsonObject, updateAction, resultListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ }
+ };
+
+ WordPress.getRestClientUtilsV1_2().get(sb.toString(), null, null, listener, errorListener);
+ }
+
+ private static void requestPostsForBlog(final long blogId,
+ final UpdateAction updateAction,
+ final UpdateResultListener resultListener) {
+ String path = "read/sites/" + blogId + "/posts/?meta=site,likes";
+
+ // append the date of the oldest cached post in this blog when requesting older posts
+ if (updateAction == UpdateAction.REQUEST_OLDER) {
+ String dateOldest = ReaderPostTable.getOldestDateInBlog(blogId);
+ if (!TextUtils.isEmpty(dateOldest)) {
+ path += "&before=" + UrlUtils.urlEncode(dateOldest);
+ }
+ }
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdatePostsResponse(null, jsonObject, updateAction, resultListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ }
+ };
+ AppLog.d(AppLog.T.READER, "updating posts in blog " + blogId);
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ private static void requestPostsForFeed(final long feedId,
+ final UpdateAction updateAction,
+ final UpdateResultListener resultListener) {
+ String path = "read/feed/" + feedId + "/posts/?meta=site,likes";
+ if (updateAction == UpdateAction.REQUEST_OLDER) {
+ String dateOldest = ReaderPostTable.getOldestDateInFeed(feedId);
+ if (!TextUtils.isEmpty(dateOldest)) {
+ path += "&before=" + UrlUtils.urlEncode(dateOldest);
+ }
+ }
+
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdatePostsResponse(null, jsonObject, updateAction, resultListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ }
+ };
+
+ AppLog.d(AppLog.T.READER, "updating posts in feed " + feedId);
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ /*
+ * called after requesting posts with a specific tag or in a specific blog/feed
+ */
+ private static void handleUpdatePostsResponse(final ReaderTag tag,
+ final JSONObject jsonObject,
+ final UpdateAction updateAction,
+ final UpdateResultListener resultListener) {
+ if (jsonObject == null) {
+ resultListener.onUpdateResult(UpdateResult.FAILED);
+ return;
+ }
+
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderPostList serverPosts = ReaderPostList.fromJson(jsonObject);
+ UpdateResult updateResult = ReaderPostTable.comparePosts(serverPosts);
+ if (updateResult.isNewOrChanged()) {
+ // gap detection - only applies to posts with a specific tag
+ ReaderPost postWithGap = null;
+ if (tag != null) {
+ switch (updateAction) {
+ case REQUEST_NEWER:
+ // if there's no overlap between server and local (ie: all server
+ // posts are new), assume there's a gap between server and local
+ // provided that local posts exist
+ int numServerPosts = serverPosts.size();
+ if (numServerPosts >= 2
+ && ReaderPostTable.getNumPostsWithTag(tag) > 0
+ && !ReaderPostTable.hasOverlap(serverPosts)) {
+ // treat the second to last server post as having a gap
+ postWithGap = serverPosts.get(numServerPosts - 2);
+ // remove the last server post to deal with the edge case of
+ // there actually not being a gap between local & server
+ serverPosts.remove(numServerPosts - 1);
+ AppLog.d(AppLog.T.READER, "added gap marker to tag " + tag.getTagNameForLog());
+ }
+ ReaderPostTable.removeGapMarkerForTag(tag);
+ break;
+ case REQUEST_OLDER_THAN_GAP:
+ // if service was started as a request to fill a gap, delete existing posts
+ // before the one with the gap marker, then remove the existing gap marker
+ ReaderPostTable.deletePostsBeforeGapMarkerForTag(tag);
+ ReaderPostTable.removeGapMarkerForTag(tag);
+ break;
+ }
+ }
+
+ ReaderPostTable.addOrUpdatePosts(tag, serverPosts);
+
+ // gap marker must be set after saving server posts
+ if (postWithGap != null) {
+ ReaderPostTable.setGapMarkerForTag(postWithGap.blogId, postWithGap.postId, tag);
+ }
+ } else if (updateResult == UpdateResult.UNCHANGED && updateAction == UpdateAction.REQUEST_OLDER_THAN_GAP) {
+ // edge case - request to fill gap returned nothing new, so remove the gap marker
+ ReaderPostTable.removeGapMarkerForTag(tag);
+ AppLog.w(AppLog.T.READER, "attempt to fill gap returned nothing new");
+ }
+ AppLog.d(AppLog.T.READER, "requested posts response = " + updateResult.toString());
+ resultListener.onUpdateResult(updateResult);
+ }
+ }.start();
+ }
+
+ /*
+ * returns the endpoint to use when requesting posts with the passed tag
+ */
+ private static String getRelativeEndpointForTag(ReaderTag tag) {
+ if (tag == null) {
+ return null;
+ }
+
+ // if passed tag has an assigned endpoint, return it and be done
+ if (!TextUtils.isEmpty(tag.getEndpoint())) {
+ return getRelativeEndpoint(tag.getEndpoint());
+ }
+
+ // check the db for the endpoint
+ String endpoint = ReaderTagTable.getEndpointForTag(tag);
+ if (!TextUtils.isEmpty(endpoint)) {
+ return getRelativeEndpoint(endpoint);
+ }
+
+ // never hand craft the endpoint for default tags, since these MUST be updated
+ // using their stored endpoints
+ if (tag.tagType == ReaderTagType.DEFAULT) {
+ return null;
+ }
+
+ return String.format("read/tags/%s/posts", ReaderUtils.sanitizeWithDashes(tag.getTagSlug()));
+ }
+
+ /*
+ * returns the passed endpoint without the unnecessary path - this is
+ * needed because as of 20-Feb-2015 the /read/menu/ call returns the
+ * full path but we don't want to use the full path since it may change
+ * between API versions (as it did when we moved from v1 to v1.1)
+ *
+ * ex: https://public-api.wordpress.com/rest/v1/read/tags/fitness/posts
+ * becomes just read/tags/fitness/posts
+ */
+ private static String getRelativeEndpoint(final String endpoint) {
+ if (endpoint != null && endpoint.startsWith("http")) {
+ int pos = endpoint.indexOf("/read/");
+ if (pos > -1) {
+ return endpoint.substring(pos + 1, endpoint.length());
+ }
+ pos = endpoint.indexOf("/v1/");
+ if (pos > -1) {
+ return endpoint.substring(pos + 4, endpoint.length());
+ }
+ }
+ return StringUtils.notNullStr(endpoint);
+ }
+
+}