package org.wikipedia.feed; import android.net.Uri; import android.os.Bundle; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import org.wikipedia.BackPressedHandler; import org.wikipedia.BuildConfig; import org.wikipedia.R; import org.wikipedia.WikipediaApp; import org.wikipedia.activity.FragmentUtil; import org.wikipedia.analytics.FeedFunnel; import org.wikipedia.feed.featured.FeaturedArticleCard; import org.wikipedia.feed.image.FeaturedImage; import org.wikipedia.feed.image.FeaturedImageCard; import org.wikipedia.feed.model.Card; import org.wikipedia.feed.news.NewsItemCard; import org.wikipedia.feed.view.FeedAdapter; import org.wikipedia.feed.view.FeedView; import org.wikipedia.history.HistoryEntry; import org.wikipedia.settings.Prefs; import org.wikipedia.settings.SettingsActivity; import org.wikipedia.util.FeedbackUtil; import org.wikipedia.util.ResourceUtil; import org.wikipedia.util.UriUtil; import org.wikipedia.views.ExploreOverflowView; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.Unbinder; public class FeedFragment extends Fragment implements BackPressedHandler { @BindView(R.id.feed_swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout; @BindView(R.id.fragment_feed_feed) FeedView feedView; @BindView(R.id.fragment_feed_header) View feedHeader; private Unbinder unbinder; private FeedAdapter<?> feedAdapter; private WikipediaApp app; private FeedCoordinator coordinator; private FeedFunnel funnel; private final FeedAdapter.Callback feedCallback = new FeedCallback(); private FeedScrollListener feedScrollListener = new FeedScrollListener(); private OverflowCallback overflowCallback = new OverflowCallback(); private boolean searchIconVisible; public interface Callback { void onFeedTabListRequested(); void onFeedSearchRequested(); void onFeedVoiceSearchRequested(); void onFeedSelectPage(HistoryEntry entry); void onFeedAddPageToList(HistoryEntry entry); void onFeedAddFeaturedPageToList(FeedFragment fragment, FeaturedArticleCard card, HistoryEntry entry); void onFeedRemovePageFromList(FeedFragment fragment, Card card, HistoryEntry entry); void onFeedSharePage(HistoryEntry entry); void onFeedNewsItemSelected(NewsItemCard card); void onFeedShareImage(FeaturedImageCard card); void onFeedDownloadImage(FeaturedImage image); void onFeaturedImageSelected(FeaturedImageCard card); void onLoginRequested(); @NonNull View getOverflowMenuAnchor(); } @NonNull public static FeedFragment newInstance() { FeedFragment fragment = new FeedFragment(); fragment.setRetainInstance(true); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); app = WikipediaApp.getInstance(); coordinator = new FeedCoordinator(app); coordinator.more(app.getWikiSite()); funnel = new FeedFunnel(app); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); View view = inflater.inflate(R.layout.fragment_feed, container, false); unbinder = ButterKnife.bind(this, view); feedAdapter = new FeedAdapter<>(coordinator, feedCallback); feedView.setAdapter(feedAdapter); feedView.setCallback(feedCallback); feedView.addOnScrollListener(feedScrollListener); swipeRefreshLayout.setColorSchemeResources(R.color.foundation_blue); swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { funnel.refresh(coordinator.getAge()); coordinator.reset(); feedAdapter.notifyDataSetChanged(); coordinator.more(app.getWikiSite()); } }); coordinator.setFeedUpdateListener(new FeedCoordinator.FeedUpdateListener() { @Override public void insert(Card card, int pos) { if (isAdded()) { swipeRefreshLayout.setRefreshing(false); if (feedView != null && feedAdapter != null) { feedAdapter.notifyItemInserted(pos); } } } @Override public void swap(Card card, int pos) { if (isAdded()) { swipeRefreshLayout.setRefreshing(false); if (feedView != null && feedAdapter != null) { feedAdapter.notifyItemChanged(pos); } } } @Override public void remove(Card card, int pos) { if (isAdded()) { swipeRefreshLayout.setRefreshing(false); if (feedView != null && feedAdapter != null) { feedAdapter.notifyItemRemoved(pos); } } } }); return view; } public void notifyItemChanged(@NonNull Card card) { if (feedAdapter != null && feedAdapter.getItemPosition(card) > -1) { feedAdapter.notifyItemChanged(feedAdapter.getItemPosition(card)); } } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setHasOptionsMenu(true); } @Override public void onResume() { super.onResume(); funnel.enter(); } @Override public void onPause() { super.onPause(); funnel.exit(); } @Override public void setUserVisibleHint(boolean visible) { super.setUserVisibleHint(visible); if (!isAdded()) { return; } if (visible) { funnel.enter(); } else { funnel.exit(); } } @Override public void onDestroyView() { coordinator.setFeedUpdateListener(null); swipeRefreshLayout.setOnRefreshListener(null); feedView.removeOnScrollListener(feedScrollListener); feedView.setCallback((FeedAdapter.Callback) null); feedView.setAdapter(null); feedAdapter = null; unbinder.unbind(); unbinder = null; super.onDestroyView(); } @Override public void onDestroy() { super.onDestroy(); app.getRefWatcher().watch(this); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_feed, menu); } @Override public void onPrepareOptionsMenu(Menu menu) { MenuItem searchItem = menu.findItem(R.id.menu_feed_search); MenuItem tabsItem = menu.findItem(R.id.menu_feed_tabs); if (searchItem != null) { searchItem.setVisible(searchIconVisible); } if (tabsItem != null) { int tabCount = Prefs.getTabCount(); tabsItem.setIcon(ResourceUtil.getTabListIcon(tabCount)); tabsItem.setVisible(tabCount > 0); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_feed_search: if (getCallback() != null) { getCallback().onFeedSearchRequested(); } return true; case R.id.menu_feed_tabs: if (getCallback() != null) { getCallback().onFeedTabListRequested(); } return true; case R.id.menu_overflow_button: Callback callback = getCallback(); if (callback == null) { return false; } showOverflowMenu(callback.getOverflowMenuAnchor()); return true; default: return super.onOptionsItemSelected(item); } } @Override public boolean onBackPressed() { return false; } @Nullable private Callback getCallback() { return FragmentUtil.getCallback(this, Callback.class); } private class FeedCallback implements FeedAdapter.Callback { @Override public void onShowCard(@Nullable Card card) { if (card != null) { funnel.cardShown(card.type()); } } @Override public void onRequestMore() { funnel.requestMore(coordinator.getAge()); coordinator.more(app.getWikiSite()); } @Override public void onRetryFromOffline() { funnel.requestMore(coordinator.getAge()); coordinator.retryFromOffline(app.getWikiSite()); } @Override public void onError(@NonNull Throwable t) { FeedbackUtil.showError(getActivity(), t); } @Override public void onSelectPage(@NonNull Card card, @NonNull HistoryEntry entry) { if (getCallback() != null) { getCallback().onFeedSelectPage(entry); funnel.cardClicked(card.type()); } } @Override public void onAddPageToList(@NonNull HistoryEntry entry) { if (getCallback() != null) { getCallback().onFeedAddPageToList(entry); } } @Override public void onAddFeaturedPageToList(@NonNull FeaturedArticleCard card, @NonNull HistoryEntry entry) { if (getCallback() != null) { getCallback().onFeedAddFeaturedPageToList(FeedFragment.this, card, entry); } } @Override public void onRemoveFeaturedPageFromList(@NonNull FeaturedArticleCard card, @NonNull HistoryEntry entry) { if (getCallback() != null) { getCallback().onFeedRemovePageFromList(FeedFragment.this, card, entry); } } @Override public void onSharePage(@NonNull HistoryEntry entry) { if (getCallback() != null) { getCallback().onFeedSharePage(entry); } } @Override public void onSearchRequested() { if (getCallback() != null) { getCallback().onFeedSearchRequested(); } } @Override public void onVoiceSearchRequested() { if (getCallback() != null) { getCallback().onFeedVoiceSearchRequested(); } } @Override public boolean onRequestDismissCard(@NonNull Card card) { int position = coordinator.dismissCard(card); funnel.dismissCard(card.type(), position); showDismissCardUndoSnackbar(card, position); return true; } @Override public void onSwiped(@IntRange(from = 0) int itemPos) { onRequestDismissCard(coordinator.getCards().get(itemPos)); } @Override public void onNewsItemSelected(@NonNull NewsItemCard card) { if (getCallback() != null) { funnel.cardClicked(card.type()); getCallback().onFeedNewsItemSelected(card); } } @Override public void onShareImage(@NonNull FeaturedImageCard card) { if (getCallback() != null) { getCallback().onFeedShareImage(card); } } @Override public void onDownloadImage(@NonNull FeaturedImage image) { if (getCallback() != null) { getCallback().onFeedDownloadImage(image); } } @Override public void onFeaturedImageSelected(@NonNull FeaturedImageCard card) { if (getCallback() != null) { funnel.cardClicked(card.type()); getCallback().onFeaturedImageSelected(card); } } @Override public void onAnnouncementPositiveAction(@NonNull Card card, @NonNull Uri uri) { funnel.cardClicked(card.type()); UriUtil.handleExternalLink(getContext(), uri); } @Override public void onAnnouncementNegativeAction(@NonNull Card card) { onRequestDismissCard(card); } } private class FeedScrollListener extends RecyclerView.OnScrollListener { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); int yOffset = feedView.computeVerticalScrollOffset() * 2; if (yOffset <= feedHeader.getHeight() || feedHeader.getTranslationY() > -feedHeader.getHeight()) { feedHeader.setTranslationY(-yOffset); } boolean shouldShowSearchIcon = feedView.getFirstVisibleItemPosition() != 0; if (shouldShowSearchIcon != searchIconVisible) { searchIconVisible = shouldShowSearchIcon; getActivity().supportInvalidateOptionsMenu(); } } } private void showDismissCardUndoSnackbar(final Card card, final int position) { Snackbar snackbar = FeedbackUtil.makeSnackbar(getActivity(), getString(R.string.menu_feed_card_dismissed), FeedbackUtil.LENGTH_DEFAULT); snackbar.setAction(R.string.feed_undo_dismiss_card, new View.OnClickListener() { @Override public void onClick(View v) { coordinator.undoDismissCard(card, position); } }); snackbar.show(); } private void showOverflowMenu(@NonNull View anchor) { ExploreOverflowView overflowView = new ExploreOverflowView(getContext()); overflowView.show(anchor, overflowCallback); } private class OverflowCallback implements ExploreOverflowView.Callback { @Override public void loginClick() { if (getCallback() != null) { getCallback().onLoginRequested(); } } @Override public void settingsClick() { startActivityForResult(SettingsActivity.newIntent(getContext()), SettingsActivity.ACTIVITY_REQUEST_SHOW_SETTINGS); } @Override public void donateClick() { UriUtil.visitInExternalBrowser(getContext(), Uri.parse(String.format(getString(R.string.donate_url), BuildConfig.VERSION_NAME, WikipediaApp.getInstance().getSystemLanguageCode()))); } @Override public void logoutClick() { WikipediaApp.getInstance().logOut(); FeedbackUtil.showMessage(FeedFragment.this, R.string.toast_logout_complete); } } }