diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml
index 576ec9e1a808..35d2bd0058d8 100644
--- a/WordPress/src/main/AndroidManifest.xml
+++ b/WordPress/src/main/AndroidManifest.xml
@@ -264,7 +264,8 @@
+ android:theme="@style/Calypso.NoActionBar">
+
+
0) {
sql += " LIMIT " + Integer.toString(maxPosts);
@@ -716,7 +727,7 @@ public static ReaderPostList getPostsWithTag(ReaderTag tag, int maxPosts, boolea
public static ReaderPostList getPostsInBlog(long blogId, int maxPosts, boolean excludeTextColumn) {
String columns = (excludeTextColumn ? COLUMN_NAMES_NO_TEXT : "tbl_posts.*");
- String sql = "SELECT " + columns + " FROM tbl_posts WHERE blog_id = ? ORDER BY tbl_posts.timestamp DESC";
+ String sql = "SELECT " + columns + " FROM tbl_posts WHERE blog_id = ? ORDER BY tbl_posts.sort_index DESC";
if (maxPosts > 0) {
sql += " LIMIT " + Integer.toString(maxPosts);
@@ -732,7 +743,7 @@ public static ReaderPostList getPostsInBlog(long blogId, int maxPosts, boolean e
public static ReaderPostList getPostsInFeed(long feedId, int maxPosts, boolean excludeTextColumn) {
String columns = (excludeTextColumn ? COLUMN_NAMES_NO_TEXT : "tbl_posts.*");
- String sql = "SELECT " + columns + " FROM tbl_posts WHERE feed_id = ? ORDER BY tbl_posts.timestamp DESC";
+ String sql = "SELECT " + columns + " FROM tbl_posts WHERE feed_id = ? ORDER BY tbl_posts.sort_index DESC";
if (maxPosts > 0) {
sql += " LIMIT " + Integer.toString(maxPosts);
@@ -769,7 +780,7 @@ public static ReaderBlogIdPostIdList getBlogIdPostIdsWithTag(ReaderTag tag, int
}
}
- sql += " ORDER BY tbl_posts.timestamp DESC";
+ sql += " ORDER BY tbl_posts.sort_index DESC";
if (maxPosts > 0) {
sql += " LIMIT " + Integer.toString(maxPosts);
@@ -793,7 +804,7 @@ public static ReaderBlogIdPostIdList getBlogIdPostIdsWithTag(ReaderTag tag, int
* same as getPostsInBlog() but only returns the blogId/postId pairs
*/
public static ReaderBlogIdPostIdList getBlogIdPostIdsInBlog(long blogId, int maxPosts) {
- String sql = "SELECT post_id FROM tbl_posts WHERE blog_id = ? ORDER BY tbl_posts.timestamp DESC";
+ String sql = "SELECT post_id FROM tbl_posts WHERE blog_id = ? ORDER BY tbl_posts.sort_index DESC";
if (maxPosts > 0) {
sql += " LIMIT " + Integer.toString(maxPosts);
@@ -847,7 +858,7 @@ private static ReaderPost getPostFromCursor(Cursor c) {
post.setShortUrl(c.getString(c.getColumnIndex("short_url")));
post.setPostAvatar(c.getString(c.getColumnIndex("post_avatar")));
- post.timestamp = c.getLong(c.getColumnIndex("timestamp"));
+ post.sortIndex = c.getDouble(c.getColumnIndex("sort_index"));
post.setPublished(c.getString(c.getColumnIndex("published")));
post.numReplies = c.getInt(c.getColumnIndex("num_replies"));
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderSearchTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderSearchTable.java
new file mode 100644
index 000000000000..9b527d3b6f9c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderSearchTable.java
@@ -0,0 +1,84 @@
+package org.wordpress.android.datasets;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.SqlUtils;
+
+import java.util.Date;
+
+/**
+ * search suggestion table - populated by user's reader search history
+ */
+public class ReaderSearchTable {
+
+ public static final String COL_ID = "_id";
+ public static final String COL_QUERY = "query_string";
+
+ protected static void createTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE tbl_search_suggestions ("
+ + " _id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + " query_string TEXT NOT NULL COLLATE NOCASE,"
+ + " date_used TEXT)");
+ db.execSQL("CREATE UNIQUE INDEX idx_search_suggestions_query ON tbl_search_suggestions(query_string)");
+ }
+
+ protected static void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS tbl_search_suggestions");
+ }
+
+ /*
+ * adds the passed query string, updating the usage date
+ */
+ public static void addOrUpdateQueryString(@NonNull String query) {
+ String date = DateTimeUtils.javaDateToIso8601(new Date());
+
+ SQLiteStatement stmt = ReaderDatabase.getWritableDb().compileStatement(
+ "INSERT OR REPLACE INTO tbl_search_suggestions (query_string, date_used) VALUES (?1,?2)");
+ try {
+ stmt.bindString(1, query);
+ stmt.bindString(2, date);
+ stmt.execute();
+ } finally {
+ SqlUtils.closeStatement(stmt);
+ }
+ }
+
+ public static void deleteQueryString(@NonNull String query) {
+ String[]args = new String[]{query};
+ ReaderDatabase.getWritableDb().delete("tbl_search_suggestions", "query_string=?", args);
+ }
+
+ public static void deleteAllQueries() {
+ SqlUtils.deleteAllRowsInTable(ReaderDatabase.getWritableDb(), "tbl_search_suggestions");
+ }
+
+ /**
+ * 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)) {
+ sql = "SELECT * FROM tbl_search_suggestions";
+ args = null;
+ } else {
+ sql = "SELECT * FROM tbl_search_suggestions WHERE query_string LIKE ?";
+ args = new String[]{filter + "%"};
+ }
+
+ 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 7ea9a62afd96..87162e8204f2 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 sortIndex;
private String published;
private String url;
@@ -116,14 +116,15 @@ 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);
+ // sort index determines how posts are sorted - this is a "score" for search results,
+ // liked date for liked posts, and published date for all others
+ if (json.has("score")) {
+ post.sortIndex = json.optDouble("score");
+ } else if (json.has("date_liked")) {
+ String likeDate = JSONUtils.getString(json, "date_liked");
+ post.sortIndex = DateTimeUtils.iso8601ToTimestamp(likeDate);
} else {
- post.timestamp = DateTimeUtils.iso8601ToTimestamp(post.published);
+ post.sortIndex = 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 e41604179971..835f3293e528 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java
@@ -1,6 +1,7 @@
package org.wordpress.android.ui;
import android.content.Context;
+import android.support.annotation.MenuRes;
import android.support.design.widget.AppBarLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -8,11 +9,11 @@
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
+import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
-import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.Spinner;
@@ -41,7 +42,6 @@ public class FilteredRecyclerView extends RelativeLayout {
private RecyclerView mRecyclerView;
private TextView mEmptyView;
private View mCustomEmptyView;
- private LinearLayout mCustomComponentsContainer;
private Toolbar mToolbar;
private AppBarLayout mAppBarLayout;
@@ -124,7 +124,6 @@ private void init() {
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical));
- mCustomComponentsContainer = (LinearLayout) findViewById(R.id.custom_components_toolbar_container);
mToolbar = (Toolbar) findViewById(R.id.toolbar_with_spinner);
mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout);
@@ -278,15 +277,12 @@ public void hideLoadingProgress() {
}
/*
- * use this to add custom control/component views to your toolbar. Components are added on the right side of the
- * toolbar */
- public void addToolbarCustomControl(View v, OnClickListener clickListener){
- if (v != null){
- mCustomComponentsContainer.addView(v);
- if (clickListener != null){
- v.setOnClickListener(clickListener);
- }
- }
+ * add a menu to the right side of the toolbar, returns the toolbar menu so the caller
+ * can act upon it
+ */
+ public Menu addToolbarMenu(@MenuRes int menuResId) {
+ mToolbar.inflateMenu(menuResId);
+ return mToolbar.getMenu();
}
public void setToolbarBackgroundColor(int color){
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
index 962d01c30d78..c2cc395c2a83 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java
@@ -83,6 +83,14 @@ public interface OnScrollToTopListener {
void onScrollToTop();
}
+ /*
+ * tab fragments implement this and return true if the fragment handles the back button
+ * and doesn't want the activity to handle it as well
+ */
+ public interface OnActivityBackPressedListener {
+ boolean onActivityBackPressed();
+ }
+
@Override
public void onCreate(Bundle savedInstanceState) {
ProfilingUtils.split("WPMainActivity.onCreate");
@@ -330,6 +338,23 @@ protected void onResume() {
ProfilingUtils.stop();
}
+ @Override
+ public void onBackPressed() {
+ // let the fragment handle the back button if it implements our OnParentBackPressedListener
+ Fragment fragment = getActiveFragment();
+ if (fragment instanceof OnActivityBackPressedListener) {
+ boolean handled = ((OnActivityBackPressedListener) fragment).onActivityBackPressed();
+ if (handled) {
+ return;
+ }
+ }
+ super.onBackPressed();
+ }
+
+ private Fragment getActiveFragment() {
+ return mTabAdapter.getFragment(mViewPager.getCurrentItem());
+ }
+
private void checkMagicLinkSignIn() {
if (getIntent() != null) {
if (getIntent().getBooleanExtra(MagicLinkSignInActivity.MAGIC_LOGIN, false)) {
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 1501c331f72d..c6249c00c9a2 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
@@ -131,7 +131,6 @@ public static void showReaderTagPreview(Context context, ReaderTag tag) {
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 4fac473ab0a7..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";
@@ -23,6 +24,7 @@ public class ReaderConstants {
static final String ARG_POST_LIST_TYPE = "post_list_type";
static final String ARG_CONTENT = "content";
static final String ARG_IS_SINGLE_POST = "is_single_post";
+ static final String ARG_SEARCH_QUERY = "search_query";
static final String KEY_ALREADY_UPDATED = "already_updated";
static final String KEY_ALREADY_REQUESTED = "already_requested";
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 5acb0dcf6dfe..04424f3a8254 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;
@@ -68,6 +70,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 f24020127e72..303041f89cbe 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
@@ -105,7 +105,7 @@ public void onSaveInstanceState(@NonNull Bundle outState) {
@Override
public void onBackPressed() {
ReaderPostListFragment fragment = getListFragment();
- if (fragment == null || !fragment.goBackInTagHistory()) {
+ if (fragment == null || !fragment.onActivityBackPressed()) {
super.onBackPressed();
}
}
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 be6d6799fcc6..7cb81395cab2 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
@@ -4,16 +4,24 @@
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
+import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.ListPopupWindow;
import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.SearchView;
+import android.text.Html;
+import android.text.TextUtils;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
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;
@@ -23,6 +31,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;
@@ -40,8 +49,10 @@
import org.wordpress.android.ui.reader.actions.ReaderBlogActions.BlockedBlogResult;
import org.wordpress.android.ui.reader.adapters.ReaderMenuAdapter;
import org.wordpress.android.ui.reader.adapters.ReaderPostAdapter;
+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;
@@ -51,6 +62,7 @@
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;
@@ -70,20 +82,29 @@ public class ReaderPostListFragment extends Fragment
implements ReaderInterfaces.OnPostSelectedListener,
ReaderInterfaces.OnTagSelectedListener,
ReaderInterfaces.OnPostPopupListener,
+ WPMainActivity.OnActivityBackPressedListener,
WPMainActivity.OnScrollToTopListener {
private ReaderPostAdapter mPostAdapter;
+ private ReaderSearchSuggestionAdapter mSearchSuggestionAdapter;
+
private FilteredRecyclerView mRecyclerView;
private boolean mFirstLoad = true;
private final ReaderTagList mTags = new ReaderTagList();
private View mNewPostsBar;
private View mEmptyView;
+ private View mEmptyViewBoxImages;
private ProgressBar mProgress;
+ private SearchView mSearchView;
+ private MenuItem mSettingsMenuItem;
+ private MenuItem mSearchMenuItem;
+
private ReaderTag mCurrentTag;
private long mCurrentBlogId;
private long mCurrentFeedId;
+ private String mCurrentSearchQuery;
private ReaderPostListType mPostListType;
private int mRestorePosition;
@@ -189,6 +210,7 @@ public void setArguments(Bundle args) {
mCurrentBlogId = args.getLong(ReaderConstants.ARG_BLOG_ID);
mCurrentFeedId = args.getLong(ReaderConstants.ARG_FEED_ID);
+ mCurrentSearchQuery = args.getString(ReaderConstants.ARG_SEARCH_QUERY);
if (getPostListType() == ReaderPostListType.TAG_PREVIEW && hasCurrentTag()) {
mTagPreviewHistory.push(getCurrentTagName());
@@ -211,6 +233,9 @@ public void onCreate(Bundle savedInstanceState) {
if (savedInstanceState.containsKey(ReaderConstants.ARG_FEED_ID)) {
mCurrentFeedId = savedInstanceState.getLong(ReaderConstants.ARG_FEED_ID);
}
+ if (savedInstanceState.containsKey(ReaderConstants.ARG_SEARCH_QUERY)) {
+ mCurrentSearchQuery = savedInstanceState.getString(ReaderConstants.ARG_SEARCH_QUERY);
+ }
if (savedInstanceState.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) {
mPostListType = (ReaderPostListType) savedInstanceState.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE);
}
@@ -233,16 +258,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();
+ }
}
}
@@ -293,7 +324,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());
@@ -308,6 +339,17 @@ 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());
+ mRecyclerView.setSwipeToRefreshEnabled(isSwipeToRefreshSupported());
+ }
+
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.FollowedTagsChanged event) {
if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) {
@@ -345,6 +387,7 @@ public void onSaveInstanceState(Bundle outState) {
outState.putLong(ReaderConstants.ARG_BLOG_ID, mCurrentBlogId);
outState.putLong(ReaderConstants.ARG_FEED_ID, mCurrentFeedId);
+ outState.putString(ReaderConstants.ARG_SEARCH_QUERY, mCurrentSearchQuery);
outState.putBoolean(ReaderConstants.KEY_WAS_PAUSED, mWasPaused);
outState.putBoolean(ReaderConstants.KEY_ALREADY_UPDATED, mHasUpdatedPosts);
outState.putBoolean(ReaderConstants.KEY_FIRST_LOAD, mFirstLoad);
@@ -372,7 +415,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa
// view that appears when current tag/blog has no posts - box images in this view are
// displayed and animated for tags only
mEmptyView = rootView.findViewById(R.id.empty_custom_view);
- mEmptyView.findViewById(R.id.layout_box_images).setVisibility(shouldShowBoxAndPagesAnimation() ? View.VISIBLE : View.GONE);
+ mEmptyViewBoxImages = mEmptyView.findViewById(R.id.layout_box_images);
mRecyclerView.setLogT(AppLog.T.READER);
mRecyclerView.setCustomEmptyView(mEmptyView);
@@ -458,16 +501,6 @@ public void onShowCustomEmptyView (EmptyViewMessageType emptyViewMsgType) {
int spacingVertical = context.getResources().getDimensionPixelSize(R.dimen.reader_card_gutters);
mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical, false));
- if (!ReaderUtils.isLoggedOutReader()) {
- View settingsControl = inflater.inflate(R.layout.filtered_recyclerview_settings_control, null);
- mRecyclerView.addToolbarCustomControl(settingsControl, new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- ReaderActivityLauncher.showReaderSubs(v.getContext());
- }
- });
- ReaderUtils.setBackgroundToRoundRipple(settingsControl);
- }
// the following will change the look and feel of the toolbar to match the current design
mRecyclerView.setToolbarBackgroundColor(ContextCompat.getColor(context, R.color.blue_medium));
mRecyclerView.setToolbarSpinnerTextColor(ContextCompat.getColor(context, R.color.white));
@@ -476,6 +509,14 @@ public void onClick(View v) {
getResources().getDimensionPixelSize(R.dimen.margin_medium) + spacingHorizontal,
getResources().getDimensionPixelSize(R.dimen.margin_extra_large) + spacingHorizontal);
+ // add a menu to the filtered recycler's toolbar
+ if (!ReaderUtils.isLoggedOutReader()
+ && (getPostListType() == ReaderPostListType.TAG_FOLLOWED || getPostListType() == ReaderPostListType.SEARCH_RESULTS)) {
+ setupRecyclerToolbar();
+ }
+
+ mRecyclerView.setSwipeToRefreshEnabled(isSwipeToRefreshSupported());
+
// bar that appears at top after new posts are loaded
mNewPostsBar = rootView.findViewById(R.id.layout_new_posts);
mNewPostsBar.setVisibility(View.GONE);
@@ -487,7 +528,6 @@ public void onClick(View view) {
}
});
-
// progress bar that appears when loading more posts
mProgress = (ProgressBar) rootView.findViewById(R.id.progress_footer);
mProgress.setVisibility(View.GONE);
@@ -495,6 +535,209 @@ public void onClick(View view) {
return rootView;
}
+ /*
+ * adds a menu to the recycler's toolbar containing settings & search items - only called
+ * for followed tags
+ */
+ private void setupRecyclerToolbar() {
+ Menu menu = mRecyclerView.addToolbarMenu(R.menu.reader_list);
+ mSettingsMenuItem = menu.findItem(R.id.menu_reader_settings);
+ mSearchMenuItem = menu.findItem(R.id.menu_reader_search);
+
+ mSettingsMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ ReaderActivityLauncher.showReaderSubs(getActivity());
+ return true;
+ }
+ });
+
+ mSearchView = (SearchView) mSearchMenuItem.getActionView();
+ mSearchView.setQueryHint(getString(R.string.reader_hint_post_search));
+ mSearchView.setSubmitButtonEnabled(false);
+ mSearchView.setIconifiedByDefault(true);
+ mSearchView.setIconified(true);
+
+ // force the search view to take up as much horizontal space as possible (without this
+ // it looks truncated on landscape)
+ int maxWidth = DisplayUtils.getDisplayPixelWidth(getActivity());
+ mSearchView.setMaxWidth(maxWidth);
+
+ // 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) {
+ resetPostAdapter(ReaderPostListType.SEARCH_RESULTS);
+ showSearchMessage();
+ mSettingsMenuItem.setVisible(false);
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ hideSearchMessage();
+ resetSearchSuggestionAdapter();
+ mSettingsMenuItem.setVisible(true);
+ mCurrentSearchQuery = null;
+
+ // return to the followed tag that was showing prior to searching
+ resetPostAdapter(ReaderPostListType.TAG_FOLLOWED);
+
+ return true;
+ }
+ });
+
+ mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ submitSearchQuery(query);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ if (TextUtils.isEmpty(newText)) {
+ showSearchMessage();
+ } else {
+ populateSearchSuggestionAdapter(newText);
+ }
+ return true;
+ }
+ }
+ );
+ }
+
+ /*
+ * 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;
+
+ mSearchView.clearFocus(); // this will hide suggestions and the virtual keyboard
+ hideSearchMessage();
+
+ // remember this query for future suggestions
+ String trimQuery = query != null ? query.trim() : "";
+ ReaderSearchTable.addOrUpdateQueryString(trimQuery);
+
+ // remove cached results for this search - search results are ephemeral so each search
+ // should be treated as a "fresh" one
+ ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(trimQuery);
+ ReaderPostTable.deletePostsWithTag(searchTag);
+
+ mPostAdapter.setCurrentTag(searchTag);
+ mCurrentSearchQuery = trimQuery;
+ updatePostsInCurrentSearch(0);
+
+ // track that the user performed a search
+ if (!trimQuery.equals("")) {
+ Map properties = new HashMap<>();
+ properties.put("query", trimQuery);
+ AnalyticsTracker.track(AnalyticsTracker.Stat.READER_SEARCH_LOADED, properties);
+ }
+ }
+
+ /*
+ * reuse "empty" view to let user know what they're querying
+ */
+ private void showSearchMessage() {
+ if (!isAdded()) return;
+
+ // clear posts so only the empty view is visible
+ getPostAdapter().clear();
+
+ setEmptyTitleAndDescription(false);
+ showEmptyView();
+ }
+
+ private void hideSearchMessage() {
+ hideEmptyView();
+ }
+
+ /*
+ * create and assign the suggestion adapter for the search view
+ */
+ private void createSearchSuggestionAdapter() {
+ mSearchSuggestionAdapter = new ReaderSearchSuggestionAdapter(getActivity());
+ mSearchView.setSuggestionsAdapter(mSearchSuggestionAdapter);
+
+ mSearchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
+ @Override
+ public boolean onSuggestionSelect(int position) {
+ return false;
+ }
+
+ @Override
+ public boolean onSuggestionClick(int position) {
+ String query = mSearchSuggestionAdapter.getSuggestion(position);
+ if (!TextUtils.isEmpty(query)) {
+ 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
*/
@@ -570,15 +813,14 @@ 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();
}
+
private void startBoxAndPagesAnimation() {
- if (!isAdded()) {
- return;
- }
+ if (!isAdded()) return;
ImageView page1 = (ImageView) mEmptyView.findViewById(R.id.empty_tags_box_page1);
ImageView page2 = (ImageView) mEmptyView.findViewById(R.id.empty_tags_box_page2);
@@ -590,51 +832,93 @@ private void startBoxAndPagesAnimation() {
}
private void setEmptyTitleAndDescription(boolean requestFailed) {
- if (!isAdded()) {
- return;
- }
+ if (!isAdded()) return;
- int titleResId;
- int descriptionResId = 0;
+ String title;
+ String description = null;
if (!NetworkUtils.isNetworkAvailable(getActivity())) {
- titleResId = R.string.reader_empty_posts_no_connection;
+ title = getString(R.string.reader_empty_posts_no_connection);
} else if (requestFailed) {
- titleResId = R.string.reader_empty_posts_request_failed;
- } else if (isUpdating()) {
- titleResId = R.string.reader_empty_posts_in_tag_updating;
- } else if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) {
- titleResId = R.string.reader_empty_posts_in_blog;
- } else if (getPostListType() == ReaderPostListType.TAG_FOLLOWED && hasCurrentTag()) {
- if (getCurrentTag().isFollowedSites()) {
- if (ReaderBlogTable.hasFollowedBlogs()) {
- titleResId = R.string.reader_empty_followed_blogs_no_recent_posts_title;
- descriptionResId = R.string.reader_empty_followed_blogs_no_recent_posts_description;
- } else {
- titleResId = R.string.reader_empty_followed_blogs_title;
- descriptionResId = R.string.reader_empty_followed_blogs_description;
- }
- } else if (getCurrentTag().isPostsILike()) {
- titleResId = R.string.reader_empty_posts_liked;
- } else if (getCurrentTag().tagType == ReaderTagType.CUSTOM_LIST) {
- titleResId = R.string.reader_empty_posts_in_custom_list;
- } else {
- titleResId = R.string.reader_empty_posts_in_tag;
- }
+ title = getString(R.string.reader_empty_posts_request_failed);
+ } else if (isUpdating() && getPostListType() != ReaderPostListType.SEARCH_RESULTS) {
+ title = getString(R.string.reader_empty_posts_in_tag_updating);
} else {
- titleResId = 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);
+ String formattedQuery = "" + mCurrentSearchQuery + "";
+ description = String.format(getString(R.string.reader_empty_posts_in_search_description), formattedQuery);
+ }
+ break;
+
+ default:
+ title = getString(R.string.reader_empty_posts_in_tag);
+ break;
+ }
}
+ setEmptyTitleAndDescription(title, description);
+ }
+
+ private void setEmptyTitleAndDescription(@NonNull String title, String description) {
+ if (!isAdded()) return;
+
TextView titleView = (TextView) mEmptyView.findViewById(R.id.title_empty);
- titleView.setText(getString(titleResId));
+ titleView.setText(title);
TextView descriptionView = (TextView) mEmptyView.findViewById(R.id.description_empty);
- if (descriptionResId == 0) {
+ if (description == null) {
descriptionView.setVisibility(View.INVISIBLE);
} else {
- descriptionView.setText(getString(descriptionResId));
+ if (description.contains("<") && description.contains(">")) {
+ descriptionView.setText(Html.fromHtml(description));
+ } else {
+ 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);
+ }
}
/*
@@ -649,12 +933,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);
@@ -698,6 +982,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;
}
}
};
@@ -719,6 +1012,9 @@ private ReaderPostAdapter getPostAdapter() {
mPostAdapter.setCurrentTag(getCurrentTag());
} else if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) {
mPostAdapter.setCurrentBlogAndFeed(mCurrentBlogId, mCurrentFeedId);
+ } else if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) {
+ ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(mCurrentSearchQuery);
+ mPostAdapter.setCurrentTag(searchTag);
}
}
return mPostAdapter;
@@ -782,12 +1078,28 @@ && getPostAdapter().isCurrentTag(tag)) {
updateCurrentTagIfTime();
}
+ /*
+ * called by the activity when user hits the back button - returns true if the back button
+ * is handled here and should be ignored by the activity
+ */
+ @Override
+ public boolean onActivityBackPressed() {
+ if (isSearchViewExpanded()) {
+ mSearchMenuItem.collapseActionView();
+ return true;
+ } else if (goBackInTagHistory()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
/*
* when previewing posts with a specific tag, a history of previewed tags is retained so
* the user can navigate back through them - this is faster and requires less memory
* than creating a new fragment for each previewed tag
*/
- boolean goBackInTagHistory() {
+ private boolean goBackInTagHistory() {
if (mTagPreviewHistory.empty()) {
return false;
}
@@ -842,7 +1154,6 @@ private void reloadTags() {
}
}
-
/*
* get posts for the current blog from the server
*/
@@ -875,6 +1186,13 @@ public void onEventMainThread(ReaderEvents.UpdatePostsEnded event) {
return;
}
+ // don't show new posts if user is searching - posts will automatically
+ // appear when search is exited
+ if (isSearchViewExpanded()
+ || getPostListType() == ReaderPostListType.SEARCH_RESULTS) {
+ return;
+ }
+
// determine whether to show the "new posts" bar - when this is shown, the newly
// downloaded posts aren't displayed until the user taps the bar - only appears
// when there are new posts in a followed tag and the user has scrolled the list
@@ -983,10 +1301,18 @@ private void setIsUpdating(boolean isUpdating, UpdateAction updateAction) {
// if swipe-to-refresh isn't active, keep it disabled during an update - this prevents
// doing a refresh while another update is already in progress
if (mRecyclerView != null && !mRecyclerView.isRefreshing()) {
- mRecyclerView.setSwipeToRefreshEnabled(!isUpdating);
+ mRecyclerView.setSwipeToRefreshEnabled(!isUpdating && isSwipeToRefreshSupported());
}
}
+ /*
+ * swipe-to-refresh isn't supported for search results since they're really brief snapshots
+ * and are unlikely to show new posts due to the way they're sorted by score
+ */
+ private boolean isSwipeToRefreshSupported() {
+ return getPostListType() != ReaderPostListType.SEARCH_RESULTS;
+ }
+
/*
* bar that appears at the top when new posts have been retrieved
*/
@@ -1114,6 +1440,9 @@ public void onPostSelected(ReaderPost post) {
post.blogId,
post.postId);
break;
+ case SEARCH_RESULTS:
+ ReaderActivityLauncher.showReaderPostDetail(getActivity(), post.blogId, post.postId);
+ break;
}
}
@@ -1169,9 +1498,7 @@ private void trackTagLoaded(ReaderTag tag) {
*/
@Override
public void onShowPostPopup(View view, final ReaderPost post) {
- if (view == null || post == null || !isAdded()) {
- return;
- }
+ if (view == null || post == null || !isAdded()) return;
Context context = view.getContext();
final ListPopupWindow listPopup = new ListPopupWindow(context);
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java
index 2d3d87f96c85..09b65dd29014 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java
@@ -8,7 +8,8 @@ public class ReaderTypes {
public enum ReaderPostListType {
TAG_FOLLOWED, // list posts in a followed tag
TAG_PREVIEW, // list posts in a specific tag
- BLOG_PREVIEW; // list posts in a specific blog/feed
+ BLOG_PREVIEW, // list posts in a specific blog/feed
+ SEARCH_RESULTS; // list posts matching a specific search keyword or phrase
public boolean isTagType() {
return this.equals(TAG_FOLLOWED) || this.equals(TAG_PREVIEW);
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 f61fe1d0bab3..b52d8899b283 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
@@ -84,8 +84,7 @@ public class ReaderPostAdapter extends RecyclerView.Adapter 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 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/res/drawable-hdpi/reader_settings.png b/WordPress/src/main/res/drawable-hdpi/reader_settings.png
deleted file mode 100644
index 29f6a064c352..000000000000
Binary files a/WordPress/src/main/res/drawable-hdpi/reader_settings.png and /dev/null differ
diff --git a/WordPress/src/main/res/drawable-xhdpi/reader_settings.png b/WordPress/src/main/res/drawable-xhdpi/reader_settings.png
deleted file mode 100644
index aad62112450b..000000000000
Binary files a/WordPress/src/main/res/drawable-xhdpi/reader_settings.png and /dev/null differ
diff --git a/WordPress/src/main/res/drawable-xxhdpi/reader_settings.png b/WordPress/src/main/res/drawable-xxhdpi/reader_settings.png
deleted file mode 100644
index 7c988d056129..000000000000
Binary files a/WordPress/src/main/res/drawable-xxhdpi/reader_settings.png and /dev/null differ
diff --git a/WordPress/src/main/res/drawable/gridicons_cog.xml b/WordPress/src/main/res/drawable/gridicons_cog.xml
new file mode 100644
index 000000000000..443773e7088e
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_cog.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/WordPress/src/main/res/drawable/gridicons_cog_light.xml b/WordPress/src/main/res/drawable/gridicons_cog_light.xml
new file mode 100644
index 000000000000..9220cd65425f
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_cog_light.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/WordPress/src/main/res/drawable/gridicons_search.xml b/WordPress/src/main/res/drawable/gridicons_search.xml
new file mode 100644
index 000000000000..ce42dc946b0b
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_search.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/WordPress/src/main/res/drawable/gridicons_search_light.xml b/WordPress/src/main/res/drawable/gridicons_search_light.xml
new file mode 100644
index 000000000000..bdccb4ecfb72
--- /dev/null
+++ b/WordPress/src/main/res/drawable/gridicons_search_light.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/WordPress/src/main/res/layout-sw720dp/stats_activity.xml b/WordPress/src/main/res/layout-sw720dp/stats_activity.xml
index 68c844301037..54585e28cd93 100644
--- a/WordPress/src/main/res/layout-sw720dp/stats_activity.xml
+++ b/WordPress/src/main/res/layout-sw720dp/stats_activity.xml
@@ -21,7 +21,7 @@
android:id="@+id/toolbar_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- style="@style/FilteredRecyclerViewFilterContainer"
+ style="@style/FilteredRecyclerViewToolbar"
app:layout_scrollFlags="scroll|enterAlways">
-
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
-
-
-
-
-
-
-
-
+ android:overlapAnchor="false" />
diff --git a/WordPress/src/main/res/layout/filtered_recyclerview_settings_control.xml b/WordPress/src/main/res/layout/filtered_recyclerview_settings_control.xml
deleted file mode 100644
index a98e4a414d55..000000000000
--- a/WordPress/src/main/res/layout/filtered_recyclerview_settings_control.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
diff --git a/WordPress/src/main/res/layout/reader_empty_view.xml b/WordPress/src/main/res/layout/reader_empty_view.xml
index 96e44f01becb..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,9 @@
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">
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 ec420c90a046..66a14fe9c601 100644
--- a/WordPress/src/main/res/layout/reader_fragment_post_cards.xml
+++ b/WordPress/src/main/res/layout/reader_fragment_post_cards.xml
@@ -8,8 +8,7 @@
+ android:layout_height="match_parent" />
+ tools:targetApi="LOLLIPOP"
+ tools:visibility="visible">
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/WordPress/src/main/res/layout/stats_activity.xml b/WordPress/src/main/res/layout/stats_activity.xml
index 138773a15c82..c7d2e93b3ff2 100644
--- a/WordPress/src/main/res/layout/stats_activity.xml
+++ b/WordPress/src/main/res/layout/stats_activity.xml
@@ -21,7 +21,7 @@
android:id="@+id/toolbar_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- style="@style/FilteredRecyclerViewFilterContainer"
+ style="@style/FilteredRecyclerViewToolbar"
app:contentInsetLeft="@dimen/margin_filter_spinner"
app:contentInsetStart="@dimen/margin_filter_spinner"
app:layout_scrollFlags="scroll|enterAlways">
diff --git a/WordPress/src/main/res/menu/reader_list.xml b/WordPress/src/main/res/menu/reader_list.xml
new file mode 100644
index 000000000000..0f810502ce33
--- /dev/null
+++ b/WordPress/src/main/res/menu/reader_list.xml
@@ -0,0 +1,18 @@
+
+
\ 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 daf708d9a245..d8a59a3089c5 100644
--- a/WordPress/src/main/res/values/strings.xml
+++ b/WordPress/src/main/res/values/strings.xml
@@ -1078,6 +1078,7 @@
Tags & Blogs
%1$d of %2$d
@string/comments
+ Search for %s
Followed tags
@@ -1103,6 +1104,7 @@
Reply to post…
Reply to comment…
Enter a URL or tag to follow
+ Search WordPress.com
New posts
@@ -1122,6 +1124,8 @@
%,d followers
SEND
Load more posts
+ Search all public WordPress.com blogs
+ Searching…
Like
@@ -1184,6 +1188,11 @@
You haven\'t liked any posts
No comments yet
This blog is empty
+ No posts found
+ No posts found for %s for your language
+
+ Clear search history?
+ Clear search history
Originally posted by %1$s on %2$s
diff --git a/WordPress/src/main/res/values/styles.xml b/WordPress/src/main/res/values/styles.xml
index 742ff2596762..dfd9d41b682d 100644
--- a/WordPress/src/main/res/values/styles.xml
+++ b/WordPress/src/main/res/values/styles.xml
@@ -27,8 +27,12 @@
- true
- @style/WordPress.SwipeToRefresh
+ - @style/WordPress.SearchViewStyle
+
+
+
@@ -86,10 +90,9 @@
- @dimen/text_sz_large
-