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