| 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); |
| } |
| } |
| } |
| |
| } |
| |