diff options
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.java | 391 |
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); + } + +} |