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