diff options
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/reader/services')
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(); + } +} |