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 -