aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/ui/reader/services
diff options
context:
space:
mode:
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/reader/services')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderCommentService.java206
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderPostService.java391
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderSearchService.java138
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderUpdateService.java331
4 files changed, 1066 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderCommentService.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderCommentService.java
new file mode 100644
index 000000000..63e58564b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderCommentService.java
@@ -0,0 +1,206 @@
+package org.wordpress.android.ui.reader.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderCommentTable;
+import org.wordpress.android.datasets.ReaderDatabase;
+import org.wordpress.android.datasets.ReaderLikeTable;
+import org.wordpress.android.datasets.ReaderUserTable;
+import org.wordpress.android.models.ReaderComment;
+import org.wordpress.android.models.ReaderCommentList;
+import org.wordpress.android.models.ReaderUserList;
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.ReaderEvents;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+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.JSONUtils;
+
+import de.greenrobot.event.EventBus;
+
+public class ReaderCommentService extends Service {
+
+ private static final String ARG_POST_ID = "post_id";
+ private static final String ARG_BLOG_ID = "blog_id";
+ private static final String ARG_COMMENT_ID = "comment_id";
+ private static final String ARG_NEXT_PAGE = "next_page";
+
+ private static int mCurrentPage;
+
+ public static void startService(Context context, long blogId, long postId, boolean requestNextPage) {
+ if (context == null) return;
+
+ Intent intent = new Intent(context, ReaderCommentService.class);
+ intent.putExtra(ARG_BLOG_ID, blogId);
+ intent.putExtra(ARG_POST_ID, postId);
+ intent.putExtra(ARG_NEXT_PAGE, requestNextPage);
+ context.startService(intent);
+ }
+
+ // Requests comments until the passed commentId is found
+ public static void startServiceForComment(Context context, long blogId, long postId, long commentId) {
+ if (context == null) return;
+
+ Intent intent = new Intent(context, ReaderCommentService.class);
+ intent.putExtra(ARG_BLOG_ID, blogId);
+ intent.putExtra(ARG_POST_ID, postId);
+ intent.putExtra(ARG_COMMENT_ID, commentId);
+ context.startService(intent);
+ }
+
+ public static void stopService(Context context) {
+ if (context == null) return;
+
+ Intent intent = new Intent(context, ReaderCommentService.class);
+ context.stopService(intent);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.READER, "reader comment service > created");
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.READER, "reader comment service > destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ return START_NOT_STICKY;
+ }
+
+ EventBus.getDefault().post(new ReaderEvents.UpdateCommentsStarted());
+
+ final long blogId = intent.getLongExtra(ARG_BLOG_ID, 0);
+ final long postId = intent.getLongExtra(ARG_POST_ID, 0);
+ final long commentId = intent.getLongExtra(ARG_COMMENT_ID, 0);
+ boolean requestNextPage = intent.getBooleanExtra(ARG_NEXT_PAGE, false);
+
+ if (requestNextPage) {
+ int prevPage = ReaderCommentTable.getLastPageNumberForPost(blogId, postId);
+ mCurrentPage = prevPage + 1;
+ } else {
+ mCurrentPage = 1;
+ }
+
+ updateCommentsForPost(blogId, postId, mCurrentPage, new UpdateResultListener() {
+ @Override
+ public void onUpdateResult(UpdateResult result) {
+ if (commentId > 0) {
+ if (ReaderCommentTable.commentExists(blogId, postId, commentId) || !result.isNewOrChanged()) {
+ EventBus.getDefault().post(new ReaderEvents.UpdateCommentsEnded(result));
+ stopSelf();
+ } else {
+ // Comment not found yet, request the next page
+ mCurrentPage++;
+ updateCommentsForPost(blogId, postId, mCurrentPage, this);
+ }
+ } else {
+ EventBus.getDefault().post(new ReaderEvents.UpdateCommentsEnded(result));
+ stopSelf();
+ }
+ }
+ });
+
+ return START_NOT_STICKY;
+ }
+
+ private static void updateCommentsForPost(final long blogId,
+ final long postId,
+ final int pageNumber,
+ final ReaderActions.UpdateResultListener resultListener) {
+ String path = "sites/" + blogId + "/posts/" + postId + "/replies/"
+ + "?number=" + Integer.toString(ReaderConstants.READER_MAX_COMMENTS_TO_REQUEST)
+ + "&meta=likes"
+ + "&hierarchical=true"
+ + "&order=ASC"
+ + "&page=" + pageNumber;
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdateCommentsResponse(jsonObject, blogId, pageNumber, resultListener);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED);
+ }
+ };
+ AppLog.d(AppLog.T.READER, "updating comments");
+ WordPress.getRestClientUtilsV1_1().get(path, null, null, listener, errorListener);
+ }
+ private static void handleUpdateCommentsResponse(final JSONObject jsonObject,
+ final long blogId,
+ final int pageNumber,
+ final ReaderActions.UpdateResultListener resultListener) {
+ if (jsonObject == null) {
+ resultListener.onUpdateResult(ReaderActions.UpdateResult.FAILED);
+ return;
+ }
+
+ new Thread() {
+ @Override
+ public void run() {
+ final boolean hasNewComments;
+
+ ReaderDatabase.getWritableDb().beginTransaction();
+ try {
+ ReaderCommentList serverComments = new ReaderCommentList();
+ JSONArray jsonCommentList = jsonObject.optJSONArray("comments");
+ if (jsonCommentList != null) {
+ for (int i = 0; i < jsonCommentList.length(); i++) {
+ JSONObject jsonComment = jsonCommentList.optJSONObject(i);
+
+ // extract this comment and add it to the list
+ ReaderComment comment = ReaderComment.fromJson(jsonComment, blogId);
+ comment.pageNumber = pageNumber;
+ serverComments.add(comment);
+
+ // extract and save likes for this comment
+ JSONObject jsonLikes = JSONUtils.getJSONChild(jsonComment, "meta/data/likes");
+ if (jsonLikes != null) {
+ ReaderUserList likingUsers = ReaderUserList.fromJsonLikes(jsonLikes);
+ ReaderUserTable.addOrUpdateUsers(likingUsers);
+ ReaderLikeTable.setLikesForComment(comment, likingUsers.getUserIds());
+ }
+ }
+ }
+
+ hasNewComments = (serverComments.size() > 0);
+
+ // save to db regardless of whether any are new so changes to likes are stored
+ ReaderCommentTable.addOrUpdateComments(serverComments);
+ ReaderDatabase.getWritableDb().setTransactionSuccessful();
+ } finally {
+ ReaderDatabase.getWritableDb().endTransaction();
+ }
+
+ ReaderActions.UpdateResult result =
+ (hasNewComments ? ReaderActions.UpdateResult.HAS_NEW : ReaderActions.UpdateResult.UNCHANGED);
+ resultListener.onUpdateResult(result);
+ }
+ }.start();
+ }
+}
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);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderSearchService.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderSearchService.java
new file mode 100644
index 000000000..457a7b69d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderSearchService.java
@@ -0,0 +1,138 @@
+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.support.annotation.NonNull;
+
+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.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.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.UrlUtils;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * service which searches for reader posts on wordpress.com
+ */
+
+public class ReaderSearchService extends Service {
+
+ private static final String ARG_QUERY = "query";
+ private static final String ARG_OFFSET = "offset";
+
+ public static void startService(Context context, @NonNull String query, int offset) {
+ Intent intent = new Intent(context, ReaderSearchService.class);
+ intent.putExtra(ARG_QUERY, query);
+ intent.putExtra(ARG_OFFSET, offset);
+ context.startService(intent);
+ }
+
+ public static void stopService(Context context) {
+ if (context == null) return;
+
+ Intent intent = new Intent(context, ReaderSearchService.class);
+ context.stopService(intent);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.READER, "reader search service > created");
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.READER, "reader search service > destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ return START_NOT_STICKY;
+ }
+
+ String query = intent.getStringExtra(ARG_QUERY);
+ int offset = intent.getIntExtra(ARG_OFFSET, 0);
+ startSearch(query, offset);
+
+ return START_NOT_STICKY;
+ }
+
+ private void startSearch(final String query, final int offset) {
+ String path = "read/search?q="
+ + UrlUtils.urlEncode(query)
+ + "&number=" + ReaderConstants.READER_MAX_SEARCH_POSTS_TO_REQUEST
+ + "&offset=" + offset
+ + "&meta=site,likes";
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (jsonObject != null) {
+ handleSearchResponse(query, offset, jsonObject);
+ } else {
+ EventBus.getDefault().post(new ReaderEvents.SearchPostsEnded(query, offset, false));
+ }
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ EventBus.getDefault().post(new ReaderEvents.SearchPostsEnded(query, offset, false));
+ }
+ };
+
+ AppLog.d(AppLog.T.READER, "reader search service > starting search for " + query);
+ EventBus.getDefault().post(new ReaderEvents.SearchPostsStarted(query, offset));
+ WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener);
+ }
+
+ private static void handleSearchResponse(final String query, final int offset, final JSONObject jsonObject) {
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderPostList serverPosts = ReaderPostList.fromJson(jsonObject);
+
+ // we want search results to be sorted based on their offset - this works because
+ // ReaderPostTable.getPostsWithTag() sorts by sort_index in descending order
+ int sortIndex = -offset - 1;
+ for (ReaderPost post: serverPosts) {
+ post.sortIndex = sortIndex;
+ sortIndex--;
+ }
+
+ ReaderPostTable.addOrUpdatePosts(getTagForSearchQuery(query), serverPosts);
+ EventBus.getDefault().post(new ReaderEvents.SearchPostsEnded(query, offset, true));
+ }
+ }.start();
+ }
+
+ /*
+ * used when storing search results in the reader post table
+ */
+ public static ReaderTag getTagForSearchQuery(@NonNull String query) {
+ String trimQuery = query != null ? query.trim() : "";
+ String slug = ReaderUtils.sanitizeWithDashes(trimQuery);
+ return new ReaderTag(slug, trimQuery, trimQuery, null, ReaderTagType.SEARCH);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderUpdateService.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderUpdateService.java
new file mode 100644
index 000000000..f3590ac78
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderUpdateService.java
@@ -0,0 +1,331 @@
+package org.wordpress.android.ui.reader.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.IBinder;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderBlogTable;
+import org.wordpress.android.datasets.ReaderDatabase;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.ReaderBlogList;
+import org.wordpress.android.models.ReaderRecommendBlogList;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagList;
+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.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+
+import java.util.EnumSet;
+import java.util.Iterator;
+
+import de.greenrobot.event.EventBus;
+
+public class ReaderUpdateService extends Service {
+
+ /***
+ * service which updates followed/recommended tags and blogs for the Reader, relies
+ * on EventBus to notify of changes
+ */
+
+ public enum UpdateTask {
+ TAGS,
+ FOLLOWED_BLOGS,
+ RECOMMENDED_BLOGS
+ }
+
+ private EnumSet<UpdateTask> mCurrentTasks;
+ private static final String ARG_UPDATE_TASKS = "update_tasks";
+
+ public static void startService(Context context, EnumSet<UpdateTask> tasks) {
+ if (context == null || tasks == null || tasks.size() == 0) {
+ return;
+ }
+ Intent intent = new Intent(context, ReaderUpdateService.class);
+ intent.putExtra(ARG_UPDATE_TASKS, tasks);
+ context.startService(intent);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AppLog.i(AppLog.T.READER, "reader service > created");
+ }
+
+ @Override
+ public void onDestroy() {
+ AppLog.i(AppLog.T.READER, "reader service > destroyed");
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && intent.hasExtra(ARG_UPDATE_TASKS)) {
+ //noinspection unchecked
+ EnumSet<UpdateTask> tasks = (EnumSet<UpdateTask>) intent.getSerializableExtra(ARG_UPDATE_TASKS);
+ performTasks(tasks);
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ private void performTasks(EnumSet<UpdateTask> tasks) {
+ mCurrentTasks = EnumSet.copyOf(tasks);
+
+ // perform in priority order - we want to update tags first since without them
+ // the Reader can't show anything
+ if (tasks.contains(UpdateTask.TAGS)) {
+ updateTags();
+ }
+ if (tasks.contains(UpdateTask.FOLLOWED_BLOGS)) {
+ updateFollowedBlogs();
+ }
+ if (tasks.contains(UpdateTask.RECOMMENDED_BLOGS)) {
+ updateRecommendedBlogs();
+ }
+ }
+
+ private void taskCompleted(UpdateTask task) {
+ mCurrentTasks.remove(task);
+ if (mCurrentTasks.isEmpty()) {
+ allTasksCompleted();
+ }
+ }
+
+ private void allTasksCompleted() {
+ AppLog.i(AppLog.T.READER, "reader service > all tasks completed");
+ stopSelf();
+ }
+
+ /***
+ * update the tags the user is followed - also handles recommended (popular) tags since
+ * they're included in the response
+ */
+ private void updateTags() {
+ com.wordpress.rest.RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleUpdateTagsResponse(jsonObject);
+ }
+ };
+
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ taskCompleted(UpdateTask.TAGS);
+ }
+ };
+ AppLog.d(AppLog.T.READER, "reader service > updating tags");
+ WordPress.getRestClientUtilsV1_2().get("read/menu", null, null, listener, errorListener);
+ }
+
+ private void handleUpdateTagsResponse(final JSONObject jsonObject) {
+ new Thread() {
+ @Override
+ public void run() {
+ // get server topics, both default & followed - but use "recommended" for logged-out
+ // reader since user won't have any followed tags
+ ReaderTagList serverTopics = new ReaderTagList();
+ serverTopics.addAll(parseTags(jsonObject, "default", ReaderTagType.DEFAULT));
+ if (ReaderUtils.isLoggedOutReader()) {
+ serverTopics.addAll(parseTags(jsonObject, "recommended", ReaderTagType.FOLLOWED));
+ } else {
+ serverTopics.addAll(parseTags(jsonObject, "subscribed", ReaderTagType.FOLLOWED));
+ }
+
+ // parse topics from the response, detect whether they're different from local
+ ReaderTagList localTopics = new ReaderTagList();
+ localTopics.addAll(ReaderTagTable.getDefaultTags());
+ localTopics.addAll(ReaderTagTable.getFollowedTags());
+ localTopics.addAll(ReaderTagTable.getCustomListTags());
+
+ if (!localTopics.isSameList(serverTopics)) {
+ AppLog.d(AppLog.T.READER, "reader service > followed topics changed");
+ // if any local topics have been removed from the server, make sure to delete
+ // them locally (including their posts)
+ deleteTags(localTopics.getDeletions(serverTopics));
+ // now replace local topics with the server topics
+ ReaderTagTable.replaceTags(serverTopics);
+ // broadcast the fact that there are changes
+ EventBus.getDefault().post(new ReaderEvents.FollowedTagsChanged());
+ }
+
+ // save changes to recommended topics
+ if (!ReaderUtils.isLoggedOutReader()) {
+ ReaderTagList serverRecommended = parseTags(jsonObject, "recommended", ReaderTagType.RECOMMENDED);
+ ReaderTagList localRecommended = ReaderTagTable.getRecommendedTags(false);
+ if (!serverRecommended.isSameList(localRecommended)) {
+ AppLog.d(AppLog.T.READER, "reader service > recommended topics changed");
+ ReaderTagTable.setRecommendedTags(serverRecommended);
+ EventBus.getDefault().post(new ReaderEvents.RecommendedTagsChanged());
+ }
+ }
+
+ taskCompleted(UpdateTask.TAGS);
+ }
+ }.start();
+ }
+
+ /*
+ * parse a specific topic section from the topic response
+ */
+ private static ReaderTagList parseTags(JSONObject jsonObject, String name, ReaderTagType tagType) {
+ ReaderTagList topics = new ReaderTagList();
+
+ if (jsonObject == null) {
+ return topics;
+ }
+
+ JSONObject jsonTopics = jsonObject.optJSONObject(name);
+ if (jsonTopics == null) {
+ return topics;
+ }
+
+ Iterator<String> it = jsonTopics.keys();
+ while (it.hasNext()) {
+ String internalName = it.next();
+ JSONObject jsonTopic = jsonTopics.optJSONObject(internalName);
+ if (jsonTopic != null) {
+ String tagTitle = JSONUtils.getStringDecoded(jsonTopic, ReaderConstants.JSON_TAG_TITLE);
+ String tagDisplayName = JSONUtils.getStringDecoded(jsonTopic, ReaderConstants.JSON_TAG_DISPLAY_NAME);
+ String tagSlug = JSONUtils.getStringDecoded(jsonTopic, ReaderConstants.JSON_TAG_SLUG);
+ String endpoint = JSONUtils.getString(jsonTopic, ReaderConstants.JSON_TAG_URL);
+
+ // if the endpoint contains `read/list` then this is a custom list - these are
+ // included in the response as default tags
+ if (tagType == ReaderTagType.DEFAULT && endpoint.contains("/read/list/")) {
+ topics.add(new ReaderTag(tagSlug, tagDisplayName, tagTitle, endpoint, ReaderTagType.CUSTOM_LIST));
+ } else {
+ topics.add(new ReaderTag(tagSlug, tagDisplayName, tagTitle, endpoint, tagType));
+ }
+ }
+ }
+
+ return topics;
+ }
+
+ private static void deleteTags(ReaderTagList tagList) {
+ if (tagList == null || tagList.size() == 0) {
+ return;
+ }
+
+ SQLiteDatabase db = ReaderDatabase.getWritableDb();
+ db.beginTransaction();
+ try {
+ for (ReaderTag tag: tagList) {
+ ReaderTagTable.deleteTag(tag);
+ ReaderPostTable.deletePostsWithTag(tag);
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+
+ /***
+ * request the list of blogs the current user is following
+ */
+ private void updateFollowedBlogs() {
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleFollowedBlogsResponse(jsonObject);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ taskCompleted(UpdateTask.FOLLOWED_BLOGS);
+ }
+ };
+
+ AppLog.d(AppLog.T.READER, "reader service > updating followed blogs");
+ // request using ?meta=site,feed to get extra info
+ WordPress.getRestClientUtilsV1_1().get("read/following/mine?meta=site%2Cfeed", listener, errorListener);
+ }
+
+ private void handleFollowedBlogsResponse(final JSONObject jsonObject) {
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderBlogList serverBlogs = ReaderBlogList.fromJson(jsonObject);
+ ReaderBlogList localBlogs = ReaderBlogTable.getFollowedBlogs();
+
+ if (!localBlogs.isSameList(serverBlogs)) {
+ // always update the list of followed blogs if there are *any* changes between
+ // server and local (including subscription count, description, etc.)
+ ReaderBlogTable.setFollowedBlogs(serverBlogs);
+ // ...but only update the follow status and alert that followed blogs have
+ // changed if the server list doesn't have the same blogs as the local list
+ // (ie: a blog has been followed/unfollowed since local was last updated)
+ if (!localBlogs.hasSameBlogs(serverBlogs)) {
+ ReaderPostTable.updateFollowedStatus();
+ AppLog.i(AppLog.T.READER, "reader blogs service > followed blogs changed");
+ EventBus.getDefault().post(new ReaderEvents.FollowedBlogsChanged());
+ }
+ }
+
+ taskCompleted(UpdateTask.FOLLOWED_BLOGS);
+ }
+ }.start();
+ }
+
+ /***
+ * request the latest recommended blogs, replaces all local ones
+ */
+ private void updateRecommendedBlogs() {
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ handleRecommendedBlogsResponse(jsonObject);
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.READER, volleyError);
+ taskCompleted(UpdateTask.RECOMMENDED_BLOGS);
+ }
+ };
+
+ AppLog.d(AppLog.T.READER, "reader service > updating recommended blogs");
+ String path = "read/recommendations/mine/"
+ + "?source=mobile"
+ + "&number=" + Integer.toString(ReaderConstants.READER_MAX_RECOMMENDED_TO_REQUEST);
+ WordPress.getRestClientUtilsV1_1().get(path, listener, errorListener);
+ }
+ private void handleRecommendedBlogsResponse(final JSONObject jsonObject) {
+ new Thread() {
+ @Override
+ public void run() {
+ ReaderRecommendBlogList serverBlogs = ReaderRecommendBlogList.fromJson(jsonObject);
+ ReaderRecommendBlogList localBlogs = ReaderBlogTable.getRecommendedBlogs();
+
+ if (!localBlogs.isSameList(serverBlogs)) {
+ ReaderBlogTable.setRecommendedBlogs(serverBlogs);
+ EventBus.getDefault().post(new ReaderEvents.RecommendedBlogsChanged());
+ }
+
+ taskCompleted(UpdateTask.RECOMMENDED_BLOGS);
+ }
+ }.start();
+ }
+}