blob: 69409385eaf2ff9000e898c2179478ff64f97cb1 [file] [log] [blame]
package org.wordpress.android.ui.reader;
import android.app.Fragment;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.ListPopupWindow;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.text.Html;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.wordpress.android.R;
import org.wordpress.android.analytics.AnalyticsTracker;
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;
import org.wordpress.android.models.ReaderPostDiscoverData;
import org.wordpress.android.models.ReaderTag;
import org.wordpress.android.models.ReaderTagList;
import org.wordpress.android.models.ReaderTagType;
import org.wordpress.android.ui.EmptyViewMessageType;
import org.wordpress.android.ui.FilteredRecyclerView;
import org.wordpress.android.ui.main.WPMainActivity;
import org.wordpress.android.ui.prefs.AppPrefs;
import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType;
import org.wordpress.android.ui.reader.actions.ReaderActions;
import org.wordpress.android.ui.reader.actions.ReaderBlogActions;
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;
import org.wordpress.android.ui.reader.views.ReaderSiteHeaderView;
import org.wordpress.android.util.AnalyticsUtils;
import org.wordpress.android.util.AniUtils;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.AppLog.T;
import org.wordpress.android.util.DateTimeUtils;
import org.wordpress.android.util.DisplayUtils;
import org.wordpress.android.util.NetworkUtils;
import org.wordpress.android.util.ToastUtils;
import org.wordpress.android.util.WPActivityUtils;
import org.wordpress.android.widgets.RecyclerItemDecoration;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import de.greenrobot.event.EventBus;
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;
private boolean mIsUpdating;
private boolean mWasPaused;
private boolean mHasUpdatedPosts;
private boolean mIsAnimatingOutNewPostsBar;
private static boolean mHasPurgedReaderDb;
private static Date mLastAutoUpdateDt;
private final HistoryStack mTagPreviewHistory = new HistoryStack("tag_preview_history");
private static class HistoryStack extends Stack<String> {
private final String keyName;
HistoryStack(@SuppressWarnings("SameParameterValue") String keyName) {
this.keyName = keyName;
}
void restoreInstance(Bundle bundle) {
clear();
if (bundle.containsKey(keyName)) {
ArrayList<String> history = bundle.getStringArrayList(keyName);
if (history != null) {
this.addAll(history);
}
}
}
void saveInstance(Bundle bundle) {
if (!isEmpty()) {
ArrayList<String> history = new ArrayList<>();
history.addAll(this);
bundle.putStringArrayList(keyName, history);
}
}
}
public static ReaderPostListFragment newInstance() {
ReaderTag tag = AppPrefs.getReaderTag();
if (tag == null) {
tag = ReaderUtils.getDefaultTag();
}
return newInstanceForTag(tag, ReaderPostListType.TAG_FOLLOWED);
}
/*
* show posts with a specific tag (either TAG_FOLLOWED or TAG_PREVIEW)
*/
static ReaderPostListFragment newInstanceForTag(ReaderTag tag, ReaderPostListType listType) {
AppLog.d(T.READER, "reader post list > newInstance (tag)");
Bundle args = new Bundle();
args.putSerializable(ReaderConstants.ARG_TAG, tag);
args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, listType);
ReaderPostListFragment fragment = new ReaderPostListFragment();
fragment.setArguments(args);
return fragment;
}
/*
* show posts in a specific blog
*/
public static ReaderPostListFragment newInstanceForBlog(long blogId) {
AppLog.d(T.READER, "reader post list > newInstance (blog)");
Bundle args = new Bundle();
args.putLong(ReaderConstants.ARG_BLOG_ID, blogId);
args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.BLOG_PREVIEW);
ReaderPostListFragment fragment = new ReaderPostListFragment();
fragment.setArguments(args);
return fragment;
}
public static ReaderPostListFragment newInstanceForFeed(long feedId) {
AppLog.d(T.READER, "reader post list > newInstance (blog)");
Bundle args = new Bundle();
args.putLong(ReaderConstants.ARG_FEED_ID, feedId);
args.putLong(ReaderConstants.ARG_BLOG_ID, feedId);
args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.BLOG_PREVIEW);
ReaderPostListFragment fragment = new ReaderPostListFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void setArguments(Bundle args) {
super.setArguments(args);
if (args != null) {
if (args.containsKey(ReaderConstants.ARG_TAG)) {
mCurrentTag = (ReaderTag) args.getSerializable(ReaderConstants.ARG_TAG);
}
if (args.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) {
mPostListType = (ReaderPostListType) args.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE);
}
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());
}
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
AppLog.d(T.READER, "reader post list > restoring instance state");
if (savedInstanceState.containsKey(ReaderConstants.ARG_TAG)) {
mCurrentTag = (ReaderTag) savedInstanceState.getSerializable(ReaderConstants.ARG_TAG);
}
if (savedInstanceState.containsKey(ReaderConstants.ARG_BLOG_ID)) {
mCurrentBlogId = savedInstanceState.getLong(ReaderConstants.ARG_BLOG_ID);
}
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);
}
if (getPostListType() == ReaderPostListType.TAG_PREVIEW) {
mTagPreviewHistory.restoreInstance(savedInstanceState);
}
mRestorePosition = savedInstanceState.getInt(ReaderConstants.KEY_RESTORE_POSITION);
mWasPaused = savedInstanceState.getBoolean(ReaderConstants.KEY_WAS_PAUSED);
mHasUpdatedPosts = savedInstanceState.getBoolean(ReaderConstants.KEY_ALREADY_UPDATED);
mFirstLoad = savedInstanceState.getBoolean(ReaderConstants.KEY_FIRST_LOAD);
}
}
@Override
public void onPause() {
super.onPause();
mWasPaused = true;
}
@Override
public void onResume() {
super.onResume();
checkPostAdapter();
if (mWasPaused) {
AppLog.d(T.READER, "reader post list > resumed from paused state");
mWasPaused = false;
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();
}
}
}
/*
* called when fragment is resumed and we're looking at posts in a followed tag
*/
private void resumeFollowedTag() {
Object event = EventBus.getDefault().getStickyEvent(ReaderEvents.TagAdded.class);
if (event != null) {
// user just added a tag so switch to it.
String tagName = ((ReaderEvents.TagAdded) event).getTagName();
EventBus.getDefault().removeStickyEvent(event);
ReaderTag newTag = ReaderUtils.getTagFromTagName(tagName, ReaderTagType.FOLLOWED);
setCurrentTag(newTag);
} else if (!ReaderTagTable.tagExists(getCurrentTag())) {
// current tag no longer exists, revert to default
AppLog.d(T.READER, "reader post list > current tag no longer valid");
setCurrentTag(ReaderUtils.getDefaultTag());
} else {
// otherwise, refresh posts to make sure any changes are reflected and auto-update
// posts in the current tag if it's time
refreshPosts();
updateCurrentTagIfTime();
}
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
reloadTags();
// purge database and update followed tags/blog if necessary - note that we don't purge unless
// there's a connection to avoid removing posts the user would expect to see offline
if (getPostListType() == ReaderPostListType.TAG_FOLLOWED && NetworkUtils.isNetworkAvailable(getActivity())) {
purgeDatabaseIfNeeded();
updateFollowedTagsAndBlogsIfNeeded();
}
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
/*
* ensures the adapter is created and posts are updated if they haven't already been
*/
private void checkPostAdapter() {
if (isAdded() && mRecyclerView.getAdapter() == null) {
mRecyclerView.setAdapter(getPostAdapter());
if (!mHasUpdatedPosts && NetworkUtils.isNetworkAvailable(getActivity())) {
mHasUpdatedPosts = true;
if (getPostListType().isTagType()) {
updateCurrentTagIfTime();
} else if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) {
updatePostsInCurrentBlogOrFeed(UpdateAction.REQUEST_NEWER);
}
}
}
}
/*
* reset the post adapter to initial state and create it again using the passed list type
*/
private void resetPostAdapter(ReaderPostListType postListType) {
mPostListType = postListType;
mPostAdapter = null;
mRecyclerView.setAdapter(null);
mRecyclerView.setAdapter(getPostAdapter());
mRecyclerView.setSwipeToRefreshEnabled(isSwipeToRefreshSupported());
}
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.FollowedTagsChanged event) {
if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) {
// reload the tag filter since tags have changed
reloadTags();
// update the current tag if the list fragment is empty - this will happen if
// the tag table was previously empty (ie: first run)
if (isPostAdapterEmpty()) {
updateCurrentTag();
}
}
}
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.FollowedBlogsChanged event) {
// refresh posts if user is viewing "Followed Sites"
if (getPostListType() == ReaderPostListType.TAG_FOLLOWED
&& hasCurrentTag()
&& getCurrentTag().isFollowedSites()) {
refreshPosts();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
AppLog.d(T.READER, "reader post list > saving instance state");
if (mCurrentTag != null) {
outState.putSerializable(ReaderConstants.ARG_TAG, mCurrentTag);
}
if (getPostListType() == ReaderPostListType.TAG_PREVIEW) {
mTagPreviewHistory.saveInstance(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);
outState.putInt(ReaderConstants.KEY_RESTORE_POSITION, getCurrentPosition());
outState.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, getPostListType());
super.onSaveInstanceState(outState);
}
private int getCurrentPosition() {
if (mRecyclerView != null && hasPostAdapter()) {
return mRecyclerView.getCurrentPosition();
} else {
return -1;
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.reader_fragment_post_cards, container, false);
mRecyclerView = (FilteredRecyclerView) rootView.findViewById(R.id.reader_recycler_view);
Context context = container.getContext();
// 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);
mEmptyViewBoxImages = mEmptyView.findViewById(R.id.layout_box_images);
mRecyclerView.setLogT(AppLog.T.READER);
mRecyclerView.setCustomEmptyView(mEmptyView);
mRecyclerView.setFilterListener(new FilteredRecyclerView.FilterListener() {
@Override
public List<FilterCriteria> onLoadFilterCriteriaOptions(boolean refresh) {
return null;
}
@Override
public void onLoadFilterCriteriaOptionsAsync(
FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener, boolean refresh) {
loadTags(listener);
}
@Override
public void onLoadData() {
if (!isAdded()) {
return;
}
if (!NetworkUtils.checkConnection(getActivity())) {
mRecyclerView.setRefreshing(false);
return;
}
if (mFirstLoad){
/* let onResume() take care of this logic, as the FilteredRecyclerView.FilterListener onLoadData method
* is called on two moments: once for first time load, and then each time the swipe to refresh gesture
* triggers a refresh
*/
mRecyclerView.setRefreshing(false);
mFirstLoad = false;
} else {
switch (getPostListType()) {
case TAG_FOLLOWED:
case TAG_PREVIEW:
updatePostsWithTag(getCurrentTag(), UpdateAction.REQUEST_NEWER);
break;
case BLOG_PREVIEW:
updatePostsInCurrentBlogOrFeed(UpdateAction.REQUEST_NEWER);
break;
}
// make sure swipe-to-refresh progress shows since this is a manual refresh
mRecyclerView.setRefreshing(true);
}
}
@Override
public void onFilterSelected(int position, FilterCriteria criteria) {
onTagChanged((ReaderTag)criteria);
}
@Override
public FilterCriteria onRecallSelection() {
if (hasCurrentTag()) {
return getCurrentTag();
} else {
AppLog.w(T.READER, "reader post list > no current tag in onRecallSelection");
return ReaderUtils.getDefaultTag();
}
}
@Override
public String onShowEmptyViewMessage(EmptyViewMessageType emptyViewMsgType) {
return null;
}
@Override
public void onShowCustomEmptyView (EmptyViewMessageType emptyViewMsgType) {
setEmptyTitleAndDescription(
EmptyViewMessageType.NETWORK_ERROR.equals(emptyViewMsgType)
|| EmptyViewMessageType.PERMISSION_ERROR.equals(emptyViewMsgType)
|| EmptyViewMessageType.GENERIC_ERROR.equals(emptyViewMsgType));
}
});
// add the item decoration (dividers) to the recycler, skipping the first item if the first
// item is the tag toolbar (shown when viewing posts in followed tags) - this is to avoid
// having the tag toolbar take up more vertical space than necessary
int spacingHorizontal = context.getResources().getDimensionPixelSize(R.dimen.reader_card_margin);
int spacingVertical = context.getResources().getDimensionPixelSize(R.dimen.reader_card_gutters);
mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical, false));
// the following will change the look and feel of the toolbar to match the current design
mRecyclerView.setToolbarBackgroundColor(ContextCompat.getColor(context, R.color.blue_medium));
mRecyclerView.setToolbarSpinnerTextColor(ContextCompat.getColor(context, R.color.white));
mRecyclerView.setToolbarSpinnerDrawable(R.drawable.arrow);
mRecyclerView.setToolbarLeftAndRightPadding(
getResources().getDimensionPixelSize(R.dimen.margin_medium) + spacingHorizontal,
getResources().getDimensionPixelSize(R.dimen.margin_extra_large) + spacingHorizontal);
// add a menu to the filtered recycler's toolbar
if (!ReaderUtils.isLoggedOutReader()
&& (getPostListType() == ReaderPostListType.TAG_FOLLOWED || getPostListType() == ReaderPostListType.SEARCH_RESULTS)) {
setupRecyclerToolbar();
}
mRecyclerView.setSwipeToRefreshEnabled(isSwipeToRefreshSupported());
// bar that appears at top after new posts are loaded
mNewPostsBar = rootView.findViewById(R.id.layout_new_posts);
mNewPostsBar.setVisibility(View.GONE);
mNewPostsBar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mRecyclerView.scrollRecycleViewToPosition(0);
refreshPosts();
}
});
// progress bar that appears when loading more posts
mProgress = (ProgressBar) rootView.findViewById(R.id.progress_footer);
mProgress.setVisibility(View.GONE);
return rootView;
}
/*
* adds a menu to the recycler's toolbar containing settings & search items - only called
* for followed tags
*/
private void setupRecyclerToolbar() {
Menu menu = mRecyclerView.addToolbarMenu(R.menu.reader_list);
mSettingsMenuItem = menu.findItem(R.id.menu_reader_settings);
mSearchMenuItem = menu.findItem(R.id.menu_reader_search);
mSettingsMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
ReaderActivityLauncher.showReaderSubs(getActivity());
return true;
}
});
mSearchView = (SearchView) mSearchMenuItem.getActionView();
mSearchView.setQueryHint(getString(R.string.reader_hint_post_search));
mSearchView.setSubmitButtonEnabled(false);
mSearchView.setIconifiedByDefault(true);
mSearchView.setIconified(true);
// force the search view to take up as much horizontal space as possible (without this
// it looks truncated on landscape)
int maxWidth = DisplayUtils.getDisplayPixelWidth(getActivity());
mSearchView.setMaxWidth(maxWidth);
// this is hacky, but we want to change the SearchView's autocomplete to show suggestions
// after a single character is typed, and there's no less hacky way to do this...
View view = mSearchView.findViewById(android.support.v7.appcompat.R.id.search_src_text);
if (view instanceof AutoCompleteTextView) {
((AutoCompleteTextView) view).setThreshold(1);
}
MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, new MenuItemCompat.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
if (getPostListType() != ReaderPostListType.SEARCH_RESULTS) {
AnalyticsTracker.track(AnalyticsTracker.Stat.READER_SEARCH_LOADED);
}
resetPostAdapter(ReaderPostListType.SEARCH_RESULTS);
showSearchMessage();
mSettingsMenuItem.setVisible(false);
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
hideSearchMessage();
resetSearchSuggestionAdapter();
mSettingsMenuItem.setVisible(true);
mCurrentSearchQuery = null;
// return to the followed tag that was showing prior to searching
resetPostAdapter(ReaderPostListType.TAG_FOLLOWED);
return true;
}
});
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
submitSearchQuery(query);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
if (TextUtils.isEmpty(newText)) {
showSearchMessage();
} else {
populateSearchSuggestionAdapter(newText);
}
return true;
}
}
);
}
/*
* start the search service to search for posts matching the current query - the passed
* offset is used during infinite scroll, pass zero for initial search
*/
private void updatePostsInCurrentSearch(int offset) {
ReaderSearchService.startService(getActivity(), mCurrentSearchQuery, offset);
}
private void submitSearchQuery(@NonNull String query) {
if (!isAdded()) return;
mSearchView.clearFocus(); // this will hide suggestions and the virtual keyboard
hideSearchMessage();
// remember this query for future suggestions
String trimQuery = query != null ? query.trim() : "";
ReaderSearchTable.addOrUpdateQueryString(trimQuery);
// remove cached results for this search - search results are ephemeral so each search
// should be treated as a "fresh" one
ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(trimQuery);
ReaderPostTable.deletePostsWithTag(searchTag);
mPostAdapter.setCurrentTag(searchTag);
mCurrentSearchQuery = trimQuery;
updatePostsInCurrentSearch(0);
// track that the user performed a search
if (!trimQuery.equals("")) {
Map<String, Object> properties = new HashMap<>();
properties.put("query", trimQuery);
AnalyticsTracker.track(AnalyticsTracker.Stat.READER_SEARCH_PERFORMED, properties);
}
}
/*
* reuse "empty" view to let user know what they're querying
*/
private void showSearchMessage() {
if (!isAdded()) return;
// clear posts so only the empty view is visible
getPostAdapter().clear();
setEmptyTitleAndDescription(false);
showEmptyView();
}
private void hideSearchMessage() {
hideEmptyView();
}
/*
* create and assign the suggestion adapter for the search view
*/
private void createSearchSuggestionAdapter() {
mSearchSuggestionAdapter = new ReaderSearchSuggestionAdapter(getActivity());
mSearchView.setSuggestionsAdapter(mSearchSuggestionAdapter);
mSearchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
@Override
public boolean onSuggestionSelect(int position) {
return false;
}
@Override
public boolean onSuggestionClick(int position) {
String query = mSearchSuggestionAdapter.getSuggestion(position);
if (!TextUtils.isEmpty(query)) {
mSearchView.setQuery(query, true);
}
return true;
}
});
}
private void populateSearchSuggestionAdapter(String query) {
if (mSearchSuggestionAdapter == null) {
createSearchSuggestionAdapter();
}
mSearchSuggestionAdapter.setFilter(query);
}
private void resetSearchSuggestionAdapter() {
mSearchView.setSuggestionsAdapter(null);
mSearchSuggestionAdapter = null;
}
/*
* is the search input showing?
*/
private boolean isSearchViewExpanded() {
return mSearchView != null && !mSearchView.isIconified();
}
private boolean isSearchViewEmpty() {
return mSearchView != null && mSearchView.getQuery().length() == 0;
}
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.SearchPostsStarted event) {
if (!isAdded()) return;
UpdateAction updateAction = event.getOffset() == 0 ? UpdateAction.REQUEST_NEWER : UpdateAction.REQUEST_OLDER;
setIsUpdating(true, updateAction);
setEmptyTitleAndDescription(false);
}
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.SearchPostsEnded event) {
if (!isAdded()) return;
UpdateAction updateAction = event.getOffset() == 0 ? UpdateAction.REQUEST_NEWER : UpdateAction.REQUEST_OLDER;
setIsUpdating(false, updateAction);
// load the results if the search succeeded and it's the current search - note that success
// means the search didn't fail, not necessarily that is has results - which is fine because
// if there aren't results then refreshing will show the empty message
if (event.didSucceed()
&& getPostListType() == ReaderPostListType.SEARCH_RESULTS
&& event.getQuery().equals(mCurrentSearchQuery)) {
refreshPosts();
}
}
/*
* called when user taps follow item in popup menu for a post
*/
private void toggleFollowStatusForPost(final ReaderPost post) {
if (post == null
|| !hasPostAdapter()
|| !NetworkUtils.checkConnection(getActivity())) {
return;
}
final boolean isAskingToFollow = !ReaderPostTable.isPostFollowed(post);
ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() {
@Override
public void onActionResult(boolean succeeded) {
if (isAdded() && !succeeded) {
int resId = (isAskingToFollow ? R.string.reader_toast_err_follow_blog : R.string.reader_toast_err_unfollow_blog);
ToastUtils.showToast(getActivity(), resId);
getPostAdapter().setFollowStatusForBlog(post.blogId, !isAskingToFollow);
}
}
};
if (ReaderBlogActions.followBlogForPost(post, isAskingToFollow, actionListener)) {
getPostAdapter().setFollowStatusForBlog(post.blogId, isAskingToFollow);
}
}
/*
* blocks the blog associated with the passed post and removes all posts in that blog
* from the adapter
*/
private void blockBlogForPost(final ReaderPost post) {
if (post == null
|| !isAdded()
|| !hasPostAdapter()
|| !NetworkUtils.checkConnection(getActivity())) {
return;
}
ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() {
@Override
public void onActionResult(boolean succeeded) {
if (!succeeded && isAdded()) {
ToastUtils.showToast(getActivity(), R.string.reader_toast_err_block_blog, ToastUtils.Duration.LONG);
}
}
};
// perform call to block this blog - returns list of posts deleted by blocking so
// they can be restored if the user undoes the block
final BlockedBlogResult blockResult = ReaderBlogActions.blockBlogFromReader(post.blogId, actionListener);
// Only pass the blogID if available. Do not track feedID
AnalyticsUtils.trackWithBlogDetails(
AnalyticsTracker.Stat.READER_BLOG_BLOCKED,
mCurrentBlogId != 0 ? mCurrentBlogId : null
);
// remove posts in this blog from the adapter
getPostAdapter().removePostsInBlog(post.blogId);
// show the undo snackbar enabling the user to undo the block
View.OnClickListener undoListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
ReaderBlogActions.undoBlockBlogFromReader(blockResult);
refreshPosts();
}
};
Snackbar.make(getView(), getString(R.string.reader_toast_blog_blocked), Snackbar.LENGTH_LONG)
.setAction(R.string.undo, undoListener)
.show();
}
/*
* box/pages animation that appears when loading an empty list
*/
private boolean shouldShowBoxAndPagesAnimation() {
return getPostListType().isTagType();
}
private void startBoxAndPagesAnimation() {
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);
ImageView page3 = (ImageView) mEmptyView.findViewById(R.id.empty_tags_box_page3);
page1.startAnimation(AnimationUtils.loadAnimation(getActivity(), R.anim.box_with_pages_slide_up_page1));
page2.startAnimation(AnimationUtils.loadAnimation(getActivity(), R.anim.box_with_pages_slide_up_page2));
page3.startAnimation(AnimationUtils.loadAnimation(getActivity(), R.anim.box_with_pages_slide_up_page3));
}
private void setEmptyTitleAndDescription(boolean requestFailed) {
if (!isAdded()) return;
String title;
String description = null;
if (!NetworkUtils.isNetworkAvailable(getActivity())) {
title = getString(R.string.reader_empty_posts_no_connection);
} else if (requestFailed) {
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 {
switch (getPostListType()) {
case TAG_FOLLOWED:
if (getCurrentTag().isFollowedSites()) {
if (ReaderBlogTable.hasFollowedBlogs()) {
title = getString(R.string.reader_empty_followed_blogs_no_recent_posts_title);
description = getString(R.string.reader_empty_followed_blogs_no_recent_posts_description);
} else {
title = getString(R.string.reader_empty_followed_blogs_title);
description = getString(R.string.reader_empty_followed_blogs_description);
}
} else if (getCurrentTag().isPostsILike()) {
title = getString(R.string.reader_empty_posts_liked);
} else if (getCurrentTag().isListTopic()) {
title = getString(R.string.reader_empty_posts_in_custom_list);
} else {
title = getString(R.string.reader_empty_posts_in_tag);
}
break;
case BLOG_PREVIEW:
title = getString(R.string.reader_empty_posts_in_blog);
break;
case SEARCH_RESULTS:
if (isSearchViewEmpty() || TextUtils.isEmpty(mCurrentSearchQuery)) {
title = getString(R.string.reader_label_post_search_explainer);
} else if (isUpdating()) {
title = getString(R.string.reader_label_post_search_running);
} else {
title = getString(R.string.reader_empty_posts_in_search_title);
String formattedQuery = "<em>" + mCurrentSearchQuery + "</em>";
description = String.format(getString(R.string.reader_empty_posts_in_search_description), formattedQuery);
}
break;
default:
title = getString(R.string.reader_empty_posts_in_tag);
break;
}
}
setEmptyTitleAndDescription(title, description);
}
private void setEmptyTitleAndDescription(@NonNull String title, String description) {
if (!isAdded()) return;
TextView titleView = (TextView) mEmptyView.findViewById(R.id.title_empty);
titleView.setText(title);
TextView descriptionView = (TextView) mEmptyView.findViewById(R.id.description_empty);
if (description == null) {
descriptionView.setVisibility(View.INVISIBLE);
} else {
if (description.contains("<") && description.contains(">")) {
descriptionView.setText(Html.fromHtml(description));
} else {
descriptionView.setText(description);
}
descriptionView.setVisibility(View.VISIBLE);
}
mEmptyViewBoxImages.setVisibility(shouldShowBoxAndPagesAnimation() ? View.VISIBLE : View.GONE);
}
private void showEmptyView() {
if (isAdded()) {
mEmptyView.setVisibility(View.VISIBLE);
}
}
private void hideEmptyView() {
if (isAdded()) {
mEmptyView.setVisibility(View.GONE);
}
}
/*
* called by post adapter when data has been loaded
*/
private final ReaderInterfaces.DataLoadedListener mDataLoadedListener = new ReaderInterfaces.DataLoadedListener() {
@Override
public void onDataLoaded(boolean isEmpty) {
if (!isAdded()) {
return;
}
mRecyclerView.setRefreshing(false);
if (isEmpty) {
setEmptyTitleAndDescription(false);
showEmptyView();
if (shouldShowBoxAndPagesAnimation()) {
startBoxAndPagesAnimation();
}
} else {
hideEmptyView();
if (mRestorePosition > 0) {
AppLog.d(T.READER, "reader post list > restoring position");
mRecyclerView.scrollRecycleViewToPosition(mRestorePosition);
}
}
mRestorePosition = 0;
}
};
/*
* called by post adapter to load older posts when user scrolls to the last post
*/
private final ReaderActions.DataRequestedListener mDataRequestedListener = new ReaderActions.DataRequestedListener() {
@Override
public void onRequestData() {
// skip if update is already in progress
if (isUpdating()) {
return;
}
// request older posts unless we already have the max # to show
switch (getPostListType()) {
case TAG_FOLLOWED:
case TAG_PREVIEW:
if (ReaderPostTable.getNumPostsWithTag(mCurrentTag) < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY) {
// request older posts
updatePostsWithTag(getCurrentTag(), UpdateAction.REQUEST_OLDER);
AnalyticsTracker.track(AnalyticsTracker.Stat.READER_INFINITE_SCROLL);
}
break;
case BLOG_PREVIEW:
int numPosts;
if (mCurrentFeedId != 0) {
numPosts = ReaderPostTable.getNumPostsInFeed(mCurrentFeedId);
} else {
numPosts = ReaderPostTable.getNumPostsInBlog(mCurrentBlogId);
}
if (numPosts < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY) {
updatePostsInCurrentBlogOrFeed(UpdateAction.REQUEST_OLDER);
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;
}
}
};
private ReaderPostAdapter getPostAdapter() {
if (mPostAdapter == null) {
AppLog.d(T.READER, "reader post list > creating post adapter");
Context context = WPActivityUtils.getThemedContext(getActivity());
mPostAdapter = new ReaderPostAdapter(context, getPostListType());
mPostAdapter.setOnPostSelectedListener(this);
mPostAdapter.setOnTagSelectedListener(this);
mPostAdapter.setOnPostPopupListener(this);
mPostAdapter.setOnDataLoadedListener(mDataLoadedListener);
mPostAdapter.setOnDataRequestedListener(mDataRequestedListener);
if (getActivity() instanceof ReaderSiteHeaderView.OnBlogInfoLoadedListener) {
mPostAdapter.setOnBlogInfoLoadedListener((ReaderSiteHeaderView.OnBlogInfoLoadedListener) getActivity());
}
if (getPostListType().isTagType()) {
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;
}
private boolean hasPostAdapter() {
return (mPostAdapter != null);
}
private boolean isPostAdapterEmpty() {
return (mPostAdapter == null || mPostAdapter.isEmpty());
}
private boolean isCurrentTag(final ReaderTag tag) {
return ReaderTag.isSameTag(tag, mCurrentTag);
}
private boolean isCurrentTagName(String tagName) {
return (tagName != null && tagName.equalsIgnoreCase(getCurrentTagName()));
}
private ReaderTag getCurrentTag() {
return mCurrentTag;
}
private String getCurrentTagName() {
return (mCurrentTag != null ? mCurrentTag.getTagSlug() : "");
}
private boolean hasCurrentTag() {
return mCurrentTag != null;
}
private void setCurrentTag(final ReaderTag tag) {
if (tag == null) {
return;
}
// skip if this is already the current tag and the post adapter is already showing it
if (isCurrentTag(tag)
&& hasPostAdapter()
&& getPostAdapter().isCurrentTag(tag)) {
return;
}
mCurrentTag = tag;
switch (getPostListType()) {
case TAG_FOLLOWED:
// remember this as the current tag if viewing followed tag
AppPrefs.setReaderTag(tag);
break;
case TAG_PREVIEW:
mTagPreviewHistory.push(tag.getTagSlug());
break;
}
getPostAdapter().setCurrentTag(tag);
hideNewPostsBar();
showLoadingProgress(false);
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
*/
private boolean goBackInTagHistory() {
if (mTagPreviewHistory.empty()) {
return false;
}
String tagName = mTagPreviewHistory.pop();
if (isCurrentTagName(tagName)) {
if (mTagPreviewHistory.empty()) {
return false;
}
tagName = mTagPreviewHistory.pop();
}
ReaderTag newTag = ReaderUtils.getTagFromTagName(tagName, ReaderTagType.FOLLOWED);
setCurrentTag(newTag);
return true;
}
/*
* load tags on which the main data will be filtered
*/
private void loadTags(FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener) {
new LoadTagsTask(listener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/*
* refresh adapter so latest posts appear
*/
private void refreshPosts() {
hideNewPostsBar();
if (hasPostAdapter()) {
getPostAdapter().refresh();
}
}
/*
* same as above but clears posts before refreshing
*/
private void reloadPosts() {
hideNewPostsBar();
if (hasPostAdapter()) {
getPostAdapter().reload();
}
}
/*
* reload the list of tags for the dropdown filter
*/
private void reloadTags() {
if (isAdded() && mRecyclerView != null) {
mRecyclerView.refreshFilterCriteriaOptions();
}
}
/*
* get posts for the current blog from the server
*/
private void updatePostsInCurrentBlogOrFeed(final UpdateAction updateAction) {
if (!NetworkUtils.isNetworkAvailable(getActivity())) {
AppLog.i(T.READER, "reader post list > network unavailable, canceled blog update");
return;
}
if (mCurrentFeedId != 0) {
ReaderPostService.startServiceForFeed(getActivity(), mCurrentFeedId, updateAction);
} else {
ReaderPostService.startServiceForBlog(getActivity(), mCurrentBlogId, updateAction);
}
}
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.UpdatePostsStarted event) {
if (!isAdded()) return;
setIsUpdating(true, event.getAction());
setEmptyTitleAndDescription(false);
}
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.UpdatePostsEnded event) {
if (!isAdded()) return;
setIsUpdating(false, event.getAction());
if (event.getReaderTag() != null && !isCurrentTag(event.getReaderTag())) {
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
// beyond the first post
if (event.getResult() == ReaderActions.UpdateResult.HAS_NEW
&& event.getAction() == UpdateAction.REQUEST_NEWER
&& getPostListType() == ReaderPostListType.TAG_FOLLOWED
&& !isPostAdapterEmpty()
&& (!isAdded() || !mRecyclerView.isFirstItemVisible())) {
showNewPostsBar();
} else if (event.getResult().isNewOrChanged()) {
refreshPosts();
} else {
boolean requestFailed = (event.getResult() == ReaderActions.UpdateResult.FAILED);
setEmptyTitleAndDescription(requestFailed);
// if we requested posts in order to fill a gap but the request failed or didn't
// return any posts, reload the adapter so the gap marker is reset (hiding its
// progress bar)
if (event.getAction() == UpdateAction.REQUEST_OLDER_THAN_GAP) {
reloadPosts();
}
}
}
/*
* get latest posts for this tag from the server
*/
private void updatePostsWithTag(ReaderTag tag, UpdateAction updateAction) {
if (!isAdded()) return;
if (!NetworkUtils.isNetworkAvailable(getActivity())) {
AppLog.i(T.READER, "reader post list > network unavailable, canceled tag update");
return;
}
if (tag == null) {
AppLog.w(T.READER, "null tag passed to updatePostsWithTag");
return;
}
AppLog.d(T.READER, "reader post list > updating tag " + tag.getTagNameForLog() + ", updateAction=" + updateAction.name());
ReaderPostService.startServiceForTag(getActivity(), tag, updateAction);
}
private void updateCurrentTag() {
updatePostsWithTag(getCurrentTag(), UpdateAction.REQUEST_NEWER);
}
/*
* update the current tag if it's time to do so - note that the check is done in the
* background since it can be expensive and this is called when the fragment is
* resumed, which on slower devices can result in a janky experience
*/
private void updateCurrentTagIfTime() {
if (!isAdded() || !hasCurrentTag()) {
return;
}
new Thread() {
@Override
public void run() {
if (ReaderTagTable.shouldAutoUpdateTag(getCurrentTag()) && isAdded()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
updateCurrentTag();
}
});
}
}
}.start();
}
private boolean isUpdating() {
return mIsUpdating;
}
/*
* show/hide progress bar which appears at the bottom of the activity when loading more posts
*/
private void showLoadingProgress(boolean showProgress) {
if (isAdded() && mProgress != null) {
if (showProgress) {
mProgress.bringToFront();
mProgress.setVisibility(View.VISIBLE);
} else {
mProgress.setVisibility(View.GONE);
}
}
}
private void setIsUpdating(boolean isUpdating, UpdateAction updateAction) {
if (!isAdded() || mIsUpdating == isUpdating) {
return;
}
if (updateAction == UpdateAction.REQUEST_OLDER) {
// show/hide progress bar at bottom if these are older posts
showLoadingProgress(isUpdating);
} else if (isUpdating && isPostAdapterEmpty()) {
// show swipe-to-refresh if update started and no posts are showing
mRecyclerView.setRefreshing(true);
} else if (!isUpdating) {
// hide swipe-to-refresh progress if update is complete
mRecyclerView.setRefreshing(false);
}
mIsUpdating = isUpdating;
// if swipe-to-refresh isn't active, keep it disabled during an update - this prevents
// doing a refresh while another update is already in progress
if (mRecyclerView != null && !mRecyclerView.isRefreshing()) {
mRecyclerView.setSwipeToRefreshEnabled(!isUpdating && isSwipeToRefreshSupported());
}
}
/*
* swipe-to-refresh isn't supported for search results since they're really brief snapshots
* and are unlikely to show new posts due to the way they're sorted
*/
private boolean isSwipeToRefreshSupported() {
return getPostListType() != ReaderPostListType.SEARCH_RESULTS;
}
/*
* bar that appears at the top when new posts have been retrieved
*/
private boolean isNewPostsBarShowing() {
return (mNewPostsBar != null && mNewPostsBar.getVisibility() == View.VISIBLE);
}
/*
* scroll listener assigned to the recycler when the "new posts" bar is shown to hide
* it upon scrolling
*/
private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
hideNewPostsBar();
}
};
private void showNewPostsBar() {
if (!isAdded() || isNewPostsBarShowing()) {
return;
}
AniUtils.startAnimation(mNewPostsBar, R.anim.reader_top_bar_in);
mNewPostsBar.setVisibility(View.VISIBLE);
// assign the scroll listener to hide the bar when the recycler is scrolled, but don't assign
// it right away since the user may be scrolling when the bar appears (which would cause it
// to disappear as soon as it's displayed)
mRecyclerView.postDelayed(new Runnable() {
@Override
public void run() {
if (isAdded() && isNewPostsBarShowing()) {
mRecyclerView.addOnScrollListener(mOnScrollListener);
}
}
}, 1000L);
// remove the gap marker if it's showing, since it's no longer valid
getPostAdapter().removeGapMarker();
}
private void hideNewPostsBar() {
if (!isAdded() || !isNewPostsBarShowing() || mIsAnimatingOutNewPostsBar) {
return;
}
mIsAnimatingOutNewPostsBar = true;
// remove the onScrollListener assigned in showNewPostsBar()
mRecyclerView.removeOnScrollListener(mOnScrollListener);
Animation.AnimationListener listener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) { }
@Override
public void onAnimationEnd(Animation animation) {
if (isAdded()) {
mNewPostsBar.setVisibility(View.GONE);
mIsAnimatingOutNewPostsBar = false;
}
}
@Override
public void onAnimationRepeat(Animation animation) { }
};
AniUtils.startAnimation(mNewPostsBar, R.anim.reader_top_bar_out, listener);
}
/*
* are we showing all posts with a specific tag (followed or previewed), or all
* posts in a specific blog?
*/
private ReaderPostListType getPostListType() {
return (mPostListType != null ? mPostListType : ReaderTypes.DEFAULT_POST_LIST_TYPE);
}
/*
* called from adapter when user taps a post
*/
@Override
public void onPostSelected(ReaderPost post) {
if (!isAdded() || post == null) return;
// "discover" posts that highlight another post should open the original (source) post when tapped
if (post.isDiscoverPost()) {
ReaderPostDiscoverData discoverData = post.getDiscoverData();
if (discoverData != null && discoverData.getDiscoverType() == ReaderPostDiscoverData.DiscoverType.EDITOR_PICK) {
if (discoverData.getBlogId() != 0 && discoverData.getPostId() != 0) {
ReaderActivityLauncher.showReaderPostDetail(
getActivity(),
discoverData.getBlogId(),
discoverData.getPostId());
return;
} else if (discoverData.hasPermalink()) {
// if we don't have a blogId/postId, we sadly resort to showing the post
// in a WebView activity - this will happen for non-JP self-hosted
ReaderActivityLauncher.openUrl(getActivity(), discoverData.getPermaLink());
return;
}
}
}
// if this is a cross-post, we want to show the original post
if (post.isXpost()) {
ReaderActivityLauncher.showReaderPostDetail(getActivity(), post.xpostBlogId, post.xpostPostId);
return;
}
ReaderPostListType type = getPostListType();
switch (type) {
case TAG_FOLLOWED:
case TAG_PREVIEW:
ReaderActivityLauncher.showReaderPostPagerForTag(
getActivity(),
getCurrentTag(),
getPostListType(),
post.blogId,
post.postId);
break;
case BLOG_PREVIEW:
ReaderActivityLauncher.showReaderPostPagerForBlog(
getActivity(),
post.blogId,
post.postId);
break;
case SEARCH_RESULTS:
AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_SEARCH_RESULT_TAPPED, post);
ReaderActivityLauncher.showReaderPostDetail(getActivity(), post.blogId, post.postId);
break;
}
}
/*
* called from adapter when user taps a tag on a post to display tag preview
*/
@Override
public void onTagSelected(String tagName) {
if (!isAdded()) return;
ReaderTag tag = ReaderUtils.getTagFromTagName(tagName, ReaderTagType.FOLLOWED);
if (getPostListType().equals(ReaderPostListType.TAG_PREVIEW)) {
// user is already previewing a tag, so change current tag in existing preview
setCurrentTag(tag);
} else {
// user isn't previewing a tag, so open in tag preview
ReaderActivityLauncher.showReaderTagPreview(getActivity(), tag);
}
}
/*
* called when user selects a tag from the tag toolbar
*/
private void onTagChanged(ReaderTag tag) {
if (!isAdded() || isCurrentTag(tag)) return;
trackTagLoaded(tag);
AppLog.d(T.READER, String.format("reader post list > tag %s displayed", tag.getTagNameForLog()));
setCurrentTag(tag);
}
private void trackTagLoaded(ReaderTag tag) {
AnalyticsTracker.Stat stat = null;
if (tag.isDiscover()) {
stat = AnalyticsTracker.Stat.READER_DISCOVER_VIEWED;
} else if (tag.isTagTopic()) {
stat = AnalyticsTracker.Stat.READER_TAG_LOADED;
} else if (tag.isListTopic()) {
stat = AnalyticsTracker.Stat.READER_LIST_LOADED;
}
if (stat == null) return;
Map<String, String> properties = new HashMap<>();
properties.put("tag", tag.getTagSlug());
AnalyticsTracker.track(stat, properties);
}
/*
* called when user taps "..." icon next to a post
*/
@Override
public void onShowPostPopup(View view, final ReaderPost post) {
if (view == null || post == null || !isAdded()) return;
Context context = view.getContext();
final ListPopupWindow listPopup = new ListPopupWindow(context);
listPopup.setAnchorView(view);
listPopup.setWidth(context.getResources().getDimensionPixelSize(R.dimen.menu_item_width));
listPopup.setModal(true);
List<Integer> menuItems = new ArrayList<>();
boolean isFollowed = ReaderPostTable.isPostFollowed(post);
if (isFollowed) {
menuItems.add(ReaderMenuAdapter.ITEM_UNFOLLOW);
} else {
menuItems.add(ReaderMenuAdapter.ITEM_FOLLOW);
}
if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) {
menuItems.add(ReaderMenuAdapter.ITEM_BLOCK);
}
listPopup.setAdapter(new ReaderMenuAdapter(context, menuItems));
listPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (!isAdded()) return;
listPopup.dismiss();
switch((int) id) {
case ReaderMenuAdapter.ITEM_FOLLOW:
case ReaderMenuAdapter.ITEM_UNFOLLOW:
toggleFollowStatusForPost(post);
break;
case ReaderMenuAdapter.ITEM_BLOCK:
blockBlogForPost(post);
break;
}
}
});
listPopup.show();
}
/*
* purge reader db if it hasn't been done yet
*/
private void purgeDatabaseIfNeeded() {
if (!mHasPurgedReaderDb) {
AppLog.d(T.READER, "reader post list > purging database");
mHasPurgedReaderDb = true;
ReaderDatabase.purgeAsync();
}
}
/*
* start background service to get the latest followed tags and blogs if it's time to do so
*/
private void updateFollowedTagsAndBlogsIfNeeded() {
if (mLastAutoUpdateDt != null) {
int minutesSinceLastUpdate = DateTimeUtils.minutesBetween(mLastAutoUpdateDt, new Date());
if (minutesSinceLastUpdate < 120) {
return;
}
}
AppLog.d(T.READER, "reader post list > updating tags and blogs");
mLastAutoUpdateDt = new Date();
ReaderUpdateService.startService(getActivity(), EnumSet.of(UpdateTask.TAGS, UpdateTask.FOLLOWED_BLOGS));
}
@Override
public void onScrollToTop() {
if (isAdded() && getCurrentPosition() > 0) {
mRecyclerView.smoothScrollToPosition(0);
mRecyclerView.showToolbar();
}
}
public static void resetLastUpdateDate() {
mLastAutoUpdateDt = null;
}
private class LoadTagsTask extends AsyncTask<Void, Void, ReaderTagList> {
private final FilteredRecyclerView.FilterCriteriaAsyncLoaderListener mFilterCriteriaLoaderListener;
public LoadTagsTask(FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener){
mFilterCriteriaLoaderListener = listener;
}
@Override
protected ReaderTagList doInBackground(Void... voids) {
ReaderTagList tagList = ReaderTagTable.getDefaultTags();
tagList.addAll(ReaderTagTable.getCustomListTags());
tagList.addAll(ReaderTagTable.getFollowedTags());
return tagList;
}
@Override
protected void onPostExecute(ReaderTagList tagList) {
if (tagList != null && !tagList.isSameList(mTags)) {
mTags.clear();
mTags.addAll(tagList);
if (mFilterCriteriaLoaderListener != null)
//noinspection unchecked
mFilterCriteriaLoaderListener.onFilterCriteriasLoaded((List)mTags);
}
}
}
}