package org.wikipedia.readinglist;
import android.content.DialogInterface;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CollapsingToolbarLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.text.Spanned;
import android.text.TextUtils;
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 android.widget.TextView;
import org.wikipedia.Constants;
import org.wikipedia.R;
import org.wikipedia.analytics.ReadingListsFunnel;
import org.wikipedia.concurrency.CallbackTask;
import org.wikipedia.history.HistoryEntry;
import org.wikipedia.history.SearchActionModeCallback;
import org.wikipedia.main.MainActivity;
import org.wikipedia.page.ExclusiveBottomSheetPresenter;
import org.wikipedia.page.PageActivity;
import org.wikipedia.page.PageTitle;
import org.wikipedia.readinglist.page.ReadingListPage;
import org.wikipedia.readinglist.page.database.ReadingListDaoProxy;
import org.wikipedia.readinglist.page.database.ReadingListPageDao;
import org.wikipedia.readinglist.sync.ReadingListSynchronizer;
import org.wikipedia.settings.Prefs;
import org.wikipedia.util.FeedbackUtil;
import org.wikipedia.util.ResourceUtil;
import org.wikipedia.util.ShareUtil;
import org.wikipedia.util.StringUtil;
import org.wikipedia.views.DefaultViewHolder;
import org.wikipedia.views.DrawableItemDecoration;
import org.wikipedia.views.MultiSelectActionModeCallback;
import org.wikipedia.views.PageItemView;
import org.wikipedia.views.SearchEmptyView;
import org.wikipedia.views.SwipeableItemTouchHelperCallback;
import org.wikipedia.views.TextInputDialog;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
import static org.wikipedia.readinglist.ReadingListActivity.EXTRA_READING_LIST_TITLE;
import static org.wikipedia.readinglist.ReadingLists.SORT_BY_NAME_ASC;
public class ReadingListFragment extends Fragment implements ReadingListItemActionsDialog.Callback {
@BindView(R.id.reading_list_toolbar) Toolbar toolbar;
@BindView(R.id.reading_list_toolbar_container) CollapsingToolbarLayout toolBarLayout;
@BindView(R.id.reading_list_app_bar) AppBarLayout appBarLayout;
@BindView(R.id.reading_list_header) ReadingListHeaderView headerImageView;
@BindView(R.id.reading_list_contents) RecyclerView recyclerView;
@BindView(R.id.reading_list_empty_text) TextView emptyView;
@BindView(R.id.search_empty_view) SearchEmptyView searchEmptyView;
private Unbinder unbinder;
@Nullable private ReadingList readingList;
@Nullable private String readingListTitle;
private ReadingListPageItemAdapter adapter = new ReadingListPageItemAdapter();
private ReadingListItemView headerView;
@Nullable private ActionMode actionMode;
private AppBarListener appBarListener = new AppBarListener();
private boolean showOverflowMenu;
@NonNull private ReadingLists readingLists = new ReadingLists();
private ReadingListsFunnel funnel = new ReadingListsFunnel();
private HeaderCallback headerCallback = new HeaderCallback();
private ItemCallback itemCallback = new ItemCallback();
private SearchCallback searchActionModeCallback = new SearchCallback();
private MultiSelectActionModeCallback multiSelectActionModeCallback = new MultiSelectCallback();
private ExclusiveBottomSheetPresenter bottomSheetPresenter = new ExclusiveBottomSheetPresenter();
@NonNull private List<ReadingListPage> displayedPages = new ArrayList<>();
private String currentSearchQuery;
@NonNull
public static ReadingListFragment newInstance(@NonNull String listTitle) {
ReadingListFragment instance = new ReadingListFragment();
Bundle args = new Bundle();
args.putString(EXTRA_READING_LIST_TITLE, listTitle);
instance.setArguments(args);
return instance;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_reading_list, container, false);
unbinder = ButterKnife.bind(this, view);
getAppCompatActivity().setSupportActionBar(toolbar);
getAppCompatActivity().getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getAppCompatActivity().getSupportActionBar().setTitle("");
appBarLayout.addOnOffsetChangedListener(appBarListener);
toolBarLayout.setCollapsedTitleTextColor(Color.WHITE);
ItemTouchHelper.Callback touchCallback = new SwipeableItemTouchHelperCallback(getContext());
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(touchCallback);
itemTouchHelper.attachToRecyclerView(recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
recyclerView.addItemDecoration(new DrawableItemDecoration(getContext(),
ResourceUtil.getThemedAttributeId(getContext(), R.attr.list_separator_drawable), true));
headerView = new ReadingListItemView(getContext());
headerView.setCallback(headerCallback);
headerView.setClickable(false);
headerView.setThumbnailVisible(false);
headerView.setShowDescriptionEmptyHint(true);
headerView.setTitleTextAppearance(R.style.ReadingListTitleTextAppearance);
readingListTitle = getArguments().getString(EXTRA_READING_LIST_TITLE);
updateReadingListData();
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
}
@Override public void onDestroyView() {
readingList = null;
readingLists.set(Collections.<ReadingList>emptyList());
recyclerView.setAdapter(null);
appBarLayout.removeOnOffsetChangedListener(appBarListener);
unbinder.unbind();
unbinder = null;
super.onDestroyView();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_reading_lists, menu);
if (showOverflowMenu) {
inflater.inflate(R.menu.menu_reading_list_item, menu);
}
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
MenuItem sortByNameItem = menu.findItem(R.id.menu_sort_by_name);
MenuItem sortByRecentItem = menu.findItem(R.id.menu_sort_by_recent);
int sortMode = Prefs.getReadingListPageSortMode(ReadingLists.SORT_BY_NAME_ASC);
sortByNameItem.setTitle(sortMode == ReadingLists.SORT_BY_NAME_ASC ? R.string.reading_list_sort_by_name_desc : R.string.reading_list_sort_by_name);
sortByRecentItem.setTitle(sortMode == ReadingLists.SORT_BY_RECENT_DESC ? R.string.reading_list_sort_by_recent_desc : R.string.reading_list_sort_by_recent);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_search_lists:
getAppCompatActivity().startSupportActionMode(searchActionModeCallback);
return true;
case R.id.menu_sort_by_name:
setSortMode(ReadingLists.SORT_BY_NAME_ASC, ReadingLists.SORT_BY_NAME_DESC);
return true;
case R.id.menu_sort_by_recent:
setSortMode(ReadingLists.SORT_BY_RECENT_DESC, ReadingLists.SORT_BY_RECENT_ASC);
return true;
case R.id.menu_reading_list_rename:
rename();
return true;
case R.id.menu_reading_list_edit_description:
editDescription();
return true;
case R.id.menu_reading_list_delete:
delete();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private AppCompatActivity getAppCompatActivity() {
return (AppCompatActivity) getActivity();
}
private void update() {
if (readingList == null) {
return;
}
emptyView.setVisibility(readingList.getPages().isEmpty() ? View.VISIBLE : View.GONE);
headerView.setReadingList(readingList);
headerImageView.setReadingList(readingList);
readingList.sort(Prefs.getReadingListPageSortMode(SORT_BY_NAME_ASC));
setSearchQuery(currentSearchQuery);
}
private void updateReadingListData() {
ReadingList.DAO.queryMruLists(null, new CallbackTask.DefaultCallback<List<ReadingList>>() {
@Override public void success(List<ReadingList> lists) {
if (getActivity() == null) {
return;
}
readingLists.set(lists);
readingList = readingLists.get(readingListTitle);
if (readingList != null) {
searchEmptyView.setEmptyText(getString(R.string.search_reading_list_no_results,
readingList.getTitle()));
}
update();
}
});
}
private void setSearchQuery(@Nullable String query) {
if (readingList == null) {
return;
}
currentSearchQuery = query;
displayedPages.clear();
if (TextUtils.isEmpty(query)) {
displayedPages.addAll(readingList.getPages());
} else {
query = query.toUpperCase();
for (ReadingListPage page : readingList.getPages()) {
if (page.title().toUpperCase().contains(query.toUpperCase())) {
displayedPages.add(page);
}
}
}
adapter.notifyDataSetChanged();
updateEmptyState(query);
}
private void updateEmptyState(@Nullable String searchQuery) {
if (TextUtils.isEmpty(searchQuery)) {
searchEmptyView.setVisibility(View.GONE);
emptyView.setVisibility(displayedPages.isEmpty() ? View.VISIBLE : View.GONE);
} else {
searchEmptyView.setVisibility(displayedPages.isEmpty() ? View.VISIBLE : View.GONE);
emptyView.setVisibility(View.GONE);
}
}
private void setSortMode(int sortModeAsc, int sortModeDesc) {
int sortMode = Prefs.getReadingListPageSortMode(ReadingLists.SORT_BY_NAME_ASC);
if (sortMode != sortModeAsc) {
sortMode = sortModeAsc;
} else {
sortMode = sortModeDesc;
}
Prefs.setReadingListPageSortMode(sortMode);
getActivity().supportInvalidateOptionsMenu();
update();
}
private void showDeleteItemsUndoSnackbar(final ReadingList readingList, final List<ReadingListPage> pages) {
String message = pages.size() == 1
? String.format(getString(R.string.reading_list_item_deleted), pages.get(0).title())
: String.format(getString(R.string.reading_list_items_deleted), pages.size());
Snackbar snackbar = FeedbackUtil.makeSnackbar(getActivity(), message,
FeedbackUtil.LENGTH_DEFAULT);
snackbar.setAction(R.string.reading_list_item_delete_undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
for (ReadingListPage page : pages) {
ReadingList.DAO.addTitleToList(readingList, page, true);
ReadingListSynchronizer.instance().bumpRevAndSync();
ReadingListPageDao.instance().markOutdated(page);
}
update();
}
});
snackbar.show();
}
private void rename() {
if (readingList == null) {
return;
}
ReadingListTitleDialog.readingListTitleDialog(getContext(), readingList.getTitle(),
readingLists.getTitlesExcept(readingList.getTitle()),
new ReadingListTitleDialog.Callback() {
@Override
public void onSuccess(@NonNull CharSequence text) {
ReadingList.DAO.renameAndSaveListInfo(readingList, text.toString());
ReadingListSynchronizer.instance().bumpRevAndSync();
update();
funnel.logModifyList(readingList, readingLists.size());
}
}).show();
}
private void editDescription() {
if (readingList == null) {
return;
}
TextInputDialog.newInstance(getContext(), new TextInputDialog.DefaultCallback() {
@Override
public void onShow(@NonNull TextInputDialog dialog) {
dialog.setHint(R.string.reading_list_description_hint);
dialog.setText(readingList.getDescription());
}
@Override
public void onSuccess(@NonNull CharSequence text) {
readingList.setDescription(text.toString());
ReadingList.DAO.saveListInfo(readingList);
ReadingListSynchronizer.instance().bumpRevAndSync();
update();
funnel.logModifyList(readingList, readingLists.size());
}
}).show();
}
private void finishActionMode() {
if (actionMode != null) {
actionMode.finish();
}
}
private void beginMultiSelect() {
if (SearchCallback.is(actionMode)) {
finishActionMode();
}
if (!MultiSelectCallback.is(actionMode)) {
getAppCompatActivity().startSupportActionMode(multiSelectActionModeCallback);
}
}
private void toggleSelectPage(@Nullable ReadingListPage page) {
if (page == null) {
return;
}
page.setSelected(!page.isSelected());
int selectedCount = getSelectedPageCount();
if (selectedCount == 0) {
finishActionMode();
} else if (actionMode != null) {
actionMode.setTitle(getString(R.string.multi_select_items_selected, selectedCount));
}
adapter.notifyDataSetChanged();
}
private int getSelectedPageCount() {
int selectedCount = 0;
for (ReadingListPage page : displayedPages) {
if (page.isSelected()) {
selectedCount++;
}
}
return selectedCount;
}
private void unselectAllPages() {
if (readingList == null) {
return;
}
for (ReadingListPage page : readingList.getPages()) {
page.setSelected(false);
}
adapter.notifyDataSetChanged();
}
private void deleteSelectedPages() {
if (readingList == null) {
return;
}
List<ReadingListPage> selectedPages = new ArrayList<>();
for (ReadingListPage page : displayedPages) {
if (page.isSelected()) {
selectedPages.add(page);
page.setSelected(false);
ReadingList.DAO.removeTitleFromList(readingList, page);
}
}
if (!selectedPages.isEmpty()) {
ReadingListSynchronizer.instance().bumpRevAndSync();
funnel.logDeleteItem(readingList, readingLists.size());
showDeleteItemsUndoSnackbar(readingList, selectedPages);
update();
}
}
private void deleteSinglePage(@Nullable ReadingListPage page) {
if (readingList == null || page == null) {
return;
}
showDeleteItemsUndoSnackbar(readingList, Collections.singletonList(page));
ReadingList.DAO.removeTitleFromList(readingList, page);
ReadingListSynchronizer.instance().bumpRevAndSync();
funnel.logDeleteItem(readingList, readingLists.size());
update();
}
private void delete() {
if (readingList != null) {
startActivity(MainActivity.newIntent(getContext())
.putExtra(Constants.INTENT_EXTRA_DELETE_READING_LIST, readingList.getTitle()));
getActivity().finish();
}
}
@Override
public void onToggleOffline(int pageIndex) {
ReadingListPage page = readingList == null ? null : readingList.get(pageIndex);
if (page == null) {
return;
}
if (page.isOffline()) {
ReadingList.DAO.anyListContainsTitleAsync(page.key(), new PageListCountCallback(page));
} else {
toggleOffline(page);
}
}
@Override
public void onShare(int pageIndex) {
ReadingListPage page = readingList == null ? null : readingList.get(pageIndex);
if (page != null) {
ShareUtil.shareText(getContext(), ReadingListDaoProxy.pageTitle(page));
}
}
@Override
public void onAddToOther(int pageIndex) {
ReadingListPage page = readingList == null ? null : readingList.get(pageIndex);
if (page != null) {
bottomSheetPresenter.show(getChildFragmentManager(),
AddToReadingListDialog.newInstance(ReadingListDaoProxy.pageTitle(page),
AddToReadingListDialog.InvokeSource.READING_LIST_ACTIVITY));
}
}
@Override
public void onDelete(int pageIndex) {
ReadingListPage page = readingList == null ? null : readingList.get(pageIndex);
deleteSinglePage(page);
}
private void toggleOffline(@NonNull ReadingListPage page) {
ReadingListData.instance().setPageOffline(page, !page.isOffline());
if (getActivity() != null) {
FeedbackUtil.showMessage(getActivity(), page.isOffline()
? R.string.reading_list_article_offline_message
: R.string.reading_list_article_not_offline_message);
adapter.notifyDataSetChanged();
}
}
@SuppressWarnings("checkstyle:magicnumber")
private void showMultiListPageConfirmToggleDialog(@NonNull final ReadingListPage page) {
if (getActivity() == null) {
return;
}
AlertDialog dialog = new AlertDialog.Builder(getContext())
.setTitle(R.string.reading_list_confirm_remove_article_from_offline_title)
.setMessage(getConfirmToggleOfflineMessage(page))
.setPositiveButton(R.string.reading_list_confirm_remove_article_from_offline,
new ConfirmRemoveFromOfflineListener(page))
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.show();
TextView text = (TextView) dialog.findViewById(android.R.id.message);
text.setLineSpacing(0, 1.3f);
}
@NonNull private Spanned getConfirmToggleOfflineMessage(@NonNull ReadingListPage page) {
String result = getString(R.string.reading_list_confirm_remove_article_from_offline_message,
"<b>" + page.title() + "</b>");
for (String key : page.listKeys()) {
result += "<br> <b>• " + ReadingListDaoProxy.listName(key) + "</b>";
}
return StringUtil.fromHtml(result);
}
private class AppBarListener implements AppBarLayout.OnOffsetChangedListener {
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
if (verticalOffset > -appBarLayout.getTotalScrollRange() && showOverflowMenu) {
showOverflowMenu = false;
toolBarLayout.setTitle("");
getAppCompatActivity().supportInvalidateOptionsMenu();
} else if (verticalOffset <= -appBarLayout.getTotalScrollRange() && !showOverflowMenu) {
showOverflowMenu = true;
toolBarLayout.setTitle(readingList.getTitle());
getAppCompatActivity().supportInvalidateOptionsMenu();
}
}
}
private class HeaderCallback implements ReadingListItemView.Callback {
@Override
public void onClick(@NonNull ReadingList readingList) {
}
@Override
public void onRename(@NonNull final ReadingList readingList) {
rename();
}
@Override
public void onEditDescription(@NonNull final ReadingList readingList) {
editDescription();
}
@Override
public void onDelete(@NonNull ReadingList readingList) {
delete();
}
}
private class ReadingListPageItemHolder extends DefaultViewHolder<PageItemView<ReadingListPage>>
implements SwipeableItemTouchHelperCallback.Callback {
private ReadingListPage page;
ReadingListPageItemHolder(PageItemView<ReadingListPage> itemView) {
super(itemView);
}
void bindItem(ReadingListPage page) {
this.page = page;
getView().setItem(page);
getView().setTitle(page.title());
getView().setDescription(page.description());
getView().setImageUrl(page.thumbnailUrl());
getView().setSelected(page.isSelected());
getView().setActionIcon(R.drawable.ic_more_vert_white_24dp);
getView().setActionHint(R.string.abc_action_menu_overflow_description);
getView().setSecondaryActionIcon(R.drawable.ic_download_circle_black_24px, !page.isOffline());
getView().setSecondaryActionHint(R.string.reading_list_article_make_offline);
}
@Override
public void onSwipe() {
deleteSinglePage(page);
}
}
private class ReadingListHeaderHolder extends RecyclerView.ViewHolder {
ReadingListHeaderHolder(View itemView) {
super(itemView);
}
}
private final class ReadingListPageItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HEADER = 0;
private static final int TYPE_ITEM = 1;
@Override
public int getItemCount() {
return 1 + displayedPages.size();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
if (type == TYPE_HEADER) {
return new ReadingListHeaderHolder(headerView);
}
return new ReadingListPageItemHolder(new PageItemView<ReadingListPage>(getContext()));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int pos) {
if (readingList != null && holder instanceof ReadingListPageItemHolder) {
((ReadingListPageItemHolder) holder).bindItem(displayedPages.get(pos - 1));
}
}
@Override
public int getItemViewType(int position) {
return position == 0 ? TYPE_HEADER : TYPE_ITEM;
}
@Override public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
if (holder instanceof ReadingListPageItemHolder) {
((ReadingListPageItemHolder) holder).getView().setCallback(itemCallback);
}
}
@Override public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
if (holder instanceof ReadingListPageItemHolder) {
((ReadingListPageItemHolder) holder).getView().setCallback(null);
}
super.onViewDetachedFromWindow(holder);
}
}
private class ItemCallback implements PageItemView.Callback<ReadingListPage> {
@Override
public void onClick(@Nullable ReadingListPage page) {
if (MultiSelectCallback.is(actionMode)) {
toggleSelectPage(page);
} else if (page != null && readingList != null) {
PageTitle title = ReadingListDaoProxy.pageTitle(page);
HistoryEntry entry = new HistoryEntry(title, HistoryEntry.SOURCE_READING_LIST);
ReadingList.DAO.makeListMostRecent(readingList);
startActivity(PageActivity.newIntent(getContext(), entry, entry.getTitle()));
}
}
@Override
public boolean onLongClick(@Nullable ReadingListPage item) {
beginMultiSelect();
toggleSelectPage(item);
return true;
}
@Override
public void onThumbClick(@Nullable ReadingListPage item) {
onClick(item);
}
@Override
public void onActionClick(@Nullable ReadingListPage page, @NonNull PageItemView view) {
if (page == null || readingList == null) {
return;
}
bottomSheetPresenter.show(getChildFragmentManager(),
ReadingListItemActionsDialog.newInstance(page, readingList));
}
@Override
public void onSecondaryActionClick(@Nullable ReadingListPage page, @NonNull PageItemView view) {
if (page != null) {
toggleOffline(page);
}
}
}
private class SearchCallback extends SearchActionModeCallback {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
actionMode = mode;
appBarLayout.setExpanded(false, true);
return super.onCreateActionMode(mode, menu);
}
@Override
protected void onQueryChange(String s) {
setSearchQuery(s);
}
@Override
public void onDestroyActionMode(ActionMode mode) {
super.onDestroyActionMode(mode);
actionMode = null;
setSearchQuery(null);
}
@Override
protected String getSearchHintString() {
return getString(R.string.search_hint_search_reading_list);
}
}
private class MultiSelectCallback extends MultiSelectActionModeCallback {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
actionMode = mode;
return super.onCreateActionMode(mode, menu);
}
@Override
protected void onDelete() {
deleteSelectedPages();
finishActionMode();
}
@Override
public void onDestroyActionMode(ActionMode mode) {
unselectAllPages();
actionMode = null;
super.onDestroyActionMode(mode);
}
}
private final class PageListCountCallback extends CallbackTask.DefaultCallback<ReadingListPage> {
private ReadingListPage page;
private PageListCountCallback(@NonNull ReadingListPage page) {
this.page = page;
}
@Override public void success(ReadingListPage fromDb) {
if (fromDb.listKeys().size() > 1) {
showMultiListPageConfirmToggleDialog(page);
} else {
toggleOffline(page);
}
}
}
private final class ConfirmRemoveFromOfflineListener implements DialogInterface.OnClickListener {
private ReadingListPage page;
private ConfirmRemoveFromOfflineListener(@NonNull ReadingListPage page) {
this.page = page;
}
@Override public void onClick(DialogInterface dialog, int which) {
toggleOffline(page);
}
}
}