diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml
index 3985170bcd6c..a011eae15895 100644
--- a/WordPress/src/main/AndroidManifest.xml
+++ b/WordPress/src/main/AndroidManifest.xml
@@ -256,7 +256,8 @@
+ android:theme="@style/Calypso.NoActionBar">
+
+
properties = new HashMap();
- properties.put("last_visible_screen", lastActivity.toString());
- if (mApplicationOpenedDate != null) {
- Date now = new Date();
- properties.put("time_in_app", DateTimeUtils.secondsBetween(now, mApplicationOpenedDate));
- mApplicationOpenedDate = null;
- }
- AnalyticsTracker.track(AnalyticsTracker.Stat.APPLICATION_CLOSED, properties);
- AnalyticsTracker.endSession(false);
- onAppGoesToBackground();
- } else {
- mIsInBackground = false;
- }
-
boolean evictBitmaps = false;
switch (level) {
case TRIM_MEMORY_COMPLETE:
@@ -796,9 +782,56 @@ private void updatePushNotificationTokenIfNotLimited() {
}
}
- public void onAppGoesToBackground() {
- AppLog.i(T.UTILS, "App goes to background");
- ConnectionChangeReceiver.setEnabled(WordPress.this, false);
+ /**
+ * The two methods below (startActivityTransitionTimer and stopActivityTransitionTimer)
+ * are used to track when the app goes to background.
+ *
+ * Our implementation uses `onActivityPaused` and `onActivityResumed` of ApplicationLifecycleMonitor
+ * to start and stop the timer that detects when the app goes to background.
+ *
+ * So when the user is simply navigating between the activities, the onActivityPaused() calls `startActivityTransitionTimer`
+ * and starts the timer, but almost immediately the new activity being entered, the ApplicationLifecycleMonitor cancels the timer
+ * in its onActivityResumed method, that in order calls `stopActivityTransitionTimer`.
+ * And so mIsInBackground would be false.
+ *
+ * In the case the app is sent to background, the TimerTask is instead executed, and the code that handles all the background logic is run.
+ */
+ private void startActivityTransitionTimer() {
+ this.mActivityTransitionTimer = new Timer();
+ this.mActivityTransitionTimerTask = new TimerTask() {
+ public void run() {
+ AppLog.i(T.UTILS, "App goes to background");
+ // We're in the Background
+ mIsInBackground = true;
+ String lastActivityString = AppPrefs.getLastActivityStr();
+ ActivityId lastActivity = ActivityId.getActivityIdFromName(lastActivityString);
+ Map properties = new HashMap();
+ properties.put("last_visible_screen", lastActivity.toString());
+ if (mApplicationOpenedDate != null) {
+ Date now = new Date();
+ properties.put("time_in_app", DateTimeUtils.secondsBetween(now, mApplicationOpenedDate));
+ mApplicationOpenedDate = null;
+ }
+ AnalyticsTracker.track(AnalyticsTracker.Stat.APPLICATION_CLOSED, properties);
+ AnalyticsTracker.endSession(false);
+ ConnectionChangeReceiver.setEnabled(WordPress.this, false);
+ }
+ };
+
+ this.mActivityTransitionTimer.schedule(mActivityTransitionTimerTask,
+ MAX_ACTIVITY_TRANSITION_TIME_MS);
+ }
+
+ private void stopActivityTransitionTimer() {
+ if (this.mActivityTransitionTimerTask != null) {
+ this.mActivityTransitionTimerTask.cancel();
+ }
+
+ if (this.mActivityTransitionTimer != null) {
+ this.mActivityTransitionTimer.cancel();
+ }
+
+ mIsInBackground = false;
}
/**
@@ -806,7 +839,7 @@ public void onAppGoesToBackground() {
* 1. the app starts (but it's not opened by a service or a broadcast receiver, i.e. an activity is resumed)
* 2. the app was in background and is now foreground
*/
- public void onAppComesFromBackground() {
+ private void onAppComesFromBackground() {
AppLog.i(T.UTILS, "App comes from background");
ConnectionChangeReceiver.setEnabled(WordPress.this, true);
AnalyticsUtils.refreshMetadata();
@@ -832,6 +865,8 @@ public void onActivityResumed(Activity activity) {
// was in background before
onAppComesFromBackground();
}
+ stopActivityTransitionTimer();
+
mIsInBackground = false;
if (mFirstActivityResumed) {
deferredInit(activity);
@@ -850,6 +885,7 @@ public void onActivityDestroyed(Activity arg0) {
@Override
public void onActivityPaused(Activity arg0) {
mLastPingDate = new Date();
+ startActivityTransitionTimer();
}
@Override
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java
index a6579a7d53a0..c7e105f62e23 100644
--- a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java
@@ -19,7 +19,7 @@
*/
public class ReaderDatabase extends SQLiteOpenHelper {
protected static final String DB_NAME = "wpreader.db";
- private static final int DB_VERSION = 115;
+ private static final int DB_VERSION = 118;
/*
* version history
@@ -66,7 +66,10 @@ public class ReaderDatabase extends SQLiteOpenHelper {
* 112 - no structural change, just reset db
* 113 - added tag_title to tag tables
* 114 - renamed tag_name to tag_slug in tag tables
- * 115 - added tag_display_name to tag tables
+ * 115 - added ReaderSearchTable
+ * 116 - added tag_display_name to tag tables
+ * 117 - changed tbl_posts.timestamp from INTEGER to REAL
+ * 118 - renamed tbl_search_history to tbl_search_suggestions
*/
/*
@@ -142,6 +145,7 @@ private void createAllTables(SQLiteDatabase db) {
ReaderUserTable.createTables(db);
ReaderThumbnailTable.createTables(db);
ReaderBlogTable.createTables(db);
+ ReaderSearchTable.createTables(db);
}
private void dropAllTables(SQLiteDatabase db) {
@@ -152,6 +156,7 @@ private void dropAllTables(SQLiteDatabase db) {
ReaderUserTable.dropTables(db);
ReaderThumbnailTable.dropTables(db);
ReaderBlogTable.dropTables(db);
+ ReaderSearchTable.dropTables(db);
}
/*
diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java
index 60732bfd845e..55429f6eb377 100644
--- a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java
@@ -122,7 +122,7 @@ protected static void createTables(SQLiteDatabase db) {
+ " featured_image TEXT,"
+ " featured_video TEXT,"
+ " post_avatar TEXT,"
- + " timestamp INTEGER DEFAULT 0,"
+ + " timestamp REAL DEFAULT 0,"
+ " published TEXT,"
+ " num_replies INTEGER DEFAULT 0,"
+ " num_likes INTEGER DEFAULT 0,"
@@ -180,6 +180,9 @@ protected static int purge(SQLiteDatabase db) {
numDeleted += purgePostsForTag(db, tag);
}
+ // delete search results
+ numDeleted += purgeSearchResults(db);
+
// delete posts in tbl_posts that no longer exist in tbl_post_tags
numDeleted += db.delete("tbl_posts", "pseudo_id NOT IN (SELECT DISTINCT pseudo_id FROM tbl_post_tags)", null);
@@ -211,6 +214,14 @@ private static int purgePostsForTag(SQLiteDatabase db, ReaderTag tag) {
return numDeleted;
}
+ /*
+ * purge all posts that were retained from previous searches
+ */
+ private static int purgeSearchResults(SQLiteDatabase db) {
+ String[] args = {Integer.toString(ReaderTagType.SEARCH.toInt())};
+ return db.delete("tbl_post_tags", "tag_type=?", args);
+ }
+
public static int getNumPostsInBlog(long blogId) {
if (blogId == 0) {
return 0;
@@ -624,7 +635,7 @@ public static void addOrUpdatePosts(final ReaderTag tag, ReaderPostList posts) {
stmtPosts.bindString(16, post.getFeaturedImage());
stmtPosts.bindString(17, post.getFeaturedVideo());
stmtPosts.bindString(18, post.getPostAvatar());
- stmtPosts.bindLong (19, post.timestamp);
+ stmtPosts.bindDouble(19, post.timestamp);
stmtPosts.bindString(20, post.getPublished());
stmtPosts.bindLong (21, post.numReplies);
stmtPosts.bindLong (22, post.numLikes);
@@ -839,7 +850,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.timestamp = c.getDouble(c.getColumnIndex("timestamp"));
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..598e596c29cc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderSearchTable.java
@@ -0,0 +1,79 @@
+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_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);
+ }
+
+ /**
+ * 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 550aba98ea8c..72907cdb3597 100644
--- a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java
@@ -39,7 +39,7 @@ public class ReaderPost {
private String primaryTag; // most popular tag on this post based on usage in blog
private String secondaryTag; // second most popular tag on this post based on usage in blog
- public long timestamp; // used for sorting
+ public double timestamp; // used for sorting
private String published;
private String url;
@@ -116,14 +116,19 @@ public static ReaderPost fromJson(JSONObject json) {
post.blogName = JSONUtils.getStringDecoded(json, "site_name");
post.published = JSONUtils.getString(json, "date");
- // the date a post was liked is only returned by the read/liked/ endpoint - if this exists,
- // set it as the timestamp so posts are sorted by the date they were liked rather than the
- // date they were published (the timestamp is used to sort posts when querying)
- String likeDate = JSONUtils.getString(json, "date_liked");
- if (!TextUtils.isEmpty(likeDate)) {
- post.timestamp = DateTimeUtils.iso8601ToTimestamp(likeDate);
+ // a post's timestamp determines its sort order
+ if (json.has("score")) {
+ // search results include a "score" that should be used for sorting
+ post.timestamp = json.optDouble("score");
} else {
- post.timestamp = DateTimeUtils.iso8601ToTimestamp(post.published);
+ // liked posts should be sorted by the date they were liked, otherwise sort by the
+ // published date
+ String likeDate = JSONUtils.getString(json, "date_liked");
+ if (!TextUtils.isEmpty(likeDate)) {
+ post.timestamp = DateTimeUtils.iso8601ToTimestamp(likeDate);
+ } else {
+ post.timestamp = DateTimeUtils.iso8601ToTimestamp(post.published);
+ }
}
// if the post is untitled, make up a title from the excerpt
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java
index e6bcc629e07b..0fa75a8b2a8b 100644
--- a/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTag.java
@@ -150,6 +150,7 @@ public static boolean isSameTag(ReaderTag tag1, ReaderTag tag2) {
public boolean isPostsILike() {
return tagType == ReaderTagType.DEFAULT && getEndpoint().endsWith("/read/liked");
}
+
public boolean isFollowedSites() {
return tagType == ReaderTagType.DEFAULT && getEndpoint().endsWith("/read/following");
}
diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java
index f20ef28da3cd..5075e81d35f9 100644
--- a/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java
+++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderTagType.java
@@ -4,12 +4,14 @@ public enum ReaderTagType {
FOLLOWED,
DEFAULT,
RECOMMENDED,
- CUSTOM_LIST;
+ CUSTOM_LIST,
+ SEARCH;
private static final int INT_DEFAULT = 0;
private static final int INT_FOLLOWED = 1;
private static final int INT_RECOMMENDED = 2;
private static final int INT_CUSTOM_LIST = 3;
+ private static final int INT_SEARCH = 4;
public static ReaderTagType fromInt(int value) {
switch (value) {
@@ -19,6 +21,8 @@ public static ReaderTagType fromInt(int value) {
return FOLLOWED;
case INT_CUSTOM_LIST:
return CUSTOM_LIST;
+ case INT_SEARCH:
+ return SEARCH;
default :
return DEFAULT;
}
@@ -32,6 +36,8 @@ public int toInt() {
return INT_RECOMMENDED;
case CUSTOM_LIST:
return INT_CUSTOM_LIST;
+ case SEARCH:
+ return INT_SEARCH;
default :
return INT_DEFAULT;
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java b/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java
index 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 9c92c40af34a..ba738ff63a57 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
@@ -80,6 +80,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");
@@ -314,6 +322,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 29e8518b6530..754d22644a1e 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java
@@ -133,7 +133,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 b31adaaf528b..fc27d404f370 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java
@@ -1,5 +1,7 @@
package org.wordpress.android.ui.reader;
+import android.support.annotation.NonNull;
+
import org.wordpress.android.models.ReaderTag;
import org.wordpress.android.ui.reader.actions.ReaderActions;
import org.wordpress.android.ui.reader.services.ReaderPostService;
@@ -63,6 +65,40 @@ public ReaderPostService.UpdateAction getAction() {
}
}
+ public static class SearchPostsStarted {
+ private final String mQuery;
+ private final int mOffset;
+ public SearchPostsStarted(@NonNull String query, int offset) {
+ mQuery = query;
+ mOffset = offset;
+ }
+ public String getQuery() {
+ return mQuery;
+ }
+ public int getOffset() {
+ return mOffset;
+ }
+ }
+ public static class SearchPostsEnded {
+ private final String mQuery;
+ private final boolean mHasResults;
+ private final int mOffset;
+ public SearchPostsEnded(@NonNull String query, int offset, boolean hasResults) {
+ mQuery = query;
+ mOffset = offset;
+ mHasResults = hasResults;
+ }
+ public boolean hasResults() {
+ return mHasResults;
+ }
+ 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 b1ea96be4c1f..f32d190bd831 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java
@@ -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 a8d2f661a868..de599e9c6735 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,15 +4,22 @@
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.view.MenuItemCompat;
import android.support.v7.widget.ListPopupWindow;
import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.SearchView;
+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;
@@ -22,6 +29,7 @@
import org.wordpress.android.datasets.ReaderBlogTable;
import org.wordpress.android.datasets.ReaderDatabase;
import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.ReaderSearchTable;
import org.wordpress.android.datasets.ReaderTagTable;
import org.wordpress.android.models.FilterCriteria;
import org.wordpress.android.models.ReaderPost;
@@ -39,8 +47,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;
@@ -69,20 +79,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;
@@ -188,6 +207,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());
@@ -210,6 +230,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);
}
@@ -232,16 +255,22 @@ public void onPause() {
@Override
public void onResume() {
super.onResume();
- checkAdapter();
+ checkPostAdapter();
if (mWasPaused) {
AppLog.d(T.READER, "reader post list > resumed from paused state");
mWasPaused = false;
- if (getPostListType().equals(ReaderPostListType.TAG_FOLLOWED)) {
+ if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) {
resumeFollowedTag();
} else {
refreshPosts();
}
+
+ // if the user was searching, make sure the filter toolbar is showing
+ // so the user can see the search keyword they entered
+ if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) {
+ mRecyclerView.showToolbar();
+ }
}
}
@@ -292,7 +321,7 @@ public void onStop() {
/*
* ensures the adapter is created and posts are updated if they haven't already been
*/
- private void checkAdapter() {
+ private void checkPostAdapter() {
if (isAdded() && mRecyclerView.getAdapter() == null) {
mRecyclerView.setAdapter(getPostAdapter());
@@ -307,6 +336,16 @@ private void checkAdapter() {
}
}
+ /*
+ * reset the post adapter to initial state and create it again using the passed list type
+ */
+ private void resetPostAdapter(ReaderPostListType postListType) {
+ mPostListType = postListType;
+ mPostAdapter = null;
+ mRecyclerView.setAdapter(null);
+ mRecyclerView.setAdapter(getPostAdapter());
+ }
+
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.FollowedTagsChanged event) {
if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) {
@@ -344,6 +383,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);
@@ -371,7 +411,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);
@@ -457,16 +497,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(getResources().getColor(R.color.blue_medium));
mRecyclerView.setToolbarSpinnerTextColor(getResources().getColor(R.color.white));
@@ -475,6 +505,12 @@ 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();
+ }
+
// bar that appears at top after new posts are loaded
mNewPostsBar = rootView.findViewById(R.id.layout_new_posts);
mNewPostsBar.setVisibility(View.GONE);
@@ -486,7 +522,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);
@@ -494,6 +529,193 @@ 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_settings);
+ mSearchMenuItem = menu.findItem(R.id.menu_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);
+
+ // 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;
+
+ // remember this query for future suggestions
+ ReaderSearchTable.addOrUpdateQueryString(query);
+
+ mSearchView.clearFocus(); // this will hide suggestions and the virtual keyboard
+ hideSearchMessage();
+
+ ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(query);
+ mPostAdapter.setCurrentTag(searchTag);
+
+ mCurrentSearchQuery = query;
+ updatePostsInCurrentSearch(0);
+ }
+
+ /*
+ * 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.populate(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;
+
+ // make sure this is for the current search
+ if (getPostListType() != ReaderPostListType.SEARCH_RESULTS
+ || !event.getQuery().equals(mCurrentSearchQuery)) {
+ return;
+ }
+
+ UpdateAction updateAction = event.getOffset() == 0 ? UpdateAction.REQUEST_NEWER : UpdateAction.REQUEST_OLDER;
+ setIsUpdating(false, updateAction);
+
+ // load the results, or show empty message if there aren't any
+ refreshPosts();
+ }
+
/*
* called when user taps follow item in popup menu for a post
*/
@@ -569,15 +791,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);
@@ -589,51 +810,88 @@ 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);
+ description = String.format(getString(R.string.reader_empty_posts_in_search_description), mCurrentSearchQuery);
+ }
+ break;
+
+ default:
+ title = getString(R.string.reader_empty_posts_in_tag);
+ break;
+ }
}
+ setEmptyTitleAndDescription(title, description);
+ }
+
+ 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));
+ 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);
+ }
}
/*
@@ -648,12 +906,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);
@@ -697,6 +955,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;
}
}
};
@@ -718,6 +985,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;
@@ -781,12 +1051,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;
}
@@ -841,7 +1127,6 @@ private void reloadTags() {
}
}
-
/*
* get posts for the current blog from the server
*/
@@ -874,6 +1159,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
@@ -1120,6 +1412,10 @@ public void onPostSelected(ReaderPost post) {
post.blogId,
post.postId);
break;
+ case SEARCH_RESULTS:
+ // TODO: track analytics
+ ReaderActivityLauncher.showReaderPostDetail(getActivity(), post.blogId, post.postId);
+ break;
}
}
@@ -1175,9 +1471,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 74718bddd643..9b94773dada6 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
@@ -5,10 +5,11 @@ public class ReaderTypes {
public static final ReaderPostListType DEFAULT_POST_LIST_TYPE = ReaderPostListType.TAG_FOLLOWED;
- public static enum ReaderPostListType {
+ 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 5d18ea78917e..72e9a79741a7 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java
@@ -52,7 +52,6 @@ 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);
+ boolean hasResults = ReaderPostTable.comparePosts(serverPosts).isNewOrChanged();
+ if (hasResults) {
+ ReaderPostTable.addOrUpdatePosts(getTagForSearchQuery(query), serverPosts);
+ }
+ EventBus.getDefault().post(new ReaderEvents.SearchPostsEnded(query, offset, hasResults));
+ }
+ }.start();
+ }
+
+ /*
+ * used when storing search results in the reader post table
+ */
+ public static ReaderTag getTagForSearchQuery(@NonNull String query) {
+ String slug = ReaderUtils.sanitizeWithDashes(query);
+ return new ReaderTag(slug, query, query, null, ReaderTagType.SEARCH);
+ }
+}
diff --git a/WordPress/src/main/res/layout-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 606ba8dcc8a9..000000000000
--- a/WordPress/src/main/res/layout/filtered_recyclerview_settings_control.xml
+++ /dev/null
@@ -1,8 +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 c20165c3eb47..b09c5154324e 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..c7de94075540
--- /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 89aa349a6779..3989939d5914 100644
--- a/WordPress/src/main/res/values/strings.xml
+++ b/WordPress/src/main/res/values/strings.xml
@@ -1070,6 +1070,7 @@
Tags & Blogs
%1$d of %2$d
@string/comments
+ Search for %s
Followed tags
@@ -1095,6 +1096,7 @@
Reply to post…
Reply to comment…
Enter a URL or tag to follow
+ Search on WordPress.com
New posts
@@ -1114,6 +1116,8 @@
%,d followers
SEND
Load more posts
+ Search all public WordPress.com blogs
+ Searching…
Like
@@ -1176,6 +1180,8 @@
You haven\'t liked any posts
No comments yet
This blog is empty
+ No Results
+ No posts found for \"%s\" for your language
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 56ee2b3ce2d7..afe93ffa570e 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
-