/*
* Copyright (c) 2015 Jonas Kalderstam.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nononsenseapps.notepad.ui.list;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.SwipeRefreshLayout;
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.util.Log;
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 com.nononsenseapps.notepad.R;
import com.nononsenseapps.notepad.data.model.sql.DAO;
import com.nononsenseapps.notepad.data.model.sql.Task;
import com.nononsenseapps.notepad.data.model.sql.TaskList;
import com.nononsenseapps.notepad.data.receiver.SyncStatusMonitor;
import com.nononsenseapps.notepad.data.service.gtasks.SyncHelper;
import com.nononsenseapps.notepad.ui.common.DialogDeleteCompletedTasks;
import com.nononsenseapps.notepad.ui.common.DialogMoveToList;
import com.nononsenseapps.notepad.ui.common.DialogPassword;
import com.nononsenseapps.notepad.ui.common.DialogPassword.PasswordConfirmedListener;
import com.nononsenseapps.notepad.ui.common.MenuStateController;
import com.nononsenseapps.notepad.util.ArrayHelper;
import com.nononsenseapps.notepad.util.AsyncTaskHelper;
import com.nononsenseapps.notepad.util.SharedPreferencesHelper;
import java.security.InvalidParameterException;
import java.util.Collection;
import java.util.Set;
import java.util.TreeSet;
public class TaskListFragment extends Fragment
implements OnSharedPreferenceChangeListener, SyncStatusMonitor.OnSyncStartStopListener,
ActionMode.Callback {
// Must be less than -1
public static final String LIST_ALL_ID_PREF_KEY = "show_all_tasks_choice_id";
public static final int LIST_ID_ALL = -2;
public static final int LIST_ID_OVERDUE = -3;
public static final int LIST_ID_TODAY = -4;
public static final int LIST_ID_WEEK = -5;
public static final String LIST_ID = "list_id";
public static final int LOADER_TASKS = 1;
public static final int LOADER_CURRENT_LIST = 0;
private static final String TAG = "TaskListFragment";
RecyclerView listView;
SimpleSectionsAdapter mAdapter;
SyncStatusMonitor syncStatusReceiver = null;
private long mListId = -1;
private TaskListFragment.TaskListCallbacks mListener;
private String mSortType = null;
private int mRowCount = 3;
private LoaderCallbacks<Cursor> mCallback = null;
private ActionMode mMode;
private SwipeRefreshLayout mSwipeRefreshLayout;
private ItemTouchHelper touchHelper;
private SelectedItemHandler selectedItemHandler;
public TaskListFragment() {
super();
}
public static TaskListFragment getInstance(final long listId) {
TaskListFragment f = new TaskListFragment();
Bundle args = new Bundle();
args.putLong(LIST_ID, listId);
f.setArguments(args);
return f;
}
public static String whereOverDue() {
return Task.Columns.DUE + " BETWEEN " + Task.OVERDUE + " AND " + Task.TODAY_START;
}
public static String andWhereOverdue() {
return " AND " + whereOverDue();
}
public static String whereToday() {
return Task.Columns.DUE + " BETWEEN " + Task.TODAY_START + " AND " + Task.TODAY_PLUS(1);
}
public static String andWhereToday() {
return " AND " + whereToday();
}
public static String whereWeek() {
return Task.Columns.DUE + " BETWEEN " + Task.TODAY_START + " AND (" + Task.TODAY_PLUS(5) +
" -1)";
}
public static String andWhereWeek() {
return " AND " + whereWeek();
}
void loadList() {
listView.setLayoutManager(new LinearLayoutManager(getActivity()));
listView.setHasFixedSize(true);
// TODO separators
touchHelper = new ItemTouchHelper(new DragHandler());
listView.setAdapter(mAdapter);
touchHelper.attachToRecyclerView(listView);
// TODO jonas
/*listView.setMultiChoiceModeListener(new MultiChoiceModeListener() {
final HashMap<Long, Task> tasks = new HashMap<Long, Task>();
// ActionMode mMode;
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Must setup the contextual action menu
final MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.fragment_tasklist_context, menu);
// Must clear for reuse
tasks.clear();
// For password
mMode = mode;
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// Here you can perform updates to the CAB due to
// an invalidate() request
return false;
}
@Override
public boolean onActionItemClicked(final ActionMode mode, MenuItem item) {
// Respond to clicks on the actions in the CAB
boolean finish = false;
int itemId = item.getItemId();
if (itemId == R.id.menu_delete) {
deleteTasks(tasks);
finish = true;
} else if (itemId == R.id.menu_switch_list) {
// show move to list dialog
DialogMoveToList.getInstance(tasks.keySet().toArray(new Long[tasks.size()]))
.show(getFragmentManager(), "move_to_list_dialog");
finish = true;
} else if (itemId == R.id.menu_share) {
startActivity(getShareIntent());
finish = true;
} else {
finish = false;
}
if (finish) {
mode.finish(); // Action picked, so close the CAB
}
return finish;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// Here you can make any necessary updates to the activity when
// the CAB is removed. By default, selected items are
// deselected/unchecked.
tasks.clear();
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
boolean checked) {
if (checked) {
tasks.put(id, new Task(mAdapter.getCursor(position)));
} else {
tasks.remove(id);
}
// Display in action bar total number of selected items
mode.setTitle(Integer.toString(tasks.size()));
}
String getShareText() {
final StringBuilder sb = new StringBuilder();
final boolean locked = SharedPreferencesHelper.isPasswordSet(getActivity());
for (Task t : tasks.values()) {
if (sb.length() > 0) {
sb.append("\n\n");
}
if (locked) {
sb.append(t.title);
} else {
sb.append(t.getText());
}
}
return sb.toString();
}
String getShareSubject() {
String result = "";
for (Task t : tasks.values()) {
result += ", " + t.title;
}
return result.length() > 0 ? result.substring(2) : result;
}
Intent getShareIntent() {
final Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, getShareText());
shareIntent.putExtra(Intent.EXTRA_SUBJECT, getShareSubject());
shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
return shareIntent;
}
});*/
}
/**
* Delete tasks and display a snackbar with an undo action
*
*/
private void deleteTasks(final Set<Long> orgItemIds) {
// If any are locked, ask for password first
final boolean locked = SharedPreferencesHelper.isPasswordSet(getActivity());
// copy to avoid threading issues
final Set<Long> itemIds = new TreeSet<>();
itemIds.addAll(orgItemIds);
final PasswordConfirmedListener pListener = new PasswordConfirmedListener() {
@Override
public void onPasswordConfirmed() {
AsyncTaskHelper.background(new AsyncTaskHelper.Job() {
@Override
public void doInBackground() {
for (Long id: itemIds) {
try {
Task.delete(id, getActivity());
} catch (Exception ignored) {
Log.e(TAG, "doInBackground:" + ignored.getMessage());
}
}
}
});
}
};
if (locked) {
DialogPassword delpf = new DialogPassword();
delpf.setListener(pListener);
delpf.show(getFragmentManager(), "multi_delete_verify");
} else {
// Just run it directly
Log.d(TAG, "deleteTasks: run it");
pListener.onPasswordConfirmed();
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
mListener = (TaskListFragment.TaskListCallbacks) getActivity();
} catch (ClassCastException e) {
throw new ClassCastException("Activity must implement " + "OnFragmentInteractionListener");
}
// We want to be notified of future changes to auto refresh
PreferenceManager.getDefaultSharedPreferences(context)
.registerOnSharedPreferenceChangeListener(this);
}
void setupSwipeToRefresh() {
// Set the offset so it comes out of the correct place
final int toolbarHeight = getResources().getDimensionPixelOffset(R.dimen.toolbar_height);
mSwipeRefreshLayout
.setProgressViewOffset(false, -toolbarHeight, Math.round(0.7f * toolbarHeight));
// The arrow will cycle between these colors (in order)
mSwipeRefreshLayout
.setColorSchemeResources(R.color.refresh_progress_1, R.color.refresh_progress_2,
R.color.refresh_progress_3);
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
boolean syncing = SyncHelper.onManualSyncRequest(getActivity());
if (!syncing) {
// Do not show refresh view
mSwipeRefreshLayout.setRefreshing(false);
}
}
});
}
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
setHasOptionsMenu(true);
selectedItemHandler = new SelectedItemHandler((AppCompatActivity) getActivity(), this);
syncStatusReceiver = new SyncStatusMonitor();
if (getArguments().getLong(LIST_ID, -1) == -1) {
throw new InvalidParameterException("Must designate a list to open!");
}
mListId = getArguments().getLong(LIST_ID, -1);
// Start loading data
mAdapter = new SimpleSectionsAdapter(this, getActivity());
// Set a drag listener
// TODO jonas
/*mAdapter.setDropListener(new DropListener() {
@Override
public void drop(int from, int to) {
Log.d("nononsenseapps drag", "Position from " + from + " to " + to);
final Task fromTask = new Task((Cursor) mAdapter.getItem(from));
final Task toTask = new Task((Cursor) mAdapter.getItem(to));
fromTask.moveTo(getActivity().getContentResolver(), toTask);
}
});*/
}
/**
* Called to have the fragment instantiate its user interface view. This is optional, and
* non-graphical fragments can return null (which is the default implementation). This will be
* called between {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}. <p/> <p>If you
* return a View from here, you will later be called in {@link #onDestroyView} when the view is
* being released.
*
* @param inflater The LayoutInflater object that can be used to inflate any views in the fragment,
* @param container If non-null, this is the parent view that the fragment's UI should be attached to. The
* fragment should not add the view itself, but this can be used to generate the LayoutParams
* of the view.
* @param savedInstanceState If non-null, this fragment is being re-constructed from a previous saved state as given
* here.
* @return Return the View for the fragment's UI, or null.
*/
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//return super.onCreateView(inflater, container, savedInstanceState);
View rootView = inflater.inflate(R.layout.fragment_task_list, container, false);
listView = (RecyclerView) rootView.findViewById(android.R.id.list);
loadList();
// ListView will only support scrolling ToolBar off-screen from Lollipop onwards.
// RecyclerView does not have this limitation
ViewCompat.setNestedScrollingEnabled(listView, true);
// setup swipe to refresh
mSwipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swiperefresh);
setupSwipeToRefresh();
return rootView;
}
@Override
public void onActivityCreated(final Bundle state) {
super.onActivityCreated(state);
// Get the global list settings
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
mCallback = new LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle arg1) {
if (id == LOADER_CURRENT_LIST) {
return new CursorLoader(getActivity(), TaskList.getUri(mListId), TaskList.Columns.FIELDS,
null, null, null);
} else {
// What sorting to use
Uri targetUri;
String sortSpec;
if (mSortType == null) {
mSortType = prefs
.getString(getString(R.string.pref_sorttype), getString(R.string.default_sorttype));
}
// All-view can't use manual sorting
if (mListId < 1 && mSortType.equals(getString(R.string.const_possubsort))) {
mSortType = getString(R.string.const_all_default_sorting);
}
if (mSortType.equals(getString(R.string.const_alphabetic))) {
targetUri = Task.URI;
sortSpec = getString(R.string.const_as_alphabetic, Task.Columns.TITLE);
} else if (mSortType.equals(getString(R.string.const_duedate))) {
targetUri = Task.URI_SECTIONED_BY_DATE;
sortSpec = null;
} else if (mSortType.equals(getString(R.string.const_modified))) {
targetUri = Task.URI;
sortSpec = Task.Columns.UPDATED + " DESC";
}
// manual sorting
else {
targetUri = Task.URI;
sortSpec = Task.Columns.LEFT;
}
String where = null;
String[] whereArgs = null;
if (mListId > 0) {
where = Task.Columns.DBLIST + " IS ?";
whereArgs = new String[]{Long.toString(mListId)};
}
return new CursorLoader(getActivity(), targetUri, Task.Columns.FIELDS, where, whereArgs,
sortSpec);
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
if (loader.getId() == LOADER_TASKS) {
mAdapter.swapCursor(c);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (loader.getId() == LOADER_TASKS) {
mAdapter.swapCursor(null);
}
}
};
getLoaderManager().restartLoader(LOADER_TASKS, null, mCallback);
}
/**
* Called when the fragment is visible to the user and actively running. This is generally tied to
* {@link Activity#onResume() Activity.onResume} of the containing Activity's lifecycle.
*/
@Override
public void onResume() {
super.onResume();
// activate monitor
if (syncStatusReceiver != null) {
syncStatusReceiver.startMonitoring(getActivity(), this);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
/**
* Called when the Fragment is no longer resumed. This is generally tied to {@link
* Activity#onPause() Activity.onPause} of the containing Activity's lifecycle.
*/
@Override
public void onPause() {
//mSwipeRefreshLayout.setRefreshing(false);// deactivate monitor
if (syncStatusReceiver != null) {
syncStatusReceiver.stopMonitoring();
}
super.onPause();
}
@Override
public void onDestroy() {
super.onDestroy();
getLoaderManager().destroyLoader(0);
}
@Override
public void onDetach() {
mListener = null;
PreferenceManager.getDefaultSharedPreferences(getActivity())
.unregisterOnSharedPreferenceChangeListener(this);
super.onDetach();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.fragment_tasklist, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
if (getActivity() instanceof MenuStateController) {
final boolean visible = ((MenuStateController) getActivity()).childItemsVisible();
menu.setGroupVisible(R.id.list_menu_group, visible);
if (!visible) {
if (mMode != null) {
mMode.finish();
}
}
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_clearcompleted:
if (mListId != -1) {
DialogDeleteCompletedTasks.showDialog(getFragmentManager(), mListId, null);
}
return true;
default:
return false;
}
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
if (isDetached()) {
// Fix crash report
return;
}
try {
boolean reload = false;
if (key.equals(getString(R.string.pref_sorttype))) {
mSortType = null;
reload = true;
}
if (reload && mCallback != null) {
getLoaderManager().restartLoader(LOADER_TASKS, null, mCallback);
}
} catch (IllegalStateException ignored) {
// Fix crash report
// Might get a race condition where fragment is detached when getString is called
}
}
/**
* @param ongoing
*/
@Override
public void onSyncStartStop(boolean ongoing) {
mSwipeRefreshLayout.setRefreshing(ongoing);
}
public ItemTouchHelper getTouchHelper() {
return touchHelper;
}
public int getRowCount() {
return mRowCount;
}
public String getSortType() {
return mSortType;
}
public TaskListCallbacks getListener() {
return mListener;
}
public SelectedItemHandler getSelectedItemHandler() {
return selectedItemHandler;
}
public long getListId() {
return mListId;
}
@Override
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
actionMode.getMenuInflater().inflate(R.menu.fragment_tasklist_context, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem item) {
// Respond to clicks on the actions in the CAB
final boolean finish;
int itemId = item.getItemId();
if (itemId == R.id.menu_delete) {
deleteTasks(selectedItemHandler.getSelected());
finish = true;
}
else if (itemId == R.id.menu_switch_list) {
// show move to list dialog
DialogMoveToList.getInstance(selectedItemHandler.getSelected())
.show(getFragmentManager(), "move_to_list_dialog");
finish = true;
} else if (itemId == R.id.menu_share) {
shareSelected(selectedItemHandler.getSelected());
finish = true;
} else {
finish = false;
}
if (finish) {
actionMode.finish(); // Action picked, so close the CAB
}
return finish;
}
private void shareSelected(Collection<Long> orgItemIds) {
// This solves threading issues
final long[] itemIds = ArrayHelper.toArray(orgItemIds);
AsyncTaskHelper.background(new AsyncTaskHelper.Job() {
@Override
public void doInBackground() {
final StringBuilder shareSubject = new StringBuilder();
final StringBuilder shareText = new StringBuilder();
final String whereId = new StringBuilder(Task.Columns._ID)
.append(" IN (").append(DAO.arrayToCommaString(itemIds))
.append(")").toString();
Cursor c = getContext().getContentResolver().query(Task.URI,
new String[] {Task.Columns._ID, Task.Columns.TITLE, Task.Columns.NOTE},
whereId, null, null);
if (c != null) {
while (c.moveToNext()) {
if (shareText.length() > 0) {
shareText.append("\n\n");
}
if (shareSubject.length() > 0) {
shareSubject.append(", ");
}
shareSubject.append(c.getString(1));
shareText.append(c.getString(1));
if (!c.getString(2).isEmpty()) {
shareText.append("\n").append(c.getString(2));
}
}
c.close();
}
final Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, shareText.toString());
shareIntent.putExtra(Intent.EXTRA_SUBJECT, shareSubject.toString());
startActivity(shareIntent);
}
});
}
@Override
public void onDestroyActionMode(ActionMode actionMode) {
//jonas
selectedItemHandler.clear();
mAdapter.notifyDataSetChanged();
}
/**
* This interface must be implemented by activities that contain TaskListFragments to allow an
* interaction in this fragment to be communicated to the activity and potentially other fragments
* contained in that activity.
*/
public interface TaskListCallbacks {
void openTask(final Uri uri, final long listId, final View origin);
}
class DragHandler extends ItemTouchHelper.Callback {
private static final String TAG = "DragHandler";
public DragHandler() {
super();
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public int getMovementFlags(final RecyclerView recyclerView,
final RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = 0;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(final RecyclerView recyclerView, final RecyclerView.ViewHolder viewHolder,
final RecyclerView.ViewHolder target) {
final Task fromTask = new Task(mAdapter.getCursor(viewHolder.getAdapterPosition()));
final Task toTask = new Task(mAdapter.getCursor(target.getAdapterPosition()));
mAdapter.notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
fromTask.moveTo(getActivity().getContentResolver(), toTask);
return true;
}
@Override
public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int direction) {
}
}
}