| 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); |
| } |
| |
| } |