diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index 429fd210a423..a011eae15895 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -389,6 +389,10 @@ android:name=".ui.reader.services.ReaderPostService" android:exported="false" android:label="Reader Post Service" /> + getQueryStrings() { - return getQueryStrings(null); - } - public static List getQueryStrings(String filter) { - List queries = new ArrayList<>(); - Cursor cursor; + /** + * Returns a cursor containing query strings previously typed by the user + * @param filter - filters the list using LIKE syntax (pass null for no filter) + * @param max - limit the list to this many items (pass zero for no limit) + */ + public static Cursor getQueryStringCursor(String filter, int max) { + String sql; + String[] args; if (TextUtils.isEmpty(filter)) { - cursor = ReaderDatabase.getReadableDb().rawQuery( - "SELECT query_string FROM tbl_search_history ORDER BY date_used DESC", null); + sql = "SELECT * FROM tbl_search_suggestions"; + args = null; } else { - String likeFilter = filter + "%"; - cursor = ReaderDatabase.getReadableDb().rawQuery( - "SELECT query_string FROM tbl_search_history WHERE query_string LIKE ? ORDER BY date_used DESC", new String[]{likeFilter}); + sql = "SELECT * FROM tbl_search_suggestions WHERE query_string LIKE ?"; + args = new String[]{filter + "%"}; } - try { - while (cursor.moveToNext()) { - queries.add(cursor.getString(0)); - } - return queries; - } finally { - SqlUtils.closeCursor(cursor); + sql += " ORDER BY date_used DESC"; + + if (max > 0) { + sql += " LIMIT " + max; } + + return ReaderDatabase.getReadableDb().rawQuery(sql, args); } } diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java index 550aba98ea8c..72907cdb3597 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java +++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java @@ -39,7 +39,7 @@ public class ReaderPost { private String primaryTag; // most popular tag on this post based on usage in blog private String secondaryTag; // second most popular tag on this post based on usage in blog - public long timestamp; // used for sorting + public double timestamp; // used for sorting private String published; private String url; @@ -116,14 +116,19 @@ public static ReaderPost fromJson(JSONObject json) { post.blogName = JSONUtils.getStringDecoded(json, "site_name"); post.published = JSONUtils.getString(json, "date"); - // the date a post was liked is only returned by the read/liked/ endpoint - if this exists, - // set it as the timestamp so posts are sorted by the date they were liked rather than the - // date they were published (the timestamp is used to sort posts when querying) - String likeDate = JSONUtils.getString(json, "date_liked"); - if (!TextUtils.isEmpty(likeDate)) { - post.timestamp = DateTimeUtils.iso8601ToTimestamp(likeDate); + // a post's timestamp determines its sort order + if (json.has("score")) { + // search results include a "score" that should be used for sorting + post.timestamp = json.optDouble("score"); } else { - post.timestamp = DateTimeUtils.iso8601ToTimestamp(post.published); + // liked posts should be sorted by the date they were liked, otherwise sort by the + // published date + String likeDate = JSONUtils.getString(json, "date_liked"); + if (!TextUtils.isEmpty(likeDate)) { + post.timestamp = DateTimeUtils.iso8601ToTimestamp(likeDate); + } else { + post.timestamp = DateTimeUtils.iso8601ToTimestamp(post.published); + } } // if the post is untitled, make up a title from the excerpt diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java index e6bcc629e07b..0fa75a8b2a8b 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java +++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java @@ -150,6 +150,7 @@ public static boolean isSameTag(ReaderTag tag1, ReaderTag tag2) { public boolean isPostsILike() { return tagType == ReaderTagType.DEFAULT && getEndpoint().endsWith("/read/liked"); } + public boolean isFollowedSites() { return tagType == ReaderTagType.DEFAULT && getEndpoint().endsWith("/read/following"); } diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java index f20ef28da3cd..5075e81d35f9 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java +++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java @@ -4,12 +4,14 @@ public enum ReaderTagType { FOLLOWED, DEFAULT, RECOMMENDED, - CUSTOM_LIST; + CUSTOM_LIST, + SEARCH; private static final int INT_DEFAULT = 0; private static final int INT_FOLLOWED = 1; private static final int INT_RECOMMENDED = 2; private static final int INT_CUSTOM_LIST = 3; + private static final int INT_SEARCH = 4; public static ReaderTagType fromInt(int value) { switch (value) { @@ -19,6 +21,8 @@ public static ReaderTagType fromInt(int value) { return FOLLOWED; case INT_CUSTOM_LIST: return CUSTOM_LIST; + case INT_SEARCH: + return SEARCH; default : return DEFAULT; } @@ -32,6 +36,8 @@ public int toInt() { return INT_RECOMMENDED; case CUSTOM_LIST: return INT_CUSTOM_LIST; + case SEARCH: + return INT_SEARCH; default : return INT_DEFAULT; } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java b/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java index b58c72b3aa41..835f3293e528 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java @@ -358,10 +358,6 @@ public void removeOnScrollListener(RecyclerView.OnScrollListener listener) { } } - public RecyclerView getInternalRecyclerView() { - return mRecyclerView; - } - public void hideToolbar(){ mAppBarLayout.setExpanded(false, true); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java index acc27c659bfc..754d22644a1e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java @@ -14,7 +14,6 @@ import org.wordpress.android.R; import org.wordpress.android.analytics.AnalyticsTracker; -import org.wordpress.android.datasets.ReaderSearchTable; import org.wordpress.android.models.AccountHelper; import org.wordpress.android.models.ReaderComment; import org.wordpress.android.models.ReaderPost; @@ -134,22 +133,6 @@ public static void showReaderTagPreview(Context context, ReaderTag tag) { context.startActivity(intent); } - public static void showReaderSearchResults(Context context, String query) { - if (TextUtils.isEmpty(query)) return; - - // record this search query - ReaderSearchTable.addOrUpdateQueryString(query); - - // TODO: track analytics - //AnalyticsTracker.track(AnalyticsTracker.Stat.???); - - Intent intent = new Intent(context, ReaderPostListActivity.class); - intent.putExtra(ReaderConstants.ARG_SEARCH_QUERY, query); - intent.putExtra(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.SEARCH_RESULTS); - context.startActivity(intent); - } - - /* * show comments for the passed Ids */ diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java index f79b9d1d0628..45440db5fd73 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java @@ -1,16 +1,17 @@ package org.wordpress.android.ui.reader; public class ReaderConstants { - public static final int READER_MAX_POSTS_TO_REQUEST = 20; // max # posts to request when updating posts - public static final int READER_MAX_POSTS_TO_DISPLAY = 200; // max # posts to display - public static final int READER_MAX_COMMENTS_TO_REQUEST = 20; // max # top-level comments to request when updating comments - public static final int READER_MAX_USERS_TO_DISPLAY = 500; // max # users to show in ReaderUserListActivity - public static final long READER_AUTO_UPDATE_DELAY_MINUTES = 10; // 10 minute delay between automatic updates - public static final int READER_MAX_RECOMMENDED_TO_REQUEST = 20; // max # of recommended blogs to request + public static final int READER_MAX_POSTS_TO_REQUEST = 20; // max # posts to request when updating posts + public static final int READER_MAX_SEARCH_POSTS_TO_REQUEST = 10; // max # posts to request when searching posts + public static final int READER_MAX_POSTS_TO_DISPLAY = 200; // max # posts to display + public static final int READER_MAX_COMMENTS_TO_REQUEST = 20; // max # top-level comments to request when updating comments + public static final int READER_MAX_USERS_TO_DISPLAY = 500; // max # users to show in ReaderUserListActivity + public static final long READER_AUTO_UPDATE_DELAY_MINUTES = 10; // 10 minute delay between automatic updates + public static final int READER_MAX_RECOMMENDED_TO_REQUEST = 20; // max # of recommended blogs to request - public static final int MIN_FEATURED_IMAGE_WIDTH = 640; // min width for an image to be suitable featured image + public static final int MIN_FEATURED_IMAGE_WIDTH = 640; // min width for an image to be suitable featured image - public static final String HTTP_REFERER_URL = "https://wordpress.com"; // referrer url for reader posts opened in a browser + public static final String HTTP_REFERER_URL = "https://wordpress.com"; // referrer url for reader posts opened in a browser // intent arguments / keys static final String ARG_TAG = "tag"; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java index b31adaaf528b..c76be80abc46 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java @@ -1,5 +1,7 @@ package org.wordpress.android.ui.reader; +import android.support.annotation.NonNull; + import org.wordpress.android.models.ReaderTag; import org.wordpress.android.ui.reader.actions.ReaderActions; import org.wordpress.android.ui.reader.services.ReaderPostService; @@ -63,6 +65,40 @@ public ReaderPostService.UpdateAction getAction() { } } + public static class SearchPostsStarted { + private final String mQuery; + private final int mOffset; + public SearchPostsStarted(@NonNull String query, int offset) { + mQuery = query; + mOffset = offset; + } + public String getQuery() { + return mQuery; + } + public int getOffset() { + return mOffset; + } + } + public static class SearchPostsEnded { + private final String mQuery; + private final boolean mDidSucceed; + private final int mOffset; + public SearchPostsEnded(@NonNull String query, int offset, boolean didSucceed) { + mQuery = query; + mOffset = offset; + mDidSucceed = didSucceed; + } + public boolean didSucceed() { + return mDidSucceed; + } + public String getQuery() { + return mQuery; + } + public int getOffset() { + return mOffset; + } + } + public static class UpdateCommentsStarted {} public static class UpdateCommentsEnded { private final ReaderActions.UpdateResult mResult; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java index 36b44f9edb67..f32d190bd831 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java @@ -8,7 +8,6 @@ import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; -import android.text.TextUtils; import android.view.MenuItem; import org.wordpress.android.R; @@ -59,12 +58,6 @@ public void onCreate(Bundle savedInstanceState) { if (tag != null && savedInstanceState == null) { showListFragmentForTag(tag, mPostListType); } - } else if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) { - String query = getIntent().getStringExtra(ReaderConstants.ARG_SEARCH_QUERY); - if (!TextUtils.isEmpty(query) && savedInstanceState == null) { - setTitle(String.format(getString(R.string.reader_title_search_results), query)); - showListFragmentForSearch(query); - } } } @@ -165,20 +158,6 @@ private void showListFragmentForFeed(long feedId) { .commit(); } - /* - * show fragment containing list of posts matching the passed search query - */ - private void showListFragmentForSearch(@NonNull String query) { - if (isFinishing()) { - return; - } - Fragment fragment = ReaderPostListFragment.newInstanceForSearch(query); - getFragmentManager() - .beginTransaction() - .replace(R.id.fragment_container, fragment, getString(R.string.fragment_tag_reader_post_list)) - .commit(); - } - private ReaderPostListFragment getListFragment() { Fragment fragment = getFragmentManager().findFragmentByTag(getString(R.string.fragment_tag_reader_post_list)); if (fragment == null) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java index 33b3d6051dcb..2795884fa0c9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -19,6 +19,7 @@ import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.AdapterView; +import android.widget.AutoCompleteTextView; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; @@ -28,6 +29,7 @@ import org.wordpress.android.datasets.ReaderBlogTable; import org.wordpress.android.datasets.ReaderDatabase; import org.wordpress.android.datasets.ReaderPostTable; +import org.wordpress.android.datasets.ReaderSearchTable; import org.wordpress.android.datasets.ReaderTagTable; import org.wordpress.android.models.FilterCriteria; import org.wordpress.android.models.ReaderPost; @@ -48,6 +50,7 @@ import org.wordpress.android.ui.reader.adapters.ReaderSearchSuggestionAdapter; import org.wordpress.android.ui.reader.services.ReaderPostService; import org.wordpress.android.ui.reader.services.ReaderPostService.UpdateAction; +import org.wordpress.android.ui.reader.services.ReaderSearchService; import org.wordpress.android.ui.reader.services.ReaderUpdateService; import org.wordpress.android.ui.reader.services.ReaderUpdateService.UpdateTask; import org.wordpress.android.ui.reader.utils.ReaderUtils; @@ -57,7 +60,6 @@ import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.DateTimeUtils; -import org.wordpress.android.util.DisplayUtils; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.WPActivityUtils; @@ -191,19 +193,6 @@ public static ReaderPostListFragment newInstanceForFeed(long feedId) { return fragment; } - public static ReaderPostListFragment newInstanceForSearch(@NonNull String query) { - AppLog.d(T.READER, "reader post list > newInstance (search)"); - - Bundle args = new Bundle(); - args.putString(ReaderConstants.ARG_SEARCH_QUERY, query); - args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.SEARCH_RESULTS); - - ReaderPostListFragment fragment = new ReaderPostListFragment(); - fragment.setArguments(args); - - return fragment; - } - @Override public void setArguments(Bundle args) { super.setArguments(args); @@ -266,16 +255,22 @@ public void onPause() { @Override public void onResume() { super.onResume(); - checkAdapter(); + checkPostAdapter(); if (mWasPaused) { AppLog.d(T.READER, "reader post list > resumed from paused state"); mWasPaused = false; - if (getPostListType().equals(ReaderPostListType.TAG_FOLLOWED)) { + if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) { resumeFollowedTag(); } else { refreshPosts(); } + + // if the user was searching, make sure the filter toolbar is showing + // so the user can see the search keyword they entered + if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) { + mRecyclerView.showToolbar(); + } } } @@ -326,7 +321,7 @@ public void onStop() { /* * ensures the adapter is created and posts are updated if they haven't already been */ - private void checkAdapter() { + private void checkPostAdapter() { if (isAdded() && mRecyclerView.getAdapter() == null) { mRecyclerView.setAdapter(getPostAdapter()); @@ -341,6 +336,16 @@ private void checkAdapter() { } } + /* + * reset the post adapter to initial state and create it again using the passed list type + */ + private void resetPostAdapter(ReaderPostListType postListType) { + mPostListType = postListType; + mPostAdapter = null; + mRecyclerView.setAdapter(null); + mRecyclerView.setAdapter(getPostAdapter()); + } + @SuppressWarnings("unused") public void onEventMainThread(ReaderEvents.FollowedTagsChanged event) { if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) { @@ -501,7 +506,8 @@ public void onShowCustomEmptyView (EmptyViewMessageType emptyViewMsgType) { getResources().getDimensionPixelSize(R.dimen.margin_extra_large) + spacingHorizontal); // add a menu to the filtered recycler's toolbar - if (!ReaderUtils.isLoggedOutReader() && getPostListType() == ReaderPostListType.TAG_FOLLOWED) { + if (!ReaderUtils.isLoggedOutReader() + && (getPostListType() == ReaderPostListType.TAG_FOLLOWED || getPostListType() == ReaderPostListType.SEARCH_RESULTS)) { setupRecyclerToolbar(); } @@ -546,16 +552,32 @@ public boolean onMenuItemClick(MenuItem item) { mSearchView.setIconifiedByDefault(true); mSearchView.setIconified(true); + // this is hacky, but we want to change the SearchView's autocomplete to show suggestions + // after a single character is typed, and there's no less hacky way to do this... + View view = mSearchView.findViewById(android.support.v7.appcompat.R.id.search_src_text); + if (view instanceof AutoCompleteTextView) { + ((AutoCompleteTextView) view).setThreshold(1); + } + MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, new MenuItemCompat.OnActionExpandListener() { @Override public boolean onMenuItemActionExpand(MenuItem item) { - showSearchUI(); + resetPostAdapter(ReaderPostListType.SEARCH_RESULTS); + showSearchMessage(); + mSettingsMenuItem.setVisible(false); return true; } @Override public boolean onMenuItemActionCollapse(MenuItem item) { - hideSearchUI(); + hideSearchMessage(); + resetSearchSuggestionAdapter(); + mSettingsMenuItem.setVisible(true); + mCurrentSearchQuery = null; + + // return to the followed tag that was showing prior to searching + resetPostAdapter(ReaderPostListType.TAG_FOLLOWED); + return true; } }); @@ -563,81 +585,71 @@ public boolean onMenuItemActionCollapse(MenuItem item) { mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { - if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) { - // TODO: reuse existing fragment - } else { - ReaderActivityLauncher.showReaderSearchResults(getActivity(), query); - } - mSearchMenuItem.collapseActionView(); + submitSearchQuery(query); return true; } @Override public boolean onQueryTextChange(String newText) { - mSearchSuggestionAdapter.populate(newText); + if (TextUtils.isEmpty(newText)) { + showSearchMessage(); + } else { + populateSearchSuggestionAdapter(newText); + } return true; } } ); } - private void showSearchUI() { + /* + * start the search service to search for posts matching the current query - the passed + * offset is used during infinite scroll, pass zero for initial search + */ + private void updatePostsInCurrentSearch(int offset) { + ReaderSearchService.startService(getActivity(), mCurrentSearchQuery, offset); + } + + private void submitSearchQuery(@NonNull String query) { if (!isAdded()) return; - // hide settings icon - mSettingsMenuItem.setVisible(false); + // remember this query for future suggestions + ReaderSearchTable.addOrUpdateQueryString(query); - // create the suggestion adapter if it doesn't already exist, otherwise repopulate it - // so the latest suggestions appear - if (mSearchSuggestionAdapter == null) { - setupSearchSuggestions(); - } else { - mSearchSuggestionAdapter.populate(); - } - - // show message letting user know what they're querying, but only if the user is in - // portrait mode or the device is a tablet (since there's not enough space for the - // message when the virtual keyboard is visible) - boolean isLandscape = DisplayUtils.isLandscape(getActivity()); - boolean isTablet = DisplayUtils.isXLarge(getActivity()); - if (isTablet || !isLandscape) { - TextView txtSearchExplainer = (TextView) getView().findViewById(R.id.text_search_explainer); - if (txtSearchExplainer.getVisibility() != View.VISIBLE) { - AniUtils.fadeIn(txtSearchExplainer, AniUtils.Duration.LONG); - } - } + mSearchView.clearFocus(); // this will hide suggestions and the virtual keyboard + hideSearchMessage(); - // hide the recycler (post list) - RecyclerView recycler = mRecyclerView.getInternalRecyclerView(); - if (recycler != null && recycler.getVisibility() == View.VISIBLE) { - AniUtils.fadeOut(recycler, AniUtils.Duration.LONG); - } + // remove cached results for this search - search results are ephemeral so each search + // should be treated as a "fresh" one + ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(query); + ReaderPostTable.deletePostsWithTag(searchTag); + + mPostAdapter.setCurrentTag(searchTag); + mCurrentSearchQuery = query; + updatePostsInCurrentSearch(0); } - private void hideSearchUI() { + /* + * reuse "empty" view to let user know what they're querying + */ + private void showSearchMessage() { if (!isAdded()) return; - // redisplay settings icon - mSettingsMenuItem.setVisible(true); + // clear posts so only the empty view is visible + getPostAdapter().clear(); - // hide the explainer - TextView txtSearchExplainer = (TextView) getView().findViewById(R.id.text_search_explainer); - if (txtSearchExplainer.getVisibility() == View.VISIBLE) { - AniUtils.fadeOut(txtSearchExplainer, AniUtils.Duration.LONG); - } - txtSearchExplainer.setVisibility(View.GONE); + setEmptyTitleAndDescription(false); + showEmptyView(); + } - // show the recycler - RecyclerView recycler = mRecyclerView.getInternalRecyclerView(); - if (recycler != null && recycler.getVisibility() != View.VISIBLE) { - AniUtils.fadeIn(recycler, AniUtils.Duration.LONG); - } + private void hideSearchMessage() { + hideEmptyView(); } /* * create and assign the suggestion adapter for the search view */ - private void setupSearchSuggestions() { + private void createSearchSuggestionAdapter() { mSearchSuggestionAdapter = new ReaderSearchSuggestionAdapter(getActivity()); mSearchView.setSuggestionsAdapter(mSearchSuggestionAdapter); @@ -651,19 +663,62 @@ public boolean onSuggestionSelect(int position) { public boolean onSuggestionClick(int position) { String query = mSearchSuggestionAdapter.getSuggestion(position); if (!TextUtils.isEmpty(query)) { - mSearchView.setQuery(query, false); + mSearchView.setQuery(query, true); } return true; } }); } + private void populateSearchSuggestionAdapter(String query) { + if (mSearchSuggestionAdapter == null) { + createSearchSuggestionAdapter(); + } + mSearchSuggestionAdapter.setFilter(query); + } + + private void resetSearchSuggestionAdapter() { + mSearchView.setSuggestionsAdapter(null); + mSearchSuggestionAdapter = null; + } + /* * is the search input showing? */ private boolean isSearchViewExpanded() { return mSearchView != null && !mSearchView.isIconified(); } + + private boolean isSearchViewEmpty() { + return mSearchView != null && mSearchView.getQuery().length() == 0; + } + + @SuppressWarnings("unused") + public void onEventMainThread(ReaderEvents.SearchPostsStarted event) { + if (!isAdded()) return; + + UpdateAction updateAction = event.getOffset() == 0 ? UpdateAction.REQUEST_NEWER : UpdateAction.REQUEST_OLDER; + setIsUpdating(true, updateAction); + setEmptyTitleAndDescription(false); + } + + @SuppressWarnings("unused") + public void onEventMainThread(ReaderEvents.SearchPostsEnded event) { + if (!isAdded()) return; + + UpdateAction updateAction = event.getOffset() == 0 ? UpdateAction.REQUEST_NEWER : UpdateAction.REQUEST_OLDER; + setIsUpdating(false, updateAction); + + // load the results if the search succeeded and it's the current search - note that success + // means the search didn't fail, not necessarily that is has results - which is fine because + // if there aren't results then refreshing will show the empty message + if (event.didSucceed() + && getPostListType() == ReaderPostListType.SEARCH_RESULTS + && event.getQuery().equals(mCurrentSearchQuery)) { + refreshPosts(); + } + } + /* * called when user taps follow item in popup menu for a post */ @@ -739,7 +794,7 @@ public void onClick(View v) { } /* - * box/pages animation that appears when loading an empty list (only appears for tags) + * box/pages animation that appears when loading an empty list */ private boolean shouldShowBoxAndPagesAnimation() { return getPostListType().isTagType(); @@ -767,35 +822,50 @@ private void setEmptyTitleAndDescription(boolean requestFailed) { title = getString(R.string.reader_empty_posts_no_connection); } else if (requestFailed) { title = getString(R.string.reader_empty_posts_request_failed); - } else if (isUpdating()) { + } else if (isUpdating() && getPostListType() != ReaderPostListType.SEARCH_RESULTS) { title = getString(R.string.reader_empty_posts_in_tag_updating); - } else if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) { - title = getString(R.string.reader_empty_posts_in_blog); - } else if (getPostListType() == ReaderPostListType.TAG_FOLLOWED && hasCurrentTag()) { - if (getCurrentTag().isFollowedSites()) { - if (ReaderBlogTable.hasFollowedBlogs()) { - title = getString(R.string.reader_empty_followed_blogs_no_recent_posts_title); - description = getString(R.string.reader_empty_followed_blogs_no_recent_posts_description); - } else { - title = getString(R.string.reader_empty_followed_blogs_title); - description = getString(R.string.reader_empty_followed_blogs_description); - } - } else if (getCurrentTag().isPostsILike()) { - title = getString(R.string.reader_empty_posts_liked); - } else if (getCurrentTag().tagType == ReaderTagType.CUSTOM_LIST) { - title = getString(R.string.reader_empty_posts_in_custom_list); - } else { - title = getString(R.string.reader_empty_posts_in_tag); - } - } else if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) { - title = getString(R.string.reader_empty_posts_in_search_title); - description = String.format(getString(R.string.reader_empty_posts_in_search_description), mCurrentSearchQuery); } else { - title = getString(R.string.reader_empty_posts_in_tag); + switch (getPostListType()) { + case TAG_FOLLOWED: + if (getCurrentTag().isFollowedSites()) { + if (ReaderBlogTable.hasFollowedBlogs()) { + title = getString(R.string.reader_empty_followed_blogs_no_recent_posts_title); + description = getString(R.string.reader_empty_followed_blogs_no_recent_posts_description); + } else { + title = getString(R.string.reader_empty_followed_blogs_title); + description = getString(R.string.reader_empty_followed_blogs_description); + } + } else if (getCurrentTag().isPostsILike()) { + title = getString(R.string.reader_empty_posts_liked); + } else if (getCurrentTag().isListTopic()) { + title = getString(R.string.reader_empty_posts_in_custom_list); + } else { + title = getString(R.string.reader_empty_posts_in_tag); + } + break; + + case BLOG_PREVIEW: + title = getString(R.string.reader_empty_posts_in_blog); + break; + + case SEARCH_RESULTS: + if (isSearchViewEmpty() || TextUtils.isEmpty(mCurrentSearchQuery)) { + title = getString(R.string.reader_label_post_search_explainer); + } else if (isUpdating()) { + title = getString(R.string.reader_label_post_search_running); + } else { + title = getString(R.string.reader_empty_posts_in_search_title); + description = String.format(getString(R.string.reader_empty_posts_in_search_description), mCurrentSearchQuery); + } + break; + + default: + title = getString(R.string.reader_empty_posts_in_tag); + break; + } } setEmptyTitleAndDescription(title, description); - mEmptyViewBoxImages.setVisibility(shouldShowBoxAndPagesAnimation() ? View.VISIBLE : View.GONE); } private void setEmptyTitleAndDescription(@NonNull String title, String description) { @@ -811,6 +881,20 @@ private void setEmptyTitleAndDescription(@NonNull String title, String descripti descriptionView.setText(description); descriptionView.setVisibility(View.VISIBLE); } + + mEmptyViewBoxImages.setVisibility(shouldShowBoxAndPagesAnimation() ? View.VISIBLE : View.GONE); + } + + private void showEmptyView() { + if (isAdded()) { + mEmptyView.setVisibility(View.VISIBLE); + } + } + + private void hideEmptyView() { + if (isAdded()) { + mEmptyView.setVisibility(View.GONE); + } } /* @@ -825,12 +909,12 @@ public void onDataLoaded(boolean isEmpty) { mRecyclerView.setRefreshing(false); if (isEmpty) { setEmptyTitleAndDescription(false); - mEmptyView.setVisibility(View.VISIBLE); + showEmptyView(); if (shouldShowBoxAndPagesAnimation()) { startBoxAndPagesAnimation(); } } else { - mEmptyView.setVisibility(View.GONE); + hideEmptyView(); if (mRestorePosition > 0) { AppLog.d(T.READER, "reader post list > restoring position"); mRecyclerView.scrollRecycleViewToPosition(mRestorePosition); @@ -874,6 +958,15 @@ public void onRequestData() { AnalyticsTracker.track(AnalyticsTracker.Stat.READER_INFINITE_SCROLL); } break; + + case SEARCH_RESULTS: + ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(mCurrentSearchQuery); + int offset = ReaderPostTable.getNumPostsWithTag(searchTag); + if (offset < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY) { + updatePostsInCurrentSearch(offset); + AnalyticsTracker.track(AnalyticsTracker.Stat.READER_INFINITE_SCROLL); + } + break; } } }; @@ -896,7 +989,8 @@ private ReaderPostAdapter getPostAdapter() { } else if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) { mPostAdapter.setCurrentBlogAndFeed(mCurrentBlogId, mCurrentFeedId); } else if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) { - mPostAdapter.setCurrentSearchQuery(mCurrentSearchQuery); + ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(mCurrentSearchQuery); + mPostAdapter.setCurrentTag(searchTag); } } return mPostAdapter; @@ -1036,7 +1130,6 @@ private void reloadTags() { } } - /* * get posts for the current blog from the server */ @@ -1069,10 +1162,10 @@ public void onEventMainThread(ReaderEvents.UpdatePostsEnded event) { return; } - // don't show new posts if user is entering a search query - posts will automatically + // don't show new posts if user is searching - posts will automatically // appear when search is exited - if (isSearchViewExpanded()) { - AppLog.d(T.READER, "skipping post reload, search view is expanded"); + if (isSearchViewExpanded() + || getPostListType() == ReaderPostListType.SEARCH_RESULTS) { return; } @@ -1322,6 +1415,9 @@ public void onPostSelected(ReaderPost post) { post.blogId, post.postId); break; + case SEARCH_RESULTS: + ReaderActivityLauncher.showReaderPostDetail(getActivity(), post.blogId, post.postId); + break; } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java index 08f422170f2e..72e9a79741a7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java @@ -2,7 +2,6 @@ import android.content.Context; import android.os.AsyncTask; -import android.support.annotation.NonNull; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; @@ -47,14 +46,12 @@ public class ReaderPostAdapter extends RecyclerView.Adapter mSearchSuggestions; +public class ReaderSearchSuggestionAdapter extends CursorAdapter { + private static final int MAX_SUGGESTIONS = 5; + private String mCurrentFilter; public ReaderSearchSuggestionAdapter(Context context) { - super(context, - android.R.layout.simple_list_item_1, - null, - new String[]{"query"}, - new int[]{android.R.id.text1}, - 0); - populate(); + super(context, null, false); } - public void populate() { - populate(null); + /* + * populate the adapter using a cursor containing past searches that match the filter + */ + public void setFilter(String filter) { + // skip if unchanged + if (isCurrentFilter(filter) && getCursor() != null) { + return; + } + Cursor cursor = ReaderSearchTable.getQueryStringCursor(filter, MAX_SUGGESTIONS); + swapCursor(cursor); + mCurrentFilter = filter; } - public void populate(String filter) { - mSearchSuggestions = ReaderSearchTable.getQueryStrings(filter); - MatrixCursor cursor = new MatrixCursor(new String[]{"_id", "query"}); + /* + * forces setFilter() to always repopulate by skipping the isCurrentFilter() check + */ + private void reload() { + String newFilter = mCurrentFilter; + mCurrentFilter = null; + setFilter(newFilter); + } - int id = 0; - for (String query : mSearchSuggestions) { - cursor.addRow(new Object[] {id++, query}); + private boolean isCurrentFilter(String filter) { + if (TextUtils.isEmpty(filter) && TextUtils.isEmpty(mCurrentFilter)) { + return true; } - - swapCursor(cursor); + return filter != null && filter.equalsIgnoreCase(mCurrentFilter); } public String getSuggestion(int position) { - if (position < 0 || position > mSearchSuggestions.size() - 1) { + Cursor cursor = (Cursor) getItem(position); + if (cursor != null) { + return cursor.getString(cursor.getColumnIndex(ReaderSearchTable.COL_QUERY)); + } else { return null; } - return mSearchSuggestions.get(position); } + private class SuggestionViewHolder { + private final TextView txtSuggestion; + private final ImageView imgDelete; + + SuggestionViewHolder(View view) { + txtSuggestion = (TextView) view.findViewById(R.id.text_suggestion); + imgDelete = (ImageView) view.findViewById(R.id.image_delete); + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View view = LayoutInflater.from(context).inflate(R.layout.reader_listitem_suggestion, parent, false); + view.setTag(new SuggestionViewHolder(view)); + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + SuggestionViewHolder holder = (SuggestionViewHolder) view.getTag(); + final String query = cursor.getString(cursor.getColumnIndex(ReaderSearchTable.COL_QUERY)); + + holder.txtSuggestion.setText(query); + holder.imgDelete.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ReaderSearchTable.deleteQueryString(query); + reload(); + } + }); + } } 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 000000000000..ec262633dc39 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/ReaderSearchService.java @@ -0,0 +1,129 @@ +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.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); + if (ReaderPostTable.comparePosts(serverPosts).isNewOrChanged()) { + 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 slug = ReaderUtils.sanitizeWithDashes(query); + return new ReaderTag(slug, query, query, null, ReaderTagType.SEARCH); + } +} diff --git a/WordPress/src/main/res/layout/filtered_list_component.xml b/WordPress/src/main/res/layout/filtered_list_component.xml index 96c788e14402..1c5a60bb4309 100644 --- a/WordPress/src/main/res/layout/filtered_list_component.xml +++ b/WordPress/src/main/res/layout/filtered_list_component.xml @@ -15,6 +15,7 @@ style="@style/FilteredRecyclerViewToolbar" android:layout_width="match_parent" android:layout_height="@dimen/toolbar_height" + android:focusableInTouchMode="true" app:contentInsetLeft="0dp" app:contentInsetStart="0dp" app:layout_scrollFlags="scroll|enterAlways"> diff --git a/WordPress/src/main/res/layout/reader_empty_view.xml b/WordPress/src/main/res/layout/reader_empty_view.xml index ffb2b8ae4453..7b501ab3b80a 100644 --- a/WordPress/src/main/res/layout/reader_empty_view.xml +++ b/WordPress/src/main/res/layout/reader_empty_view.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:layout_marginTop="@dimen/toolbar_height" android:gravity="center" android:orientation="vertical" android:visibility="gone" @@ -13,7 +14,7 @@ android:id="@+id/layout_box_images" android:layout_width="wrap_content" android:layout_height="100dp" - android:layout_marginBottom="8dp" + android:layout_marginBottom="@dimen/margin_medium" android:visibility="gone" tools:visibility="visible"> @@ -68,6 +69,7 @@ android:layout_marginBottom="@dimen/empty_list_title_bottom_margin" android:layout_marginLeft="@dimen/empty_list_title_side_margin" android:layout_marginRight="@dimen/empty_list_title_side_margin" + android:layout_marginTop="@dimen/margin_medium" android:text="@string/reader_empty_posts_in_tag" app:fixWidowWords="true" /> diff --git a/WordPress/src/main/res/layout/reader_fragment_post_cards.xml b/WordPress/src/main/res/layout/reader_fragment_post_cards.xml index c8acb14a5097..66a14fe9c601 100644 --- a/WordPress/src/main/res/layout/reader_fragment_post_cards.xml +++ b/WordPress/src/main/res/layout/reader_fragment_post_cards.xml @@ -57,14 +57,4 @@ android:visibility="gone" tools:visibility="visible" /> - - diff --git a/WordPress/src/main/res/layout/reader_listitem_suggestion.xml b/WordPress/src/main/res/layout/reader_listitem_suggestion.xml new file mode 100644 index 000000000000..040f83a20520 --- /dev/null +++ b/WordPress/src/main/res/layout/reader_listitem_suggestion.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index e7cb63451328..3989939d5914 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1117,6 +1117,7 @@ SEND Load more posts Search all public WordPress.com blogs + Searching… Like