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 -