package org.wikipedia.history;
import android.content.DialogInterface;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
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.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
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.helper.ItemTouchHelper;
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 org.wikipedia.BackPressedHandler;
import org.wikipedia.R;
import org.wikipedia.WikipediaApp;
import org.wikipedia.activity.FragmentUtil;
import org.wikipedia.database.DatabaseClient;
import org.wikipedia.database.contract.PageHistoryContract;
import org.wikipedia.page.PageTitle;
import org.wikipedia.util.FeedbackUtil;
import org.wikipedia.views.DefaultViewHolder;
import org.wikipedia.views.MultiSelectActionModeCallback;
import org.wikipedia.views.PageItemView;
import org.wikipedia.views.SearchEmptyView;
import org.wikipedia.views.SwipeableItemTouchHelperCallback;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
import static org.wikipedia.Constants.HISTORY_FRAGMENT_LOADER_ID;
public class HistoryFragment extends Fragment implements BackPressedHandler {
public interface Callback {
void onLoadPage(PageTitle title, HistoryEntry entry);
void onClearHistory();
}
private Unbinder unbinder;
@BindView(R.id.history_list) RecyclerView historyList;
@BindView(R.id.history_empty_container) View historyEmptyView;
@BindView(R.id.search_empty_view) SearchEmptyView searchEmptyView;
private WikipediaApp app;
private String currentSearchQuery;
private LoaderCallback loaderCallback = new LoaderCallback();
private HistoryEntryItemAdapter adapter = new HistoryEntryItemAdapter();
private ItemCallback itemCallback = new ItemCallback();
private ActionMode actionMode;
private SearchActionModeCallback searchActionModeCallback = new HistorySearchCallback();
private MultiSelectCallback multiSelectCallback = new MultiSelectCallback();
private HashSet<Integer> selectedIndices = new HashSet<>();
@NonNull public static HistoryFragment newInstance() {
return new HistoryFragment();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
app = WikipediaApp.getInstance();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_history, container, false);
unbinder = ButterKnife.bind(this, view);
searchEmptyView.setEmptyText(R.string.search_history_no_results);
ItemTouchHelper.Callback touchCallback = new SwipeableItemTouchHelperCallback(getContext());
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(touchCallback);
itemTouchHelper.attachToRecyclerView(historyList);
historyList.setLayoutManager(new LinearLayoutManager(getContext()));
historyList.setAdapter(adapter);
getActivity().getSupportLoaderManager().initLoader(HISTORY_FRAGMENT_LOADER_ID, null, loaderCallback);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onDestroyView() {
getActivity().getSupportLoaderManager().destroyLoader(HISTORY_FRAGMENT_LOADER_ID);
historyList.setAdapter(null);
adapter.setCursor(null);
unbinder.unbind();
unbinder = null;
super.onDestroyView();
}
@Override
public void setUserVisibleHint(boolean visible) {
if (!isAdded()) {
return;
}
if (!visible && actionMode != null) {
actionMode.finish();
}
}
@Override
public boolean onBackPressed() {
if (actionMode != null) {
actionMode.finish();
return true;
}
return false;
}
private void updateEmptyState(@Nullable String searchQuery) {
if (TextUtils.isEmpty(searchQuery)) {
searchEmptyView.setVisibility(View.GONE);
historyEmptyView.setVisibility(adapter.isEmpty() ? View.VISIBLE : View.GONE);
} else {
searchEmptyView.setVisibility(adapter.isEmpty() ? View.VISIBLE : View.GONE);
historyEmptyView.setVisibility(View.GONE);
}
historyList.setVisibility(adapter.isEmpty() ? View.GONE : View.VISIBLE);
}
@Override
public void onDestroy() {
super.onDestroy();
app.getRefWatcher().watch(this);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_history, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
boolean isHistoryAvailable = !adapter.isEmpty();
menu.findItem(R.id.menu_clear_all_history)
.setVisible(isHistoryAvailable)
.setEnabled(isHistoryAvailable);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_clear_all_history:
new AlertDialog.Builder(getContext())
.setTitle(R.string.dialog_title_clear_history)
.setMessage(R.string.dialog_message_clear_history)
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Clear history!
new DeleteAllHistoryTask(app).execute();
onClearHistoryClick();
}
})
.setNegativeButton(R.string.no, null).create().show();
return true;
case R.id.menu_search_history:
if (actionMode == null) {
actionMode = ((AppCompatActivity) getActivity())
.startSupportActionMode(searchActionModeCallback);
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void onPageClick(PageTitle title, HistoryEntry entry) {
Callback callback = callback();
if (callback != null) {
callback.onLoadPage(title, entry);
}
}
private void onClearHistoryClick() {
Callback callback = callback();
if (callback != null) {
callback.onClearHistory();
}
}
private void finishActionMode() {
if (actionMode != null) {
actionMode.finish();
}
}
private void beginMultiSelect() {
if (HistorySearchCallback.is(actionMode)) {
finishActionMode();
}
if (!MultiSelectCallback.is(actionMode)) {
((AppCompatActivity) getActivity()).startSupportActionMode(multiSelectCallback);
}
}
private void toggleSelectPage(@Nullable IndexedHistoryEntry indexedEntry) {
if (indexedEntry == null) {
return;
}
if (selectedIndices.contains(indexedEntry.getIndex())) {
selectedIndices.remove(indexedEntry.getIndex());
} else {
selectedIndices.add(indexedEntry.getIndex());
}
int selectedCount = selectedIndices.size();
if (selectedCount == 0) {
finishActionMode();
} else if (actionMode != null) {
actionMode.setTitle(getString(R.string.multi_select_items_selected, selectedCount));
}
adapter.notifyDataSetChanged();
}
private void unselectAllPages() {
selectedIndices.clear();
adapter.notifyDataSetChanged();
}
private void deleteSelectedPages() {
List<HistoryEntry> selectedEntries = new ArrayList<>();
for (int index : selectedIndices) {
HistoryEntry entry = adapter.getItem(index);
if (entry != null) {
selectedEntries.add(entry);
app.getDatabaseClient(HistoryEntry.class).delete(entry,
PageHistoryContract.PageWithImage.SELECTION);
}
}
selectedIndices.clear();
if (!selectedEntries.isEmpty()) {
showDeleteItemsUndoSnackbar(selectedEntries);
adapter.notifyDataSetChanged();
}
}
private void showDeleteItemsUndoSnackbar(final List<HistoryEntry> entries) {
String message = entries.size() == 1
? String.format(getString(R.string.history_item_deleted), entries.get(0).getTitle().getDisplayText())
: String.format(getString(R.string.history_items_deleted), entries.size());
Snackbar snackbar = FeedbackUtil.makeSnackbar(getActivity(), message,
FeedbackUtil.LENGTH_DEFAULT);
snackbar.setAction(R.string.history_item_delete_undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
DatabaseClient<HistoryEntry> client = app.getDatabaseClient(HistoryEntry.class);
for (HistoryEntry entry : entries) {
client.upsert(entry, PageHistoryContract.PageWithImage.SELECTION);
}
adapter.notifyDataSetChanged();
}
});
snackbar.show();
}
private void restartLoader() {
getActivity().getSupportLoaderManager().restartLoader(HISTORY_FRAGMENT_LOADER_ID, null, loaderCallback);
}
private class LoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String titleCol = PageHistoryContract.PageWithImage.TITLE.qualifiedName();
String selection = null;
String[] selectionArgs = null;
String searchStr = currentSearchQuery;
if (!TextUtils.isEmpty(searchStr)) {
searchStr = searchStr.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
selection = "UPPER(" + titleCol + ") LIKE UPPER(?) ESCAPE '\\'";
selectionArgs = new String[]{"%" + searchStr + "%"};
}
Uri uri = PageHistoryContract.PageWithImage.URI;
final String[] projection = null;
String order = PageHistoryContract.PageWithImage.ORDER_MRU;
return new CursorLoader(getContext().getApplicationContext(),
uri, projection, selection, selectionArgs, order);
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
adapter.setCursor(cursor);
if (!isAdded()) {
return;
}
updateEmptyState(currentSearchQuery);
getActivity().supportInvalidateOptionsMenu();
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.setCursor(null);
}
}
private static class IndexedHistoryEntry {
private final int index;
@NonNull private final HistoryEntry entry;
IndexedHistoryEntry(@NonNull HistoryEntry entry, int index) {
this.entry = entry;
this.index = index;
}
public int getIndex() {
return index;
}
@NonNull public HistoryEntry getEntry() {
return entry;
}
}
private class HistoryEntryItemHolder extends DefaultViewHolder<PageItemView<IndexedHistoryEntry>>
implements SwipeableItemTouchHelperCallback.Callback {
private int index;
HistoryEntryItemHolder(PageItemView<IndexedHistoryEntry> itemView) {
super(itemView);
}
void bindItem(@NonNull Cursor cursor) {
index = cursor.getPosition();
IndexedHistoryEntry indexedEntry
= new IndexedHistoryEntry(HistoryEntry.DATABASE_TABLE.fromCursor(cursor), index);
getView().setItem(indexedEntry);
getView().setTitle(indexedEntry.getEntry().getTitle().getDisplayText());
getView().setDescription(indexedEntry.getEntry().getTitle().getDescription());
getView().setImageUrl(PageHistoryContract.PageWithImage.IMAGE_NAME.val(cursor));
getView().setSelected(selectedIndices.contains(indexedEntry.getIndex()));
// Check the previous item, see if the times differ enough
// If they do, display the section header.
// Always do it this is the first item.
String curTime = getDateString(indexedEntry.getEntry().getTimestamp());
String prevTime = "";
if (cursor.getPosition() != 0) {
cursor.moveToPrevious();
HistoryEntry prevEntry = HistoryEntry.DATABASE_TABLE.fromCursor(cursor);
prevTime = getDateString(prevEntry.getTimestamp());
cursor.moveToNext();
}
getView().setHeaderText(curTime.equals(prevTime) ? null : curTime);
}
private String getDateString(Date date) {
return DateFormat.getDateInstance().format(date);
}
@Override
public void onSwipe() {
selectedIndices.add(index);
deleteSelectedPages();
}
}
private final class HistoryEntryItemAdapter extends RecyclerView.Adapter<HistoryEntryItemHolder> {
@Nullable private Cursor cursor;
@Override
public int getItemCount() {
return cursor == null ? 0 : cursor.getCount();
}
public boolean isEmpty() {
return getItemCount() == 0;
}
@Nullable public HistoryEntry getItem(int position) {
if (cursor == null) {
return null;
}
int prevPosition = cursor.getPosition();
cursor.moveToPosition(position);
HistoryEntry entry = HistoryEntry.DATABASE_TABLE.fromCursor(cursor);
cursor.moveToPosition(prevPosition);
return entry;
}
public void setCursor(@Nullable Cursor newCursor) {
if (cursor == newCursor) {
return;
}
if (cursor != null) {
cursor.close();
}
cursor = newCursor;
this.notifyDataSetChanged();
}
@Override
public HistoryEntryItemHolder onCreateViewHolder(ViewGroup parent, int type) {
return new HistoryEntryItemHolder(new PageItemView<IndexedHistoryEntry>(getContext()));
}
@Override
public void onBindViewHolder(HistoryEntryItemHolder holder, int pos) {
if (cursor == null) {
return;
}
cursor.moveToPosition(pos);
holder.bindItem(cursor);
}
@Override public void onViewAttachedToWindow(HistoryEntryItemHolder holder) {
super.onViewAttachedToWindow(holder);
holder.getView().setCallback(itemCallback);
}
@Override public void onViewDetachedFromWindow(HistoryEntryItemHolder holder) {
holder.getView().setCallback(null);
super.onViewDetachedFromWindow(holder);
}
}
private class ItemCallback implements PageItemView.Callback<IndexedHistoryEntry> {
@Override
public void onClick(@Nullable IndexedHistoryEntry indexedEntry) {
if (MultiSelectCallback.is(actionMode)) {
toggleSelectPage(indexedEntry);
} else if (indexedEntry != null) {
HistoryEntry newEntry = new HistoryEntry(indexedEntry.getEntry().getTitle(), HistoryEntry.SOURCE_HISTORY);
onPageClick(indexedEntry.getEntry().getTitle(), newEntry);
}
}
@Override
public boolean onLongClick(@Nullable IndexedHistoryEntry indexedEntry) {
beginMultiSelect();
toggleSelectPage(indexedEntry);
return true;
}
@Override
public void onThumbClick(@Nullable IndexedHistoryEntry indexedEntry) {
onClick(indexedEntry);
}
@Override
public void onActionClick(@Nullable IndexedHistoryEntry entry, @NonNull PageItemView view) {
}
@Override
public void onSecondaryActionClick(@Nullable IndexedHistoryEntry entry, @NonNull PageItemView view) {
}
}
private class HistorySearchCallback extends SearchActionModeCallback {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
actionMode = mode;
return super.onCreateActionMode(mode, menu);
}
@Override
protected void onQueryChange(String s) {
currentSearchQuery = s;
restartLoader();
}
@Override
public void onDestroyActionMode(ActionMode mode) {
super.onDestroyActionMode(mode);
if (!TextUtils.isEmpty(currentSearchQuery)) {
currentSearchQuery = "";
restartLoader();
}
actionMode = null;
}
@Override
protected String getSearchHintString() {
return getContext().getResources().getString(R.string.search_hint_search_history);
}
}
private class MultiSelectCallback extends MultiSelectActionModeCallback {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
actionMode = mode;
selectedIndices.clear();
return super.onCreateActionMode(mode, menu);
}
@Override
protected void onDelete() {
deleteSelectedPages();
finishActionMode();
}
@Override
public void onDestroyActionMode(ActionMode mode) {
unselectAllPages();
actionMode = null;
super.onDestroyActionMode(mode);
}
}
@Nullable private Callback callback() {
return FragmentUtil.getCallback(this, Callback.class);
}
}